Unreal/Ureal Engine 5 정리

Chapter 04. Pawn

GameStudy 2023. 8. 19. 11:18

4.1 Gameplay Framework

4.1-1 게임플레이 프레임워크

  - 언리얼 엔진을 통한 게임 제작이란,

    레벨 구성

    레벨에 WorldStatic과 같이 미리 배치되어 플레이어와 상호작용할 액터들을 배치하는 작업.

    게임 플레이 설계

    레벨 구성이 완료된 뒤, 정해진 규칙에 따라(프로그래밍 로직) 게임 플레이가 원활하게 진행되게끔 하는 작업.

    이 작업을 원활하게 진행되게끔 하기 위해 언리얼 엔진은 게임플레이 프레임워크를 제공함.

 

  - 게임플레이 프레임워크

    다양한 게임 장르와 멀티 플레이까지 수용할 수 있도록 언리얼이 자체적으로 설계한 프레임워크.

    게임플레이 프레임워크의 핵심 3가지 요소는

    언리얼 엔진 게임이 시작되기 위한 3가지 조건이기도 함.

    게임 모드(Game Mode)

    게임의 규칙을 관리하는 클래스.

    플레이어 컨트롤러(Player Controller)

    실질적으로 게임에 입장하는 플레이어.

    폰(Pawn)

    플레이어가 조종 할 수 있는 액터.

 

4.1-2 Game Mode

  - 게임 모드는 플레이어에게 보이지 않는 무형의 요소. 두 가지 역할을 함.

    첫 번째는 게임플레이 중 사건이 발생할 때 게임 진행의 심판 역할.

      ex. 슈팅 게임 중, 아군이 쏜 총알에 내가 데미지를 입을 것인지 말것인지.

    두 번째는 플레이어의 입장을 준비하는 역할.

      ex. 플레이어 컨트롤러, 플레이어가 빙의할 폰 지정, ...

 

  - 게임 서버와 게임 모드

    이후에 배우지만, 게임 모드는 서버와 밀접한 관계가 있음. 혹은 거의 동치.

    게임 모드는 하나의 PC에만 존재하게 됨. 

    만약 디아블로처럼 방을 파서 플레이하는 게임이라면(리슨 서버)

    방장의 PC에만 게임 모드 액터가 존재하게됨.

    혹은 데디케이티드 서버(클라 로직은 없고 오롯히 서버 로직만 담당함)의 경우엔

    데디 서버에만 게임 모드 액터가 존재하게 됨을 참고로 알아두자.

    예로들어 배그가 데디서버 방식인데, 데디서버를 직접적으로 뚫을 수 없으니

    게임 시작 직후 승자를 결정해서 내가 바로 승리하는 핵은 없음.

    게임 모드가 서버에만 존재하기 때문. 각 클라에는 존재하지 않아서 수정조차 불가능.

    클라 프로그래머가 잘못 작성한 RPC 코드 혹은 서버에서만 돌아야하는 코드를

    클라에서도 돌게해서 생기는 스피드핵이 대표적임.    

 

  - 게임 모드 생성

    새 C++ 클래스 > GameModeBase 부모 클래스 > "SGameMode"

    Path > Game

<hide/>

// SGameMode.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "SGameMode.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API ASGameMode : public AGameModeBase
{
    GENERATED_BODY()
    
};
<hide/>

// SGameMode.cpp


#include "Game/SGameMode.h"

 

  - 게임모드 설정 방법

    언리얼 에디터 > Toolbar > Settings > Project Settings

    Maps & Modes > Default Modes > Default GameMode에 SGameMode 지정하면

    해당 레벨에 지정된 게임 모드가 없다면 이 DefaultGameMode를 사용함.

    바로 밑 Selected GameMode에서 기본 폰 클래스 등을 지정할 수도 있음.

    특정 레벨에 특정 게임 모드를 지정하려면 Toolbar > Window > World Settings

    World Settings > GameMode Override에 SGameMode 지정.

    아래 Selected GameMode에서는 플레이어 컨트롤러와 폰도 지정 할 수 있음.

    그러나 SGameMode는 수정 할 수 없음. C++ 클래스기 때문.

 

  - 블루프린트 게임모드 애셋 생성

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

    Game > 새 블루프린트 애셋 > SGameMode 부모 클래스 > "BP_GameMode"

    World Settings > GameMode Override에 BP_GameMode를 지정해보면 수정 가능함을 알 수 있음.

 

