UE5 탑다운뷰 프로젝트 분석

1000·2023년 7월 3일
0

UE5 Study

목록 보기
1/8

UE5에서 기본으로 제공하는 탑다운뷰 프로젝트를 생성하여 분석해보자!
카메라는 캐릭터를 따라다니며 탑다운뷰로 고정되어 있고
캐릭터는 마우스를 클릭한 방향으로 이동하게 되는 방식입니다.

Combat이라는 이름으로 프로젝트는 생성하였으며, 각 파일 및 클래스에 대한 설명을 서술합니다.

1. Combat.h

DECLARE_LOG_CATEGORY_EXTERN(LogCombat, Log, All);

프로젝트 명으로 생성되는 파일로, 주로 프로젝트에 범용적으로 사용되는 로그나 전처리기를 작성합니다.

2. ACombatGameMode

AGameModeBase를 상속받는 클래스입니다.

ACombatGameMode::ACombatGameMode()
{
	// use our custom PlayerController class
	PlayerControllerClass = ACombatPlayerController::StaticClass();

	// set default pawn class to our Blueprinted character
	static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/TopDown/Blueprints/BP_TopDownCharacter"));
	if (PlayerPawnBPClass.Class != nullptr)
	{
		DefaultPawnClass = PlayerPawnBPClass.Class;
	}

	// set default controller to our Blueprinted controller
	static ConstructorHelpers::FClassFinder<APlayerController> PlayerControllerBPClass(TEXT("/Game/TopDown/Blueprints/BP_TopDownPlayerController"));
	if(PlayerControllerBPClass.Class != NULL)
	{
		PlayerControllerClass = PlayerControllerBPClass.Class;
	}
}

PlayerControllerClass는 커스텀한 ControllerClass를 사용하도록 하고
DefaultPawnClass 또한 커스텀한 Character BP를 사용하도록 합니다.

3. ACombatPlayerController

UCLASS()
class ACombatPlayerController : public APlayerController
{
	GENERATED_BODY()

public:
	ACombatPlayerController();

	/** Time Threshold to know if it was a short press */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	float ShortPressThreshold;

	/** FX Class that we will spawn when clicking */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	UNiagaraSystem* FXCursor;

	/** MappingContext */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input, meta=(AllowPrivateAccess = "true"))
	class UInputMappingContext* DefaultMappingContext;
	
	/** Jump Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input, meta=(AllowPrivateAccess = "true"))
	class UInputAction* SetDestinationClickAction;

	/** Jump Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Input, meta=(AllowPrivateAccess = "true"))
	class UInputAction* SetDestinationTouchAction;

protected:
	/** True if the controlled character should navigate to the mouse cursor. */
	uint32 bMoveToMouseCursor : 1;

	virtual void SetupInputComponent() override;
	
	// To add mapping context
	virtual void BeginPlay();

	/** Input handlers for SetDestination action. */
	void OnInputStarted();
	void OnSetDestinationTriggered();
	void OnSetDestinationReleased();
	void OnTouchTriggered();
	void OnTouchReleased();

private:
	FVector CachedDestination; // 목적지 좌표 캐싱

	bool bIsTouch; // Is it a touch device
	float FollowTime; // For how long it has been pressed
};

APlayerController 컨트롤러를 상속받는 클래스입니다.
Input을 받아 터치한 방향으로 캐릭터를 이동시키고, 목적지에 터치 이펙트를 남기는 기능을 합니다.
마우스 클릭 이벤트와 터치 이벤트의 핸들러가 다르므로 둘다 받을 수 있도록 이벤트를 정의합니다.

ACombatPlayerController::ACombatPlayerController()
{
	bShowMouseCursor = true;
	DefaultMouseCursor = EMouseCursor::Default;
	CachedDestination = FVector::ZeroVector;
	FollowTime = 0.f;
}

ACombatPlayerController의 생성자에서 bShowMouseCursor를 true로 하여 게임내에서 마우스 커서가 보이도록 합니다.
DefaultMouseCursor 마우스 커서는 Default로 셋팅한다. 이것은 마우스의 모양을 설정할 수 있으므로 변경이 필요하다면 설정할 수 있다.

void ACombatPlayerController::BeginPlay()
{
	// Call the base class  
	Super::BeginPlay();

	//Add Input Mapping Context
	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
	{
		Subsystem->AddMappingContext(DefaultMappingContext, 0);
	}
}

