Unreal/Ureal Engine 5 정리

Chapter 05. Character

GameStudy 2023. 8. 19. 16:34

5.1 캐릭터

5.1-1 캐릭터 Vs 폰

  - ACharacter Vs. APawn

   인간형 폰을 좀 더 효과적으로 제작하기 위한 특수한 클래스가 캐릭터 클래스.

    ACharacter는 APawn 클래스를 상속 받고 있음을 볼 수 있음.

    CapsuleComponent와 SkeletalMeshComponent를 사용하는 것은 같지만,

    ACharacter는 CharacterMovementComponent를 사용함.

    그리고 각 컴포넌트가 Private 접근 지정자라서 게터를 활용해야 함.

 

  - CharacterMovement Vs. FloatingPawnMovement

    점프와 같은 중력을 반영한 움직임을 제공함.

    걷기 외에도 기어가기, 날아가기, 수영하기 등의 다양한 이동 모드 설정가능.

    멀티 플레이 네트워크 환경에서 캐릭터들의 움직임을 자동으로 동기화함.

 

  - 캐릭터 무브먼트 컴포넌트의 이동 옵션(EMovementMode)

    Movement Mode: None, Walking, Falling, ...

    이동 기능을 끄고 싶으면 None 모드

    이동 모드에서의 이동 수치: MaxWalkSpeed

    폴링 모드에서의 점프 수치: JumpZVelocity

 

5.1-2 Enhanced Input System

  - 기존 입력 시스템은 입력 관련 설정을 해둔 뒤에 게임 로직에서 입력 값을 처리함.(이전 단원에서 배움)

    이런 방식은 런타임 중에 사용자의 키세팅 변경에 대응할 수 없음.

    그래서 플레이어의 최종 입력을 게임 로직에서 진행할 수 있도록 시스템을 변경함.

 

  - 향상된 입력 시스템 동작 구성

    사용자의 입력 데이터를 최종 함수에 매핑하는 과정을 체계적으로 구성

    입력을 처리하는 주체는 액션. 입력과 액션이 연결됨.

    액션에선 어떻게 들어온 입력을 재가공(Modifier)할지

    어떻게 입력 이벤트를 활성화(Trigger)할지의 설정 진행.

    마지막으로 게임 로직에서 작성한 특정 함수와 매핑 진행.

    플랫폼에 따른 다양한 입력 장치의 설정

      ex. 게임 패드용 입력 매핑 컨텍스트, 키보드용 입력 매핑 컨텍스트

    입력 값의 변환(Modifier)

      ex. AD/WS 입력 값을 Y축과 X축으로 변경, 값 반전의 처리

    이벤트 발생 조건의 상세 설정(Trigger)

      ex. 일반 버튼인가? 축 이동인가? 일정 이상 눌러야 하는가?

 

  - 과거에는 Modifier와 Trigger의 설정을 게임로직에서 설정함.

    향상된 입력 시스템을 통해 게임 로직의 부담을 줄이고,

    런타임에서 우리가 원하는 설정들을 자유롭게 변경 가능하게끔 함.

 

  - 플러그인과 모듈 추가

    플러그인을 추가하고자 한다면 .uproject 파일을 수정해야함. .uproject 파일은 프로젝트 폴더에 있음.

    A 모듈에 B 모듈을 추가하고자 한다면 A 모듈의 .Build.cs 파일을 수정해야함.

    프로젝트 폴더 > Source > StudyProject 폴더에 StudyProject.Build.cs 파일이 있음.

    이 경우 A 모듈은 StudyProject 모듈(주 게임 모듈)이고 B 모듈은 EnhancedInput 플러그인 속 EnhancedInput 모듈임.

<hide/>

StudyProject.uproject

{
  "FileVersion": 3,
  "EngineAssociation": "5.1",
  "Category": "",
  "Description": "",
  "Modules": [
    {
      "Name": "StudyProject",
      "Type": "Runtime",
      "LoadingPhase": "Default",
      "AdditionalDependencies": [
        "Engine"
      ]
    }
  ],
  "Plugins": [
    {
      "Name": "ModelingToolsEditorMode",
      "Enabled": true,
      "TargetAllowList": [
        "Editor"
      ]
    },
    {
      "Name": "EnhancedInput",
      "Enabled": true
    }
  ]
}
<hide/>

// StudyProject.Build.cs

using UnrealBuildTool;

public class StudyProject : ModuleRules
{
    public StudyProject(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
    
        PublicDependencyModuleNames.AddRange(new string[] 
        {
            // Initial Modules
            "Core", "CoreUObject", "Engine", "InputCore",
        
            // Input
            "EnhancedInput",
        });

        PrivateDependencyModuleNames.AddRange(new string[] {  });

    }
}

 

  - InputAction 애셋 생성

    Content Browser > StudyProject 우클릭 > 새 폴더 "Inputs"

    Input 폴더 우클릭 > 새 폴더 "InputActions"

    InputActions > 새 Input 애셋 > Input Action > "IA_Move" 생성

    IA_Move는 Axis mapping임. 그것도 전후, 좌우 2D.

    따라서 Value Type에 Vector2D.

 

  - InputConfig 생성

    새 C++ 클래스 > DataAsset 부모 클래스 > "SInputConfigData"

    Path > Inputs

    InputConfigData 클래스는 보통 레벨과 폰에 따라 별도로 만듦.

    일단 강의에서는 SInputConfigData 하나로 모두 해결. 각자 포트폴리오에서 여러 개를 활용해봐도 좋음.