4.1-3 Player Controller

  - 플레이어 컨트롤러

    게임 세계에서 플레이어를 대변하는 무형의 액터.

    플레이어 컨트롤러는 플레이어와 1:1로 소통하면서 폰을 조종하는 역할.

    플레이어가 입장할 때마다 배정. 배정된 플레이어 컨트롤러는 변경할 수 없음.

    

    플레이어 컨트롤러에게 조종당하는 액터.

    플레이어 컨트롤러는 뇌. 폰은 육체의 개념.

    레벨에 배치된 개체들과 충돌하면서 상호작용하는 역할.

    플레이어는 플레이어 컨트롤러를 통해 현재 조종하는 폰을 버리고

    다른 폰으로 옮겨가 조종할 수 있음.

 

  - 로그인(Login)

    플레이어의 게임 입장. 에디터 > Toolbar > 플레이 버튼을 통해 가능.

 

  - 빙의(Possess)

    언리얼에서 플레이어가 플레이어 컨트롤러를 통해 폰을 조종하는 행위.

 

  - 플레이어 컨트롤러 생성 및 설정 방법

    New C++ Class > PlayerController 부모 클래스 > "SPlayerController"

    Path > Controllers (폴더가 없다면 직접 생성하면 됨.)

<hide/>

// SPlayerController.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SPlayerController.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API ASPlayerController : public APlayerController
{
    GENERATED_BODY()
    
};
<hide/>

// SPlayerController.cpp


#include "Controllers/SPlayerController.h"
<hide/>

// SGameMode.h

...
class STUDYPROJECT_API ASGameMode : public AGameModeBase
{
    ...

public:
    ASGameMode();
    
};
<hide/>

// SGameMode.cpp


#include "Game/SGameMode.h"
#include "Controllers/SPlayerController.h"

ASGameMode::ASGameMode()
{
    PlayerControllerClass = ASPlayerController::StaticClass();
}

 

  - 플레이어 컨트롤러 액터 개체를 생성하고 해당 개체를 지정하는 것이 아님.

    플레이어 컨트롤러 클래스 정보를 지정함.

    이는 멀티플레이까지 고려된 구조. 플레이어가 입장할 때마다

    플레이어 컨트롤러 개체를 생성하는 방식.

 

  - 언리얼 오브젝트의 클래스 정보는 언리얼 헤더 툴에 의해 자동으로 생성됨.

    해당 정보는 StaticClass() 함수에 의해 가져올 수 있음. 다만, 컴파일 단계의 정보임에 주의.

    런타임 단계에서 클래스 정보는 GetClass() 함수로 가져올 수 있음.

 

  - 플레이어 컨트롤러 블루프린트 애셋 생성

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

    Controllers > 새 블루프린트 애셋 > SPlayerController 부모 클래스 > "BP_PlayerController"

    World Settings > Selected GameMode > Player Controller Class에 BP_PlayerController 지정.

 

4.1-4 Pawn

  - 폰의 생성

    새 C++ 클래스 > Pawn 부모 클래스 > "SPlayerPawn"

    Path > Characters

<hide/>

// SPlayerPawn.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "SPlayerPawn.generated.h"

UCLASS()
class STUDYPROJECT_API ASPlayerPawn : public APawn
{
    GENERATED_BODY()

public:
    ASPlayerPawn();

};
<hide/>

// SPlayerPawn.cpp


#include "Characters/SPlayerPawn.h"

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

}
<hide/>

// SGameMode.cpp


#include "Game/SGameMode.h"
#include "Controllers/SPlayerController.h"
#include "Characters/SPlayerPawn.h"

ASGameMode::ASGameMode()
{
    PlayerControllerClass = ASPlayerController::StaticClass();
    DefaultPawnClass = ASPlayerPawn::StaticClass();
}

 

  - 이벤트 함수 실습

    핵심적인 이벤트 함수를 모두 모아서 실행 흐름을 파악해보고자 함.

    이벤트 함수들을 override 할 때는 부모 클래스에서 접근 지정자를 확인 후

    그에 맞춘 접근 지정자를 설정하는 것이 올바른 방법.

<hide/>

// SGameMode.h

...
class STUDYPROJECT_API ASGameMode : public AGameModeBase
{
    ...

public:
    ASGameMode();

    virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
    virtual void InitGameState() override;

    virtual void PostInitializeComponents() override;

    virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMassage) override;
    virtual APlayerController* Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage);
    virtual void PostLogin(APlayerController* NewPlayer) override;
    
};
<hide/>

// SGameMode.cpp


...

void ASGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
    UE_LOG(LogTemp, Error, TEXT("         Start ASGameMode::        InitGame()"));
    Super::InitGame(MapName, Options, ErrorMessage);
    UE_LOG(LogTemp, Error, TEXT("         End   ASGameMode::        InitGame()"));
}

void ASGameMode::InitGameState()
{
    UE_LOG(LogTemp, Error, TEXT("         Start ASGameMode::        InitGameState()"));
    Super::InitGameState();
    UE_LOG(LogTemp, Error, TEXT("         End   ASGameMode::        InitGameState()"));
}

void ASGameMode::PostInitializeComponents()
{
    UE_LOG(LogTemp, Error, TEXT("         Start ASGameMode::        PostInitializeComponents()"));
    Super::PostInitializeComponents();
    UE_LOG(LogTemp, Error, TEXT("         End   ASGameMode::        PostInitializeComponents()"));
}

void ASGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMassage)
{
    UE_LOG(LogTemp, Error, TEXT("         Start ASGameMode::        PreLogin()"));
    Super::PreLogin(Options, Address, UniqueId, ErrorMassage);
    UE_LOG(LogTemp, Error, TEXT("         End   ASGameMode::        PreLogin()"));
}

APlayerController* ASGameMode::Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
    UE_LOG(LogTemp, Error, TEXT("         Start ASGameMode::        Login()"));
    APlayerController* PlayerController = Super::Login(NewPlayer, InRemoteRole, Portal, Options, UniqueId, ErrorMessage);
    UE_LOG(LogTemp, Error, TEXT("         End   ASGameMode::        Login()"));

    return PlayerController;
}

void ASGameMode::PostLogin(APlayerController* NewPlayer)
{
    UE_LOG(LogTemp, Error, TEXT("         Start ASGameMode::        PostLogin(ASPlayerController)"));
    Super::PostLogin(NewPlayer);
    UE_LOG(LogTemp, Error, TEXT("         End   ASGameMode::        PostLogin(ASPlayerController)"));
}
<hide/>

// SPlayerController.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SPlayerController.generated.h"

/**
 * 
 */
UCLASS()
class STUDYPROJECT_API ASPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    ASPlayerController();

    virtual void PostInitializeComponents() override;

    virtual void PlayerTick(float DeltaSeconds) override;

protected:
    virtual void SetupInputComponent() override;

    virtual void OnPossess(APawn* aPawn) override;
    virtual void OnUnPossess() override;

    virtual void BeginPlay() override;
    virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
    
};
<hide/>

// SPlayerController.cpp


#include "Controllers/SPlayerController.h"

ASPlayerController::ASPlayerController()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ASPlayerController::PostInitializeComponents()
{
    UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::PostInitializeComponents()"));
    Super::PostInitializeComponents();
    UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::PostInitializeComponents()"));
}

void ASPlayerController::PlayerTick(float DeltaSeconds)
{
    static bool bOnce = false;
    if (false == bOnce)
    {
        UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::PlayerTick()"));
    }
    Super::PlayerTick(DeltaSeconds);
    if (false == bOnce)
    {
        UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::PlayerTick()"));
        bOnce = true;
    }
}

void ASPlayerController::SetupInputComponent()
{
    UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::SetupInputComponent()"));
    Super::SetupInputComponent();
    UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::SetupInputComponent()"));
}

void ASPlayerController::OnPossess(APawn* aPawn)
{
    UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::OnPossess(ASPlayerPawn)"));
    Super::OnPossess(aPawn);
    UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::OnPossess(ASPlayerPawn)"));
}

void ASPlayerController::OnUnPossess()
{
    UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::OnUnPossess()"));
    Super::OnUnPossess();
    UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::OnUnPossess()"));
}

void ASPlayerController::BeginPlay()
{
    UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::BeginPlay()"));
    Super::BeginPlay();
    UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::BeginPlay()"));
}

