Unreal 5/FPS Shooting

3. [Unreal 5 / C++] 동적 크로스헤어 구현

돼지표 2025. 1. 3. 21:10

이번 시간에는 플레이어의 움직임(속도)에 비례하여 동적으로 변화하는 크로스헤어를 구현해볼 것이다.

 

우선 크로스헤어에 필요한 Texture를 구해준다.
여기서 동적으로 움직이는 부분은 상하좌우의 Line이고, 중간의 Dot은 정적이니 필수는 아니다.


CrosshairWidget 클래스 작성

UserWidget을 상속받는 클래스를 작성하고, 상하좌우 Line에 해당하는 이미지를 바인딩한다.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Image.h"
#include "CrosshairWidget.generated.h"

UCLASS()
class PROJECT_4_API UCrosshairWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
    UPROPERTY(meta = (BindWidget))
    UImage* LineTop;

    UPROPERTY(meta = (BindWidget))
    UImage* LineBottom;

    UPROPERTY(meta = (BindWidget))
    UImage* LineLeft;

    UPROPERTY(meta = (BindWidget))
    UImage* LineRight;

    // 위치 업데이트 함수
    UFUNCTION(BlueprintCallable, Category = "Crosshair")
    void UpdateCrosshairSize(const float Speed);
};

 

Speed를 매개변수로 받아 각 Line의 위치를 조절하는 함수를 작성한다.

// Fill out your copyright notice in the Description page of Project Settings.
#include "Player/Ui/CrosshairWidget.h"

void UCrosshairWidget::UpdateCrosshairSize(const float Speed)
{
    if (!LineTop || !LineBottom || !LineLeft || !LineRight)
        return;

    // 위치 계산 (간격은 Speed, FireRate, Recoil 값을 기반으로 설정)
    float Offset = FMath::Clamp(Speed * 0.1f, 0, 50.0f);

    // 각 라인의 위치 조정
    LineTop->SetRenderTranslation(FVector2D(0, -Offset));
    LineBottom->SetRenderTranslation(FVector2D(0, Offset));
    LineLeft->SetRenderTranslation(FVector2D(-Offset, 0));
    LineRight->SetRenderTranslation(FVector2D(Offset, 0));
}

Widget Blueprint 생성

만든 C++클래스를 기반으로한 블루프린트 클래스를 생성해준다.

 

생성한 위젯 블루프린트에 들어가면 처음에는 아무것도 없을 것이다.
여기에 캔버스 패널, 중심점, 상하좌우 Line에 해당하는 Image를 추가해준다.

 

UpdateCrosshairSize 함수에서 Line들의 위치를 0~50 사이의 값으로 지정해주기 때문에, 렌더 트랜스폼의 위치는 0, 0으로 고정된 채 앵커를 조절하여 Line의 위치를 조절해야 한다.

 

또한, 계층구조에서 이름으로 해당 이미지를 찾아 바인드를 하기 때문에 각 위치에 맞게 이름을 정확하게 기입해야 한다.


UiComponent 클래스 작성

Ui 위젯들을 총괄할 UiComponent 클래스를 만든다.
UiComponent를 캐릭터에 붙여 현재 캐릭터에 크로스헤어를 만들 수 있다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Blueprint/UserWidget.h"
#include "CrosshairWidget.h"
#include "UiComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class PROJECT_4_API UUiComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	UUiComponent();

	// Sets default values for this component's properties
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:
	float GetAimSize() const { return AimSize; }
	void SetAimSize(float Size) { AimSize = Size; }

	UPROPERTY(EditAnywhere, Category = "UI")
	UCrosshairWidget *CrosshairWidget;

private:
	void UpdateCrosshair(float DeltaTime);

	float AimSize;
	float MaxAimSize;
	float MinAimSize;

};

 

UiComponent의 BeginPlay에서 우리가 만든 크로스헤어위젯을 뷰포트에 추가한다.

void UUiComponent::BeginPlay()
{
	Super::BeginPlay();

    if (CrosshairWidget) CrosshairWidget->AddToViewport();
}

 


속도와 점프 상태에 따른 AimSize 조정

현재 플레이어 캐릭터의 속도를 가져와 AimSize를 구한다.

  • 캐릭터가 점프 중일 땐 속도에 관계없이 AimSize가 증가한다.
  • 캐릭터의 속도가 100 이상일 때는 속도에 비례하여 증가하고,
    속도가 100 이하일 때는 감소하며, 속도가 0일 때는 매우 빠르게 감소한다.

여기서 AimSize는 0과 미리 정해둔 사이즈 이상으로는 커지지 않게 Clamp를 사용하여 제한하고 마지막으로 UpdateCrosshairSize  함수에 계산된 AimSize를 전달한다. 

void UUiComponent::UpdateCrosshair(float DeltaTime)
{
    if (!CrosshairWidget) return;
    // Example: Update crosshair based on actor's velocity
    AActor* Owner = GetOwner();
    if (Owner)
    {
        FVector Velocity = Owner->GetVelocity();
        float Speed = Velocity.Size();
        if (Speed == 0 && AimSize == 0)return; // 불필요한 계산 최적화

        bool IsJumping = Cast<ACharacter>(Owner)->GetCharacterMovement()->IsFalling();
        
        if (IsJumping) {
            AimSize += 2000.f * DeltaTime; // 빠르게 증가
        }
        else {
            if (Speed > 100)
            {
                AimSize += Speed * DeltaTime; // 빠르게 증가
            }
            else if (Speed > 0)
            {
                AimSize -= FMath::Clamp(Speed * DeltaTime * 50, 0.0f, AimSize); // 점진적으로 감소
            }
            else
            {
                AimSize -= FMath::Clamp(2000.f * DeltaTime, 0.0f, AimSize); // 빠르게 감소
            }
        }
        // 최소 크기 제한
        AimSize = FMath::Clamp(AimSize, MinAimSize, MaxAimSize);
        CrosshairWidget->UpdateCrosshairSize(AimSize);
    }
}

 

해당 함수를 Tick에서 호출하여 동적으로 움직이는 크로스헤어를 구현할 수 있다.

void UUiComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	
    UpdateCrosshair(DeltaTime);
}