<hide/>

// SInputConfigData.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "SInputConfigData.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API USInputConfigData : public UDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TObjectPtr<class UInputAction> MoveAction;
    
};
<hide/>

// SInputConfigData.cpp


#include "Inputs/SInputConfigData.h"

 

  - 블루프린트 InputConfig 애셋 생성

    Content Browser > StudyProject > Inputs 우클릭 > 새 폴더 "InputConfigs"

    InputConfigs > 새 Miscellaneous 애셋 > Data Asset 부모 클래스 > "IC_PlayerCharacter"

    IC_PlayerCharacter 더블클릭 후 MoveAction에 IA_Move 지정.

 

  - InputMappingContext 생성

    Content Browser > StudyProject > Inputs 우클릭 > 새 폴더 "InputMappingContexts"

    InputMappingContexts 우클릭 > 새 Input 애셋

    Input Mapping Context 부모 클래스 > "IMC_PlayerCharacter" 

    아래 그림을 참고하여 설정.

 

5.1-3 캐릭터 생성

  - 캐릭터 클래스 생성

    새 C++ 클래스 > Character 부모 클래스 > "SPlayerCharacter"

    Path > Characters

<hide/>

// SPlayerCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "SPlayerCharacter.generated.h"

UCLASS()
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    ASPlayerCharacter();

    virtual void PossessedBy(AController* NewController) override;

protected:
    virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

private:
    void Move(const FInputActionValue& InValue);

private:
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TObjectPtr<class USpringArmComponent> SpringArmComponent;
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TObjectPtr<class UCameraComponent> CameraComponent;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TObjectPtr<class USInputConfigData> PlayerCharacterInputConfigData;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TObjectPtr<class UInputMappingContext> PlayerCharacterInputMappingContext;

};
<hide/>

// SPlayerCharacter.h


#include "Characters/SPlayerCharacter.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Inputs/SInputConfigData.h"

ASPlayerCharacter::ASPlayerCharacter()
{
    PrimaryActorTick.bCanEverTick = false;

    float CharacterHalfHeight = 90.f;
    float CharacterRadius = 40.f;

    GetCapsuleComponent()->InitCapsuleSize(CharacterRadius, CharacterHalfHeight);

#pragma region InitializeSkeletalMesh
    FVector PivotPosition(0.f, 0.f, -CharacterHalfHeight);
    FRotator PivotRotation(0.f, -90.f, 0.f);
    GetMesh()->SetRelativeLocationAndRotation(PivotPosition, PivotRotation);
#pragma endregion

#pragma region InitializeSpringArm
    SpringArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComponent"));
    SpringArmComponent->SetupAttachment(RootComponent);
    SpringArmComponent->TargetArmLength = 400.f;
    SpringArmComponent->SetRelativeRotation(FRotator(-15.f, 0.f, 0.f));
#pragma endregion

#pragma region InitializeCamera
    CameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
    CameraComponent->SetupAttachment(SpringArmComponent);
#pragma endregion

    GetCharacterMovement()->MaxWalkSpeed = 500.f;                 // 이동 속도.
    GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;            // 조이스틱과 관련된 속도.
    GetCharacterMovement()->JumpZVelocity = 700.f;                // 점프 속도.
    GetCharacterMovement()->AirControl = 0.35f;                   // 체공시 움직임 제어 계수.
    GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;  // 정지 계수.

}

void ASPlayerCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);
}

void ASPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    if (APlayerController* PlayerController = Cast<APlayerController>(NewController))
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
        {
            Subsystem->AddMappingContext(PlayerCharacterInputMappingContext, 0);
        }
    }

    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        EnhancedInputComponent->BindAction(PlayerCharacterInputConfigData->MoveAction, ETriggerEvent::Triggered, this, &ASPlayerCharacter::Move);
    }
}

void ASPlayerCharacter::Move(const FInputActionValue& InValue)
{
    FVector2D MovementVector = InValue.Get<FVector2D>();

    AddMovementInput(GetActorForwardVector(), MovementVector.X);
    AddMovementInput(GetActorRightVector(), MovementVector.Y);
}

 

  - 캐릭터 블루프린트 애셋 생성

    Content Browser > StudyProject > Characters > 새 블루프린트 애셋 > SPlayerCharacter 부모 클래스

    "BP_PlayerCharacter" 생성 후 Details > ASPlayerCharacter 탭 > InputConfigData와 InputMappingContext 지정.

    SkeletalMesh와 Animation Blueprint도 지정.

    Override Input Component Class에는 EnhancedInputComponent 지정.

 

  - 새로 만든 캐릭터를 기본 폰 클래스로 설정

    World Settings > Selected GameMode > Default Pawn Class에 BP_PlayerCharacter 지정.

 

  - 블루프린트 애셋을 적극 활용하는 이유.

    C++ 코드에서 오브젝트 패스를 활용해서 로직을 짜다보면 아트팀에서 폴더를 변경하거나

    이름을 변경하면 CDO 로드에러 혹은 플레이가 안될 수 있음.

    프로그래밍을 할 수 없는 아트팀은 당황해버림. 칼퇴했던 프로그래머가 돌아와야할수도..

    물론 이마저도 팀바팀이기에 사수님이 하시는데로 따라하는게 정답임.

 

  - GameMode C++ 클래스에서 하드코딩으로 애셋 지정하기