void ASPlayerController::EndPlay(EEndPlayReason::Type EndPlayReason)
{
    UE_LOG(LogTemp, Warning, TEXT("       Start ASPlayerController::EndPlay()"));
    Super::EndPlay(EndPlayReason);
    UE_LOG(LogTemp, Warning, TEXT("       End   ASPlayerController::EndPlay()"));
}
<hide/>

// SPlayerPawn.h

...
class STUDYPROJECT_API ASPlayerPawn : public APawn
{
    ...

public:
    ASPlayerPawn();                                   // 언리얼의 Construction Script에 따라, CDO 생성 시점임. 우리가 생각하는 개체 생성 시점과 조금 다름. 

    virtual void PostInitializeComponents() override; // 우리가 생각하는 개체 생성 시점 함수.

    virtual void PossessedBy(AController* NewController) override;
    virtual void UnPossessed() override;

    virtual void Tick(float DeltaSeconds) override;
    virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;

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

    virtual void BeginPlay() override;

};
<hide/>

// SPlayerPawn.cpp


#include "Characters/SPlayerPawn.h"

ASPlayerPawn::ASPlayerPawn()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ASPlayerPawn::PostInitializeComponents()
{
    UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      PostInitializeComponents()"));
    Super::PostInitializeComponents();
    UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      PostInitializeComponents()"));
}

void ASPlayerPawn::PossessedBy(AController* NewController)
{
    UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      PossessedBy(ASPlayerController)"));
    Super::PossessedBy(NewController);
    UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      PossessedBy(ASPlayerController)"));
}

void ASPlayerPawn::UnPossessed()
{
    UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      UnPossessed()"));
    Super::UnPossessed();
    UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      UnPossessed()"));
}

void ASPlayerPawn::Tick(float DeltaSeconds)
{
    static bool bOnce = false;
    if (false == bOnce)
    {
        UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      Tick()"));
    }
    Super::Tick(DeltaSeconds);
    if (false == bOnce)
    {
        UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      Tick()"));
        bOnce = true;
    }
}

void ASPlayerPawn::EndPlay(EEndPlayReason::Type EndPlayReason)
{
    UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      EndPlay()"));
    Super::EndPlay(EndPlayReason);
    UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      EndPlay()"));
}

void ASPlayerPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      SetupPlayerInputComponent(PlayerInputComponent)"));
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      SetupPlayerInputComponent(PlayerInputComponent)"));
}

void ASPlayerPawn::BeginPlay()
{
    UE_LOG(LogTemp, Log, TEXT("                Start ASPlayerPawn::      BeginPlay()"));
    Super::BeginPlay();
    UE_LOG(LogTemp, Log, TEXT("                End   ASPlayerPawn::      BeginPlay()"));
}

 

  - 출력 결과

    Output Log > Filters > Categories > Show All 체크해제 후 LogTemp만 체크.

    CDO가 먼저 생성되기 위해서 생성자가 처음 호출됨. 중단점 걸어서 확인 가능.

    그 뒤로 이벤트 함수들이 순서대로 호출됨.

    출력 결과를 확인 했다면 이벤트 함수는 모두 지움. 

 

  - 기존에 레벨에 배치된 액터와 동적 생성한 액터

    기존에 레벨에 배치되었던 액터는 에디터 플레이시에 생성자가 호출되지 않음.

    최초로 배치 되어 질 때 호출됨.

    런타임 중에 생성되는 액터는 생성될 때 생성자가 호출됨.

 

  - Pawn 개체의 Auto Possess Player 속성

    싱글 플레이 게임에서는 정해진 폰이 있으므로, 미리 생성해두고 빙의 절차를 생략 가능.

    이때 Pawn 개체 클릭 > Details > Auto Possess Player 속성을 설정. Player 0는 로컬 플레이어를 의미함.

    다만 MMORPG의 경우에는 플레이어 컨트롤러가 어떤 폰에 빙의될지 로그인 전까지 모름.

 

4.2 인간형 폰 설계