BeginPlay에서는 향상된 입력 플러그인(Enhanced Input)을 사용할 수 있도록 합니다.
향상된 입력은 UE 5.1부터 기본으로 포함되는 플러그인으로 Input 관리를 보다 편리하게 해줍니다.

void ACombatPlayerController::SetupInputComponent()
{
	// set up gameplay key bindings
	Super::SetupInputComponent();

	// Set up action bindings
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent))
	{
		// Setup mouse input events
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Started, this, &ACombatPlayerController::OnInputStarted);
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Triggered, this, &ACombatPlayerController::OnSetDestinationTriggered);
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Completed, this, &ACombatPlayerController::OnSetDestinationReleased);
		EnhancedInputComponent->BindAction(SetDestinationClickAction, ETriggerEvent::Canceled, this, &ACombatPlayerController::OnSetDestinationReleased);

		// Setup touch input events
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Started, this, &ACombatPlayerController::OnInputStarted);
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Triggered, this, &ACombatPlayerController::OnTouchTriggered);
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Completed, this, &ACombatPlayerController::OnTouchReleased);
		EnhancedInputComponent->BindAction(SetDestinationTouchAction, ETriggerEvent::Canceled, this, &ACombatPlayerController::OnTouchReleased);
	}
}

SetupInputComponent 함수에서 EnhancedInputComponent에 마우스 Input과 터치 Input을 각각 바인딩해줍니다.
ETriggerEvent::Started는 Input이 최초로 감지되었을 때 호출 됩니다.
ETriggerEvent::Triggered는 터치 입력이 있을 때 매 frame마다 Event가 발생합니다.

void ACombatPlayerController::OnInputStarted()
{
	StopMovement();
}

ETriggerEvent::Started일 때는 기존 이동을 멈춰줍니다.

// Triggered every frame when the input is held down
void ACombatPlayerController::OnTouchTriggered()
{
	bIsTouch = true;
	OnSetDestinationTriggered();
}

void ACombatPlayerController::OnTouchReleased()
{
	bIsTouch = false;
	OnSetDestinationReleased();
}

OnTouchTriggered()와 OnTouchReleased() 터치가 Triggered/Released 되었을 때 호출 됩니다.
Triggerd함수에서 마우스의 Input인지 터치에 의한 Input인지 구분하기 위해 bIsTouch flag를 설정해줍니다.

// Triggered every frame when the input is held down
void ACombatPlayerController::OnSetDestinationTriggered()
{
	// We flag that the input is being pressed
	FollowTime += GetWorld()->GetDeltaSeconds();
	
	// We look for the location in the world where the player has pressed the input
	FHitResult Hit;
	bool bHitSuccessful = false;
	if (bIsTouch)
	{
		bHitSuccessful = GetHitResultUnderFinger(ETouchIndex::Touch1, ECollisionChannel::ECC_Visibility, true, Hit);
	}
	else
	{
		bHitSuccessful = GetHitResultUnderCursor(ECollisionChannel::ECC_Visibility, true, Hit);
	}

	// If we hit a surface, cache the location
	if (bHitSuccessful)
	{
		CachedDestination = Hit.Location;
	}
	
	// Move towards mouse pointer or touch
	APawn* ControlledPawn = GetPawn();
	if (ControlledPawn != nullptr)
	{
		FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
		ControlledPawn->AddMovementInput(WorldDirection, 1.0, false);
	}
}

OnSetDestinationTriggered() 함수는 마우스와 터치 Input이 Triggered 되었을 때 호출됩니다.
매 Tick 마다 FollowTime를 누적하여 short Press 인지 long Press 인지 체크할 때 사용하도록 합니다.

bTouch flag에 따라 GetHitResultUnderFinger 혹은 GetHitResultUnderCursor 함수를 사용하여 HitResult를 가져옵니다.
bHitSuccessful가 true라면 CachedDestination에 목적지 위치를 캐싱합니다.

컨트롤하는 Pawn을 가져와 해당 Pawn의 노말 벡터와 목적지 위치를 이용하여 이동할 방향 벡터를 설정해줍니다.

void ACombatPlayerController::OnSetDestinationReleased()
{
	// If it was a short press
	if (FollowTime <= ShortPressThreshold)
	{
		// We move there and spawn some particles
		UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, CachedDestination);
		UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, FXCursor, CachedDestination, FRotator::ZeroRotator, FVector(1.f, 1.f, 1.f), true, true, ENCPoolMethod::None, true);
	}

	FollowTime = 0.f;
}