<hide/>

// SGameMode.cpp


...
#include "Characters/SPlayerCharacter.h"

ASGameMode::ASGameMode()
{
    ...

    static ConstructorHelpers::FClassFinder<ASPlayerCharacter> DefaultPlayerCharacterClassRef(TEXT("오브젝트패스"));
    if (DefaultPlayerCharacterClassRef.Class)
    {
        DefaultPawnClass = DefaultPlayerCharacterClassRef.Class;
    }
}

 

5.2 시점 구현

5.2-1 Control Rotation

  - 시점 구현 선행 지식

    Backview 혹은 Quarterview 같은 시점을 구현하기 위해서는 컨트롤 로테이션에 대한 개념이 잡혀야함.

    시점 뿐만아니라, 앞으로 컨텐츠 프로그래머가 되어서 차량 혹은 관전자 등의 기능을 구현할 때도

    필수적으로 컨트롤 로테이션에 대한 개념이 제대로 박혀 있어야 원하는대로 구현 가능.

 

  - 플레이어 컨트롤러

    게임 세계의 물리적인 요소가 아직 고려되지 않은

    플레이어의 의지 관련된 데이터를 관리함.

    플레이어 컨트롤러는 플레이어의 의지를 나타내는

    컨트롤 회전(Control Rotation)이라는 멤버 변수를 관리함.

  - 

    게임 세계에서 물리적인 제약을 가지기 때문에,

    현재 캐릭터가 처한 물리적인 상황을 고려함.

    폰이 관리하는 대표적인 멤버 변수는 속도임.

 

 

 

  - 컨트롤 회전 실습

    마우스 움직임에 따라 폰이 회전해야 할 최종 목표 회전 값을 설정.

    이를 향해 폰이 일정한 속도로 회전하는 기능을 구현하고자 함.

    Content Browser > InputActions > 새 Input 애셋 > Input Action 부모 클래스 > "IA_Look"

    Value Type에 Vector2D.

    InputConfigs > IC_PlayerCharacter에도 IA_Look을 추가하기 위해서 SInputConfigData 클래스 파일 수정.

    IMC_PlayerCharacter에도 아래 그림과 같이 수정.

<hide/>

// SInputConfigData.h

...
class STUDYPROJECT_API USInputConfigData : public UDataAsset
{
    ...

public:
    ...

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TObjectPtr<class UInputAction> LookAction;
    
};

IA_Look 매핑 설정. Y축은 값을 반대로 해야 제대로 들어감.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

private:
    void Move(const FInputActionValue& InValue);

    void Look(const FInputActionValue& InValue);

private:
    ...

};
<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    ...

    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        EnhancedInputComponent->BindAction(PlayerCharacterInputConfigData->MoveAction, ETriggerEvent::Triggered, this, &ASPlayerCharacter::Move);
        EnhancedInputComponent->BindAction(PlayerCharacterInputConfigData->LookAction, ETriggerEvent::Triggered, this, &ASPlayerCharacter::Look);
    }
}

void ASPlayerCharacter::Move(const FInputActionValue& InValue)
{
    if (nullptr != GetController())
    {
        FVector2D MovementVector = InValue.Get<FVector2D>();

        const FRotator ControlRotation = GetController()->GetControlRotation();
        const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f); // 플레이어의 회전 의지 중 Yaw 성분으로만 전진 방향을 결정하고자 함.

        const FVector ForwardVector = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        const FVector RightVector = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

        AddMovementInput(ForwardVector, MovementVector.X);
        AddMovementInput(RightVector, MovementVector.Y);
    }    
}

void ASPlayerCharacter::Look(const FInputActionValue& InValue)
{
    if (nullptr != GetController())
    {
        FVector2D LookVector = InValue.Get<FVector2D>();

        AddControllerYawInput(LookVector.X); // 아까 IMC에서 X에다가 마우스 좌우 값을 넣었음.
        AddControllerPitchInput(LookVector.Y);
    }
}

 

  - 뷰포트에서 플레이어 컨트롤러의 컨트롤 회전값 확인하는 방법

    Shift + F1을 눌러서 포커싱 제거

    뷰포트를 클릭 후 틸드(~) 키를 눌러 콘솔 명령어 입력 창 생성

    아래와 같은 명령어를 작성하고 엔터