4.2-1 인간형 폰 구현

  - 인간형 메시 준비

    이후 실습에서는 SkeletalMeshComponent를 활용할 예정.

    StaticMesh와는 다르게, 본에 대한 정보를 담고 있는 StaticMesh를 SkeletalMesh라고 함.

    무료 사이트인 믹사모를 통해서 얻어올 예정. 여기로 접속 후 회원가입/로그인 진행.

    Characters 탭에서 "Steve" 검색 후 클릭 > USE THIS CHARACTER 클릭.

    우측에 DOWNLOAD 클릭 > Format에 FBX Binary(.fbx) > Pose에는 T-pose 선택 후 DOWNLOAD 클릭.

    언리얼 에디터 > Content Browser > Content 우클릭 > 새 폴더 "Mixamo"

    Mixamo 우클릭 > 새 폴더 "Meshes" > Meshes 우클릭 > 새 폴더 "Steve"

    Steve 우클릭 > Import to /Game... 클릭 > 다운로드 받은 Steve.fbx 더블클릭. 

    다이얼로그에서 아무것도 선택 안함. 그대로 Import 클릭. 

 

  - 인간형 폰을 위한 컴포넌트

    Capsule Component

    폰이 움직임에 따라 레벨에 배치된 액터들과 충돌 반응할 컴포넌트. 루트컴포넌트로 지정.

    캐릭터 절반 높이와 몸둘레를 설정해주면 됨. 

    Content Browser > 측정 하고자 하는 매시 에셋을 Viewport에 드래그 드랍. 

    Viewport 좌상단 Perspective 버튼 클릭 > Front 선택

    원하는 시작 위치에 마우스 휠 버튼 누르고, 끝 위치까지 드래그 하면 cm 단위의 값이 나옴. 이게 길이.

    돌아가고자 하면 다시 Front 버튼 클릭 > Perspective로 변경.

    SkeletalMesh Component

    캐릭터 랜더링과 애니메이션 담당. 

    FloatingPawnMovement Component

    플레이어의 입력에 따라 캐릭터의 움직임 담당.

    SpringArm Component

    3인칭 시점으로 카메라 구도를 편리하게 설정할 수 있게끔 돕는 컴포넌트.

    카메라 지지대의 길이와 컴포넌트의 회전을 설정하면 됨.

    Camera Component

    이 컴포넌트가 바라보는 게임 세계의 모습이 플레이어 화면에 전송됨.

 

  - 블루프린트 실습

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

    Characters > 새 블루프린트 애셋 > Pawn 부모 클래스 > "BP_TestPlayerPawn"

 

  - 인간형 폰 구현 실습

<hide/>

// SPlayerPawn.h

...
class STUDYPROJECT_API ASPlayerPawn : public APawn
{
    ...

private:
    UPROPERTY(EditAnywhere, Category = ASPlayerPawn, meta = (AllowPrivateAccess))
    TObjectPtr<class UCapsuleComponent> CapsuleComponent;

    UPROPERTY(EditAnywhere, Category = ASPlayerPawn, meta = (AllowPrivateAccess))
    TObjectPtr<class USkeletalMeshComponent> SkeletalMeshComponent;

    UPROPERTY(EditAnywhere, Category = ASPlayerPawn, meta = (AllowPrivateAccess))
    TObjectPtr<class UFloatingPawnMovement> FloatingPawnMovementComponent;

    UPROPERTY(EditAnywhere, Category = ASPlayerPawn, meta = (AllowPrivateAccess))
    TObjectPtr<class USpringArmComponent> SpringArmComponent;

    UPROPERTY(EditAnywhere, Category = ASPlayerPawn, meta = (AllowPrivateAccess))
    TObjectPtr<class UCameraComponent> CameraComponent;

};
<hide/>

// SPlayerPawn.cpp


#include "Characters/SPlayerPawn.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/FloatingPawnMovement.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"

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

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

#pragma region InitializeCapsuleComponent
    CapsuleComponent = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComponent"));
    SetRootComponent(CapsuleComponent);
    CapsuleComponent->SetCapsuleHalfHeight(CharacterHalfHeight);
    CapsuleComponent->SetCapsuleRadius(CharacterRadius);
#pragma endregion

#pragma region InitializeSkeletalMesh
    SkeletalMeshComponent = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("SkeletalMeshComponent"));
    SkeletalMeshComponent->SetupAttachment(RootComponent);
    FVector PivotPosition(0.f, 0.f, -CharacterHalfHeight);
    FRotator PivotRotation(0.f, -90.f, 0.f);
    SkeletalMeshComponent->SetRelativeLocationAndRotation(PivotPosition, PivotRotation);
    //static ConstructorHelpers::FObjectFinder<USkeletalMesh> SkeletalMeshAsset(TEXT("오브젝트 패스"));
    //if (true == SkeletalMeshAsset.Succeeded())
    //{
    //    SkeletalMeshComponent->SetSkeletalMesh(SkeletalMeshAsset.Object);
    //}