OnSetDestinationReleased() 함수는 마우스와 터치의 Input이 Released 되었을 때 호출됩니다.
해당 함수에서는 short press로 판별되면 목적지 위치에 터치 파티클을 생성해주도록 합니다.

4. ACombatCharacter

UCLASS(Blueprintable)
class ACombatCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ACombatCharacter();

	// Called every frame.
	virtual void Tick(float DeltaSeconds) override;

	/** Returns TopDownCameraComponent subobject **/
	FORCEINLINE class UCameraComponent* GetTopDownCameraComponent() const { return TopDownCameraComponent; }
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }

private:
	/** Top down camera */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* TopDownCameraComponent;

	/** Camera boom positioning the camera above the character */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;
};

ACombatCharacter 클래스는 이동하는 캐릭터 클래스입니다.
ACharacter클래스를 상속받으며, CameraComponent와 SpringArmComponent를 가지고 있습니다.
Tick을 상속받지만 기본 생성 시 별다른 동작을 하진 않습니다. 필요할 때 사용하면 될 것 같습니다.

ACombatCharacter::ACombatCharacter()
{
	// Set size for player capsule
	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

	// Don't rotate character to camera direction
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	// Configure character movement
	GetCharacterMovement()->bOrientRotationToMovement = true; // Rotate character to moving direction
	GetCharacterMovement()->RotationRate = FRotator(0.f, 640.f, 0.f);
	GetCharacterMovement()->bConstrainToPlane = true;
	GetCharacterMovement()->bSnapToPlaneAtStart = true;

	// Create a camera boom...
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);
	CameraBoom->SetUsingAbsoluteRotation(true); // Don't want arm to rotate when character does
	CameraBoom->TargetArmLength = 800.f;
	CameraBoom->SetRelativeRotation(FRotator(-60.f, 0.f, 0.f));
	CameraBoom->bDoCollisionTest = false; // Don't want to pull camera in when it collides with level

	// Create a camera...
	TopDownCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("TopDownCamera"));
	TopDownCameraComponent->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
	TopDownCameraComponent->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

	// Activate ticking in order to update the cursor every frame.
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = true;
}

ACombatCharacter의 생성자에서 하는 일을 살펴보도록 합니다.

먼저 캐릭터의 캡슐형 충돌체의 사이즈를 설정합니다.
그리고 캐릭터가 카메라의 방향으로 회전하지 않도록 UseControllerRotation 플래그들을 false로 셋팅합니다.

캐릭터 무브먼트 또한 설정하며, bOrientRotationToMovement를 true로 하여 캐릭터를 이동시킬 때 이동 방향으로 회전시킵니다. 그리고 RotationRate를 설정하여 캐릭터가 회전할 때 초당 640도의 회전 속도로 회전시켜 이동하게 합니다. bConstrainToPlane를 true로 하여 캐릭터의 이동을 평면으로 제한하고, 시작할 때 캐릭터의 위치가 평면을 벗어난 상태라면 가까운 평면에 붙여서 시작하도록 합니다. 여기서 평면이랑 네비게이션 메시를 의미합니다.

다음으로 SpringArmComponent를 생성하고 설정합니다.
SpringArmComponent는 카메라가 캐릭터에게서 적당한 위치를 유지하도록 합니다.
UsingAbsoluteRotation을 설정하여 캐릭터가 회전할 때 SpringArmComponent가 같이 회전하지 않도록 합니다. 즉, 상위 컴포넌트의 회전을 따르지 않고 월드 좌표계의 회전을 따르도록 합니다. TargetArmLength는 카메라와 캐릭터의 거리를 설정하는 변수로, 800으로 설정합니다. RelativeRotation을 설정하여 SpringArm을 회전시켜 카메라가 위에서 캐릭터를 내려다보도록 설정합니다. bDoCollisionTest는 카메라가 벽 등 충돌체에 닿으면 카메라와 캐릭터의 거리를 좁혀 카메라가 벽을 뚫지 않도록 해주는 변수이지만, 현재 프로젝트에서는 사용하지 않습니다.

CameraComponent를 생성하고 설정합니다.
SetupAttachment 함수를 이용하여 SpringArm에 카메라를 부착합니다.

마지막으로 커서 위치를 받아와 이동해야 하므로 Tick을 켜주도록 합니다.

이렇게 에디터를 실행하게 되면 캐릭터가 마우스를 클릭한 위치로 이동하며, 터치 이펙트가 생성되는 것을 볼 수 있습니다.

profile
Game Programming

0개의 댓글