DisplayAll PlayerController ControlRotation

 

  - Controller과 Pawn, 그리고 컴포넌트간의 관계

    Controller - Pawn

    bUseControllerRotation- 속성에 의해서 결정됨.

    true이면 해당 Control Rotation 값이 Pawn의 회전에 영향을 줌.

      ex. Character 클래스는 기본적으로 bUseControllerRotationYaw가 true임.

        따라서 Control Rotation Yaw 값이 폰의 Yaw 값과 연동됨.

        반대로 bUseControllerRotationPitch는 false가 기본값. 연동되지 않음.

    이때 두 값의 동기화는 기본값으로 즉시 이뤄지게끔 되어 있음.

    Pawn - SpringArm Component

    bUsePawnControlRotation 속성에 의해서 폰의 회전 값이 연동될지 안될지 결정.

      ex. bUsePawnControlRotation이 false이면 폰이 회전해도 스프링암은 회전하지 않음.

        CutScene에 활용될 수 있음.

    bDoCollisionTest 속성은 폰과 카메라 사이에 벽이 있을 경우 카메라를 폰쪽으로 당길지 결정.

    bInherit- 속성에 의해서 Root Component의 회전 값이 연동될지 안될지 결정.

    Pawn - Camera Component

    bUsePawnControlRotation 속성에 의해서 폰의 회전 값이 연동될지 안될지 결정.

 

bDoCollisionTest. 3인칭 시점과 관련

 

  - 캐릭터 무브먼트의 회전 옵션

    Rotation Rate

    회전 속도의 지정

    Use Controller Desired Rotation

    컨트롤 회전을 목표 회전으로 삼고 지정한 속도로 돌리기

    Orient Rotation To Movement

    캐릭터 이동 방향에 회전을 일치시키기.

    폰의 회전 옵션과 충돌나면 움직임이 이상해지므로 주의.

 

  - 마우스 감도 조절

    UE4 이 버전에서는 InputPitchScale, InputYawScale, InputRollScale으로 조절했음.

    UE5 이상 버전에서는 Deprecated 되었음.

    따라서 Look() 함수의 인자로 전달되는 값에 계수를 곱하면서 마우스 감도 조절하면 됨.

 

5.2-2 BackView

  - BackView 구현 실습

    이전까지 배운 ControlRotation과 SpringArm 컴포넌트를 활용하여 3인칭 백뷰를 구현해보고자 함.

<hide/>

// SPlayerCharacter.h

...

UENUM()
enum class EViewMode : uint8
{
    BackView,
    End
};

UCLASS()
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

    void SetViewMode(EViewMode InViewMode);

protected:
    ...

private:
    ...

    EViewMode CurrentViewMode = EViewMode::End; // UPROPERTY() 매크로를 사용하지 않으므로, 초기화에 유념해야함.

};
<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::PossessedBy(AController* NewController)
{
    ...

    SetViewMode(EViewMode::BackView);
}

void ASPlayerCharacter::SetViewMode(EViewMode InViewMode)
{
    if (CurrentViewMode == InViewMode)
    {
        return;
    }

    CurrentViewMode = InViewMode;

    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        SpringArmComponent->TargetArmLength = 400.f;
        SpringArmComponent->SetRelativeRotation(FRotator::ZeroRotator); // ControlRotation이 Pawn의 회전과 동기화 -> Pawn의 회전이 SprintArm의 회전 동기화. 이로인해 SetRotation()이 무의미.

        bUseControllerRotationPitch = false;
        bUseControllerRotationYaw = false;
        bUseControllerRotationRoll = false;

        SpringArmComponent->bUsePawnControlRotation = true;
        SpringArmComponent->bDoCollisionTest = true;
        SpringArmComponent->bInheritPitch = true;
        SpringArmComponent->bInheritYaw = true;
        SpringArmComponent->bInheritRoll = false;

        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

...

 

  - BackView에 적합한 이동 구현

    대표적인 백뷰 게임으로 GTA가 있음. GTA의 이동을 생각해보면, 

    컨트롤 회전에 의해 카메라가 회전하면, 카메라 시선 방향으로 폰도 천천히 회전함.

 

  - 카메라 방향 얻어오기

    현재 bUsePawnControlRotation 속성에 의해 컨트롤 회전 값 == 스프링암 회전값 상태.

    컨트롤 회전 값이 카메라가 바라보는 방향임. 폰의 방향과 회전의 관계는 아래와 같음.

폰이 바라보는 방향 == X축 방향 (1, 0, 0) == 액터의 회전 값 (0, 0, 0)

    

  - 카메라 회전 행렬 만들기

    현재 컨트롤 회전의 Pitch 값은 의미가 없음. 마우스 Y를 올린다해도

    캐릭터가 허공으로 올려다 보면 안됨. 전투기라면 의미가 있음.

    즉, 컨트롤 회전의 Yaw 값만으로 회전 행렬 생성.

 

  - 이동 방향 구하기

    컨트롤 회전 값으로부터 회전 행렬을 생성 후 행렬의 축 성분을 추출.

    생성된 회전행렬에서 X축이 캐릭터가 이동할 ForwardVector. Y축이 RightVector.

 

  - 실습

<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::Move(const FInputActionValue& InValue)
{
    if (nullptr != GetController())
    {
        FVector2D MovementVector = InValue.Get<FVector2D>();

        switch (CurrentViewMode) // Tap + Enter시에 자동완성.
        { 
        case EViewMode::BackView:
        { // Switch-Case 구문 내에서 Scope를 지정하면 해당 Scope 내에서 변수 선언이 가능해짐.
            const FRotator ControlRotation = GetController()->GetControlRotation();
            const FRotator ControlRotationYaw(0.f, ControlRotation.Yaw, 0.f);

            const FVector ForwardVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::X);
            const FVector RightVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::Y);

            AddMovementInput(ForwardVector, MovementVector.X);
            AddMovementInput(RightVector, MovementVector.Y);

            break;
        }
        case EViewMode::End:
            break;
        default:
            AddMovementInput(GetActorForwardVector(), MovementVector.X);
            AddMovementInput(GetActorRightVector(), MovementVector.Y);

            break;
        }

    }    
}