#pragma endregion

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

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

    FloatingPawnMovementComponent = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("FloatingPawnMovementComponent"));

}

 

  - 폰 블루프린트 애셋 생성

    Content Browser > Characters > 새 블루프린트 애셋 > SPlayerPawn 부모 클래스 > "BP_PlayerPawn"

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

    Skeletal Mesh Component에 Skeletal Mesh로 Swat을 지정하고 플레이.

 

4.2-2 폰의 조작

  - 폰의 움직임 구현

    Toolbar > Settings > Project Settings > Input에서 입력 설정 가능.

    지금 설정하는 입력 방식은 삭제될(Deprecated) 방식이라서

    다음 단원에서는 다른 방식을 배울 예정. 다른 방식과 비슷하지만, 이 방식이 훨씬 쉬워서 이해하기 좋음.

    또 만약 UE4 프로젝트에 취업하게 된다면 이 방식으로 작업할 것이기에 이번 단원만 이렇게 진행.

 

  - Binding > Axis Mappings & Action Mappings

    Axis Mappings는 조이스틱의 레버. -1 ~ 1 사이의 값이 게임 로직에 전달됨.

    Action Mappings는 조이스틱의 버튼이라고 생각하면 됨. 0 혹은 1.

    휴지통 기호를 누르면 기존의 설정들이 초기화됨. 초기화 클릭해서 모두 삭제.

 

  - 입력 추가

    Axis Mappings 오른쪽 + 버튼 클릭.

    UpDown과 LeftRight 입력 생성.

    UpDown에는 W / S 키, LeftRight에는 A / D 키를 배정.

    이때 각각의 키가 서로 다른 방향으로 구분하기 위해 Scale 값을 다르게 설정.

 

  - 입력과 폰의 게임 로직

    언리얼 엔진은 입력 설정을 처리하기 위해 InputComponent라는 컴포넌트를 제공함.

    InputComponent에 입력 처리 함수를 연결(bind)시키면 입력 신호는 자동으로

    해당 입력 처리 함수의 인자값으로 전달됨.

    연결 함수를 호출하기에 적절한 함수가 SetupPlayerInputComponent() 함수.

 

  - BindAxis() 함수 실습

<hide/>

// SPlayerPawn.h

...
class STUDYPROJECT_API ASPlayerPawn : public APawn
{
    ...

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

private:
    void UpDown(float InAxisValue);
    
    void LeftRight(float InAxisValue);

};
<hide/>

// SPlayerPawn.cpp


...

void ASPlayerPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    PlayerInputComponent->BindAxis(TEXT("UpDown"), this, &ThisClass::UpDown);
    PlayerInputComponent->BindAxis(TEXT("LeftRight"), this, &ThisClass::LeftRight);
}

void ASPlayerPawn::UpDown(float InAxisValue)
{
    AddMovementInput(GetActorForwardVector(), InAxisValue);
}

void ASPlayerPawn::LeftRight(float InAxisValue)
{
    AddMovementInput(GetActorRightVector(), InAxisValue);
}

 

  - AddMovementInput(WorldDirection, Scale) 함수

    AddMovementInput(WorldDirection, Scale) 함수는 마치 자동차 운전석과 같음.

    WorldDirection은 핸들. 즉 이동할 방향을 결정할 수 있음.

    월드 좌표계 기준의 방향 벡터를 인자로 전달해야 함.

    Scale은 액셀. 1은 전진 -1은 후진.

 

  - 플레이어 컨트롤러 필터링 실습

    언리얼 엔진의 입력 시스템은 플레이어 컨트롤러를 거쳐서 폰에 전달됨.

    만약 플레이어 컨트롤러에서 특정 입력 처리 로직이 작성되면,

    해당 입력은 필터링 되어서 폰에 전달되지 않음. (아래 그림 참조)

    다만 폰을 조종하기 위한 입력 로직은 폰 클래스에 구현하는 것이 일반적임.

    GTA 같이 다양한 폰(ex. 차량, 비행기, ...)에 빙의하는 경우에는 폰에 구현.

    아래 실습 코드는 확인 후에 삭제.

<hide/>

// SPlayerController.h

...
class STUDYPROJECT_API ASPlayerController : public APlayerController
{
    ...

protected:
    virtual void SetupInputComponent() override;

private:
    void LeftRight(float InAxisValue);
    
};
<hide/>

// SPlayerController.cpp


#include "Controllers/SPlayerController.h"

ASPlayerController::ASPlayerController()
{
    PrimaryActorTick.bCanEverTick = true; // 액터의 Tick이 돌아야 입력도 처리할 수 있음.
}

void ASPlayerController::SetupInputComponent()
{
    Super::SetupInputComponent();

    InputComponent->BindAxis(TEXT("LeftRight"), this, &ThisClass::LeftRight);
}

void ASPlayerController::LeftRight(float InAxisValue)
{
    UE_LOG(LogTemp, Error, TEXT("ASPlayerController::LeftRight(%.3f)"), InAxisValue);
}

https://docs.unrealengine.com/5.0/ko/input-overview-in-unreal-engine/

 

  - SetInputMode() 함수

    플레이 버튼을 누르고 매번 뷰포트를 클릭해서

    포커스를 잡아야 입력 신호가 비로소 게임에 전달됨.

    플레이 버튼을 누르면 곧바로 포커싱 되게끔 하려면 SetInputMode() 함수를 사용함.

<hide/>

// SPlayerController.h

...
class STUDYPROJECT_API ASPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    ASPlayerController();

protected:
    virtual void BeginPlay() override;
    
};
<hide/>

// SPlayerController.cpp


#include "Controllers/SPlayerController.h"

ASPlayerController::ASPlayerController()
{
    PrimaryActorTick.bCanEverTick = true; 
}

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

    FInputModeGameOnly InputModeGameOnly;
    SetInputMode(InputModeGameOnly);
}

 

4.2-3 애니메이션 기초

  - 실습 준비

    마찬가지로 믹사모의 Animations 탭에서 애니메이션을 다운받고자 함.

    "swagger walk" 라고 검색 후 나오는 애니메이션 다운로드. InPlace 옵션을 체크하고 다운로드.

    언리얼 에디터 > Content Browser > Mixamo 우클릭 > 새 폴더 "Animtions"

    Animtions 우클릭 > 새 폴더 "AnimationSequences"

    AnimationSequences > Import to /Game... 클릭 > SwaggerWalk.fbx 선택

    FBX Import 다이얼로그에서 Import Mesh는 체크 해제(애니메이션을 임포트 하는것이므로.)

    Skeleton에 Swat_Skeleton 지정 후 Import 클릭. T Pose 애니메이션은 제거.

    애니메이션 이름을 "Walk_NoWeapon"로 변경.

 

  - 런타임 중에 애셋 로드 실습

    Content Browser > Walk_NoWeapon 애셋의 오브젝트 패스 복사.

<hide/>

// SPlayerPawn.h

...
class STUDYPROJECT_API ASPlayerPawn : public APawn
{
    ...

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

    virtual void BeginPlay() override;

private:
    ...

};
<hide/>

// SPlayerPawn.cpp


...

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

    SkeletalMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);

    UAnimationAsset* AnimationAsset = LoadObject<UAnimationAsset>(SkeletalMeshComponent, TEXT("/Script/Engine.AnimSequence'/Game/StudyProject/Animtions/AnimationAssets/Walk_NoWeapon.Walk_NoWeapon'"));
    if (nullptr != AnimationAsset)
    {
        SkeletalMeshComponent->PlayAnimation(AnimationAsset, true);
    }
}