void ASPlayerCharacter::Look(const FInputActionValue& InValue)
{
    if (nullptr != GetController())
    {
        FVector2D LookVector = InValue.Get<FVector2D>();

        switch (CurrentViewMode)
        {
        case EViewMode::BackView:
            AddControllerYawInput(LookVector.X);
            AddControllerYawInput(LookVector.Y);
            break;
        case EViewMode::End:
            break;
        default:
            break;
        }
    }
}

 

  - bOrientRotationToMovement 속성

    캐릭터가 시선 방향으로 잘 이동하지만, 캐릭터가 회전하지 않아서 어색함.

    월드의 X축을 향해 바라보면서 이동하고 있음.

    이는 캐릭터가 움직이는 방향으로 자동으로 회전시켜주는 캐릭터 무브먼트 컴포넌트의

    bOrientRotationToMovement 기능을 사용하면 손쉽게 해결 가능.

    회전 속도를 함께 지정해, 이동 방향으로 캐릭터가 부드럽게 회전하도록 기능을 추가함.

<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::SetViewMode(EViewMode InViewMode)
{
    ...

    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        ...

        GetCharacterMovement()->bOrientRotationToMovement = true;
        GetCharacterMovement()->bUseControllerDesiredRotation = false;
        GetCharacterMovement()->RotationRate = FRotator(0.f, 480.f, 0.f);

        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

...

 

  - bUseControllerRotationYaw와 GTA 시점 방식의 차이

    bUseControllerRotation을 true로 두면, 항상 카메라의 시선 방향으로 캐릭터가 회전.

    그러나 GTA 시점 방식은, 플레이어의 이동 키가 눌려야만

    카메라의 시점 방향으로 회전함. 미묘한 차이가 있음.

 

5.2-3 QuarterView

  - BackView 방식에서는 상하 키 입력과 좌우 키 입력을 각각 처리 했음.

    QuarterView 방식에서는 상하 좌우를 조합해 캐릭터의 회전과 이동이 이뤄짐.

<hide/>

// SPlayerCharacter.h

...

UENUM()
enum class EViewMode : uint8
{
    BackView,
    QuarterView,
    End
};

UCLASS()
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

    virtual void Tick(float DeltaSeconds) override;

    void SetViewMode(EViewMode InViewMode);

protected:
    ...

private:
    ...

    FVector DirectionToMove = FVector::ZeroVector;

};
<hide/>

// SPlayerCharacter.h


...

ASPlayerCharacter::ASPlayerCharacter()
{
    PrimaryActorTick.bCanEverTick = true;

    ...

}

void ASPlayerCharacter::PossessedBy(AController* NewController)
{
    ...

    SetViewMode(EViewMode::QuarterView);
}

void ASPlayerCharacter::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);

    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        break;
    case EViewMode::QuarterView:
    {
        if (KINDA_SMALL_NUMBER < DirectionToMove.SizeSquared())
        {
            GetController()->SetControlRotation(FRotationMatrix::MakeFromX(DirectionToMove).Rotator());
            AddMovementInput(DirectionToMove);
            DirectionToMove = FVector::ZeroVector;
        }
        break;
    }
    case EViewMode::End:
        break;
    default:
        break;
    }
}

void ASPlayerCharacter::SetViewMode(EViewMode InViewMode)
{
    ...

    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        ...

        break;
    case EViewMode::QuarterView:
        SpringArmComponent->TargetArmLength = 900.f;
        SpringArmComponent->SetRelativeRotation(FRotator(-45.f, 0.f, 0.f));

        bUseControllerRotationPitch = false;
        bUseControllerRotationYaw = false;
        bUseControllerRotationRoll = false;

        SpringArmComponent->bUsePawnControlRotation = false;
        SpringArmComponent->bDoCollisionTest = false;
        SpringArmComponent->bInheritPitch = false;
        SpringArmComponent->bInheritYaw = false;
        SpringArmComponent->bInheritRoll = false;

        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

...

void ASPlayerCharacter::Move(const FInputActionValue& InValue)
{
    if (nullptr != GetController())
    {
        ...

        switch (CurrentViewMode) 
        { 
        case EViewMode::BackView:
        { 
            ...
        }
        case EViewMode::QuarterView:
            DirectionToMove.X = MovementVector.X;
            DirectionToMove.Y = MovementVector.Y;

            break;
        case EViewMode::End:
            break;
        default:
            ...

            break;
        }

    }    
}

void ASPlayerCharacter::Look(const FInputActionValue& InValue)
{
    if (nullptr != GetController())
    {
        ...

        switch (CurrentViewMode)
        {
        case EViewMode::BackView:
            AddControllerYawInput(LookVector.X);
            AddControllerYawInput(LookVector.Y);
            break;
        case EViewMode::End:
            break;
        default:
            break;
        }
    }
}

 

  - 입력 관련 이벤트 함수와 Tick() 이벤트 함수

    플레이어의 입력 값에 따라 액터의 행동이 결정되어야 하므로,

    입력 관련 이벤트 함수를 먼저 호출해서 플레이어의 의지를 확인 후

    Tick() 이벤트 함수를 호출해서 플레이어의 입력에 대응할 액터의 최종 행동을 결정함.

    Tick() 이벤트 함수를 통해 게임 로직의 결과가 나온다면 Animation이 업데이트됨.

    주의해야 할 점은, 입력 관련 이벤트 함수는 해당 입력이 있을때만 매 프레임 호출됨.

입력 관련 이벤트 함수 > 액터의 Tick() 이벤트 함수 > 애니메이션 관련 이벤트 함수

 

  - FRotationMatrix

    언리얼 엔진의 FRotationMatrix는 회전된 좌표계 정보를 저장하는 행렬.

 

  - BackView 방식과 QuarterView 방식의 이동 방향 도출 

    BackView 방식

    컨트롤 회전 FRotator 값으로 회전 행렬을 생성하고,

    이를 토대로 변환된 좌표계의 X축, Y축 방향 도출.

    QuarterView 방식

    입력 이벤트 함수에서 X축 값과 Y축 값으로 방향 벡터 값을 생성하고

    이 방향 벡터를 캐릭터의 시선 방향으로 간주함. 그래서 MakeFromX() 함수 사용.

    직교하는 나머지 두 축을 구해 회전 행렬을 생성. 

    이 회전 행렬에 대응되는 FRotator 값을 얻어옴. 즉 GTA 방식과 반대로 흘러감.

 

  - 하나의 벡터로부터 회전 행렬을 구하는 함수

    MakeFromX(), MakeFromY(), MakeFromZ()

    ex. QuarterView 방식에서는 두 축의 입력을 합산한 최종 벡터 방향과

    캐릭터의 시선 방향(X축)이 일치해야 하므로 MakeFromX()가 사용 된 것.

 

  - bUseControllerDesiredRotation 속성

    현재 캐릭터는 키보드 조합에 따라 최소 45도 단위로 끊어져서 회전함.

    캐릭터 무브먼트의 bUseControllerDesiredRotation 속성을 체크하면

    컨트롤 회전이 가리키는 방향으로 캐릭터가 부드럽게 회전함.

    bUseControllerRotationYaw 속성을 해제하고 대신 bUseControllerDesiredRotation을 설정.

<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::SetViewMode(EViewMode InViewMode)
{
    ...

    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        ...

        break;
    case EViewMode::QuarterView:
        ...

        GetCharacterMovement()->bOrientRotationToMovement = false;
        GetCharacterMovement()->bUseControllerDesiredRotation = true;
        GetCharacterMovement()->RotationRate = FRotator(0.f, 480.f, 0.f);

        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

...

 

5.2-4 시점 전환

  - 시점 전환용 단축키 추가

    Content Browser > InputActions > 새 Input 애셋 > Input Action 부모 클래스 > "IA_ChangeView"

    SInputConfigData 클래스를 아래와 같이 수정하고 IC_PlayerCharacter에 IA_ChangeView 추가.

    IMC_PlayerCharacter에 IA_ChangeView를 추가하고 V키를 지정.

<hide/>

// SInputConfigData.h

...
class STUDYPROJECT_API USInputConfigData : public UDataAsset
{
    ...

public:
    ...

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TObjectPtr<class UInputAction> ChangeViewAction;
    
};
<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

private:
    ...

    void ChangeView(const FInputActionValue& InValue);

private:
    ...

};
<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    ...

    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        ...
        EnhancedInputComponent->BindAction(PlayerCharacterInputConfigData->ChangeViewAction, ETriggerEvent::Started, this, &ASPlayerCharacter::ChangeView);
    }
}

...