...

 

  - 애니메이션 블루프린트

    위와 같이 코드로 애니메이션을 지정하는 방법도 있지만,

    게임의 규모가 커지면 애니메이션을 재생하는 코드를 관리 하는데 한계에 부딪힘.

    그래서 언리얼 엔진은 체계적으로 애니메이션 시스템을 설계하도록

    별도의 애니메이션 시스템을 제공함. 이것이 애니메이션 블루프린트.

 

  - 애니메이션 블루프린트 생성

    Content Browser > Animations 폴더 빈공간 우클릭 > 새 Animation 애셋

    Animation Blueprint > SK_Mannequin >  AnimInstance 부모 클래스 > "ABP_PlayerPawn"

    해당 애셋을 더블 클릭하면 애니메이션 블루프린트가 열림.

 

  - 애님 그래프(Anim Graph) 

    애니메이션 블루프린트에는 여러 기능이 있는데,

    애니메이션을 설계하는 애님 그래프라는 작업 환경을 제공함.

    그래프 창 > AnimGraph 탭 클릭하면 열림.

 

  - 애님 그래프 실습

    애니메이션 블루프린트 우측 하단 > 애셋 브라우저 클릭

    목록에 위치한 Walk_NoWeapon을 애님 그래프에 끌어다 놓음.

    Walk_NoWeapon 애니메이션 재생 노드에서 사람 모양의 핀을 

    최종 애니메이션 노드에 드래그해서 연결.

    Walk_NoWeapon 클릭 > Details > Loop Animation 체크해서 반복되게끔 지정.

 

  - 그래프 컴파일 방법

    Toolbar > Compile을 클릭.

    다만, 컴파일 시에 저장을 꼭 해줘야 날아가지 않음.

    Compile 버튼 옆 점 세 개 클릭 > Save On Compile > On Success Only 클릭

    컴파일이 성공하면 자동으로 저장해줌.

 

  - 컴파일이 성공하면 프리뷰 인스턴스 디버깅에 의해 

    왼쪽의 프리뷰에서 해당 애니메이션이 재생되는 것을 볼 수 있음.

 

  - 애님 인스턴스(Anim Instance)

    애님 인스턴스는 C++ 클래스임. 애니메이션 블루프린트를 관리함.

    애니메이션 블루프린트는 애님 그래프 로직에 따라 동작하는

    캐릭터 애니메이션 시스템을 구동시킴.

    즉, 애님 인스턴스 C++ 클래스에서 애니메이션에 필요한 기준값(속도, 상태, ...)을 업데이트함.

    애니메이션 블루프린트는 업데이트 된 기준값들을 가지고 애니메이션 그래프를 동작시킴.

 

  - 스켈레탈 메시 컴포넌트와 애님 인스턴스 실습

    스켈레탈 메시 컴포넌트는 자신이 관리하는 캐릭터의 애니메이션을 

    애님 인스턴스에게 위임하는 구조로 설계되어 있음.

    스켈레탈 메시가 이 애니메이션 블루프린트를 실행 시키려면

    1. 애니메이션 블루프린트 애셋의 경로 정보 복사

    2. 경로 정보를 통해 애님인스턴스 변수 생성

      단, 블루프린트 애셋은 경로 정보 마지막에 _C를 꼭 붙혀야함.

    3. 스켈레탈 메시 컴포넌트의 애님 인스턴스 클래스 속성에

      해당 애님 인스턴스 클래스 정보 지정.

    4. 이전에 생성한 BeginPlay() 함수의 코드는 제거

<hide/>

// SPlayerPawn.cpp


...

ASPlayerPawn::ASPlayerPawn()
{
    ...

#pragma region InitializeSkeletalMesh
    ...
    
    SkeletalMeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
    //static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassInfo(TEXT("/Script/Engine.AnimBlueprint'/Game/StudyProject/Animtions/ABP_PlayerPawn.ABP_PlayerPawn_C'"));
    static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassInfo(TEXT("'/Game/StudyProject/Animtions/ABP_PlayerPawn.ABP_PlayerPawn_C'")); // 위 오브젝트 패스 대신, 작은 따옴표 안의 내용만으로도 가능함.
    if (true == AnimInstanceClassInfo.Succeeded())
    {
        SkeletalMeshComponent->SetAnimClass(AnimInstanceClassInfo.Class);
    }
#pragma endregion

    ...

}

...

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

    //SkeletalMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
    //
    //UAnimationAsset* AnimationAsset = LoadObject<UAnimationAsset>(SkeletalMeshComponent, TEXT("/Script/Engine.AnimSequence'/Game/StudyProject/Animtions/AnimationAssets/Walk_NoWeapon.Walk_NoWeapon'"));
    //if (nullptr != AnimationAsset)
    //{
    //    SkeletalMeshComponent->PlayAnimation(AnimationAsset, true);
    //}
}

....

 

  - 블루프린트 클래스로 설정하기

    Content Browser > BP_PlayerPawn > SkeletalMeshComponent 클릭 > Details

    Animation Mode에 Use Animation Blueprint 지정.

    Anim Class에는 ABP_PlayerPawn 지정.

    기존에 생성자에 작성한 코드는 삭제.