void ASPlayerCharacter::ChangeView(const FInputActionValue& InValue)
{
    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        SetViewMode(EViewMode::QuarterView);
        break;
    case EViewMode::QuarterView:
        SetViewMode(EViewMode::BackView);
        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

 

  - 컨트롤 방식 전환 시 스프링암의 길이 부드럽게 전환 구현

    SpringArm의 길이와 회전 값이 목표 값까지 각각 서서히 변경되도록

    FMath 클래스에서 제공하는 InterpTo() 함수를 활용

    InterpTo() 함수는 등속으로 목표 지점까지 진행하되, 목표 지점에 도달하면

    그 값에서 멈추는 함수임. float형은 FInterpTo(), FVector형은 VInterpTo()

    FRotator형은 RInterpTo()라는 세 가지 함수를 FMath에서 제공함.

<hide/>

// SPlayerCharacter.h

...
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

private:
    ...

    float DestArmLength = 0.f;

    float ArmLengthChangeSpeed = 3.f;

    FRotator DestArmRotation = FRotator::ZeroRotator;

    float ArmRotationChangeSpeed = 10.f;

};
<hide/>

// SPlayerCharacter.h


...

void ASPlayerCharacter::PossessedBy(AController* NewController)
{
    ...

    SetViewMode(EViewMode::BackView);
    DestArmLength = 450.f; // 초기화에서 한 번 지정해줘야 함.
    DestArmRotation = FRotator::ZeroRotator;
}

void ASPlayerCharacter::Tick(float DeltaSeconds)
{
    ...

    if (KINDA_SMALL_NUMBER < abs(DestArmLength - SpringArmComponent->TargetArmLength))
    {
        SpringArmComponent->TargetArmLength = FMath::FInterpTo(SpringArmComponent->TargetArmLength, DestArmLength, DeltaSeconds, ArmLengthChangeSpeed);
        SpringArmComponent->SetRelativeRotation(FMath::RInterpTo(SpringArmComponent->GetRelativeRotation(), DestArmRotation, DeltaSeconds, ArmRotationChangeSpeed));
    }
}

void ASPlayerCharacter::SetViewMode(EViewMode InViewMode)
{
    ...

    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        //SpringArmComponent->TargetArmLength = 400.f;
        //SpringArmComponent->SetRelativeRotation(FRotator::ZeroRotator); // ControlRotation이 Pawn의 회전과 동기화 -> Pawn의 회전이 SprintArm의 회전 동기화. 이로인해 SetRotation()이 무의미.

        ...

        break;
    case EViewMode::QuarterView:
        //SpringArmComponent->TargetArmLength = 900.f;
        //SpringArmComponent->SetRelativeRotation(FRotator(-45.f, 0.f, 0.f));

        ...

        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

...

void ASPlayerCharacter::ChangeView(const FInputActionValue& InValue)
{
    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        /* Case 1. 이전에 BackView 시점이었다면

          BackView는 컨트롤 회전값 == 스프링암 회전값.
          그러나 QuarterView는 캐릭터의 회전값 == 컨트롤 회전값.
          따라서 시점 변경 전에 캐릭터의 현재 회전값을 컨트롤 회전에 세팅해둬야 함.
          안그러면 컨트롤 회전이 일어나면서 현재 캐릭터의 회전값이 스프링암 회전값(컨트롤 회전값)으로 동기화됨.
        */
        GetController()->SetControlRotation(GetActorRotation());
        DestArmLength = 900.f;
        DestArmRotation = FRotator(-45.f, 0.f, 0.f);
        SetViewMode(EViewMode::QuarterView);
        
        break;
    case EViewMode::QuarterView:
        /* Case 2. 이전에 QuarterView 시점이었다면

          컨트롤 회전이 캐릭터 회전에 맞춰져 있을거임.
          //QuarterView는 현재 스프링암의 회전값을 컨트롤 회전에 세팅해둔 상태에서 시점 변경해야 올바름.
          BackView에서는 컨트롤 로테이션이 폰의 회전과 동기화되고 폰의 회전이 스프링 암의 회전과 동기화.
          따라서 스프링 암의 회전을 임의로 설정할 수 없음. 0으로 고정.
        */
        GetController()->SetControlRotation(FRotator::ZeroRotator);
        DestArmLength = 450.f;
        DestArmRotation = FRotator::ZeroRotator;
        SetViewMode(EViewMode::BackView);

        break;
    case EViewMode::End:
        break;
    default:
        break;
    }
}

 

5.2-5 Data Asset

  - 데이터 애셋

    UDataAsset을 상속 받은 언리얼 오브젝트 클래스

    에디터에서 애셋 형태로 편리하게 데이터를 관리할 수 있음.

    SPlayerCharacter 클래스의 거의 모든 코드가 시점 변경에 쓰이고 있는 것을 

    DataAsset을 통해 해결하기 해보고자 함.

 

  - 데이터 애셋 생성

    새 C++ 클래스 > PrimaryDataAsset 부모 클래스 > "SViewModeData"

    Path > Data

    아래와 같이 작성

<hide/>

// SViewModeData.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "SViewModeData.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API USViewModeData : public UPrimaryDataAsset
{
    GENERATED_BODY()
    
public:
    UPROPERTY(EditAnywhere, Category = ToPawn)
    uint8 bUseControllerRotationPitch : 1;

    UPROPERTY(EditAnywhere, Category = ToPawn)
    uint8 bUseControllerRotationYaw : 1;

    UPROPERTY(EditAnywhere, Category = ToPawn)
    uint8 bUseControllerRotationRoll : 1;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    uint8 bUsePawnControlRotation : 1;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    uint8 bDoCollisionTest : 1;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    uint8 bInheritPitch : 1;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    uint8 bInheritYaw : 1;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    uint8 bInheritRoll : 1;

    UPROPERTY(EditAnywhere, Category = ToPawn)
    FRotator RotationRate;

    UPROPERTY(EditAnywhere, Category = ToPawn)
    uint8 bOrientRotationToMovement : 1;

    UPROPERTY(EditAnywhere, Category = ToPawn)
    uint8 bUseControllerDesiredRotation : 1;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    float DestArmLength;

    UPROPERTY(EditAnywhere, Category = ToSpringArmComponent)
    FRotator DestArmRotation;
    
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = InputMappingContext)
    TObjectPtr<class UInputMappingContext> InputMappingContext;
    
};

 

  - USViewModeData 클래스를 상속 받는 데이터 애셋 생성

    Content Browser > StudyProject 우클릭 > 새 폴더 "DataAssets"

    DataAssets > Miscellaneous > 새 DataAsset 애셋 > SViewModeData 부모 클래스

    "DA_BackView" / "DA_QuarterView" 생성. 아래 그림을 참고하여 지정.

DA_BackView
DA_QuarterView

<hide/>

// SPlayerCharacter.h

...

UENUM()
enum class EViewMode : uint8
{
    None, // 블루프린트 클래스에서 TMap의 원소를 추가할 때 사용할 기본값.
    BackView,
    QuarterView,
    End
};

UCLASS()
class STUDYPROJECT_API ASPlayerCharacter : public ACharacter
{
    ...

public:
    ...

    void SetViewMode(EViewMode InViewMode);
    
    void SetViewModeWithDataAsset(const class USViewModeData* InCharacterViewModeData);

protected:
    ...

private:
    ...

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ASPlayerCharacter, meta = (AllowPrivateAccess))
    TMap<EViewMode, class USViewModeData*> CharacterViewModeManager;

};
<hide/>

// SPlayerCharacter.h

...
#include "Data/SViewModeData.h"

...

void ASPlayerCharacter::PossessedBy(AController* NewController)
{
    ...

    CurrentViewMode = EViewMode::BackView;
    SetViewModeWithDataAsset(CharacterViewModeManager[EViewMode::BackView]);
}

...

void ASPlayerCharacter::SetViewModeWithDataAsset(const USViewModeData* InCharacterViewModeData)
{
    bUseControllerRotationPitch = InCharacterViewModeData->bUseControllerRotationPitch;
    bUseControllerRotationYaw = InCharacterViewModeData->bUseControllerRotationYaw;
    bUseControllerRotationRoll = InCharacterViewModeData->bUseControllerRotationRoll;

    SpringArmComponent->bUsePawnControlRotation = InCharacterViewModeData->bUsePawnControlRotation;
    SpringArmComponent->bDoCollisionTest = InCharacterViewModeData->bDoCollisionTest;
    SpringArmComponent->bInheritPitch = InCharacterViewModeData->bInheritPitch;
    SpringArmComponent->bInheritYaw = InCharacterViewModeData->bInheritYaw;
    SpringArmComponent->bInheritRoll = InCharacterViewModeData->bInheritRoll;

    GetCharacterMovement()->bOrientRotationToMovement = InCharacterViewModeData->bOrientRotationToMovement;
    GetCharacterMovement()->bUseControllerDesiredRotation = InCharacterViewModeData->bUseControllerDesiredRotation;
    GetCharacterMovement()->RotationRate = InCharacterViewModeData->RotationRate;

    DestArmLength = InCharacterViewModeData->DestArmLength;
    DestArmRotation = InCharacterViewModeData->DestArmRotation;

    if (APlayerController* PC = Cast<APlayerController>(GetController()))
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
        {
            Subsystem->ClearAllMappings();

            if (UInputMappingContext* NewMappingContext = CharacterViewModeManager[CurrentViewMode]->InputMappingContext)
            {
                Subsystem->AddMappingContext(NewMappingContext, 0);
            }
        }
    }
}

...

void ASPlayerCharacter::ChangeView(const FInputActionValue& InValue)
{
    switch (CurrentViewMode)
    {
    case EViewMode::BackView:
        GetController()->SetControlRotation(GetActorRotation());
        CurrentViewMode = EViewMode::QuarterView;
        SetViewModeWithDataAsset(CharacterViewModeManager[CurrentViewMode]);
        
        break;
    case EViewMode::QuarterView:
        GetController()->SetControlRotation(FRotator::ZeroRotator);
        CurrentViewMode = EViewMode::BackView;
        SetViewModeWithDataAsset(CharacterViewModeManager[CurrentViewMode]);
    case EViewMode::End:
        break;
    default:
        break;
    }
}

BP_PlayerCharacter > Details 수정