오늘은 3인칭 캐릭터를 완성했다. 먼저 게임모드(GameMode)와 플레이어 컨트롤러(PlayerController), 캐릭터(Character) 클래스의 관계를 이해하고 C++로 기본 뼈대를 만들었다. 그 후 Enhanced Input System을 사용해 입력 체계를 구축하고, 이를 C++ 함수에 바인딩(Binding)하여 실제로 캐릭터를 움직이고, 점프하고, 달리게 구현했다. 마지막으로 애니메이션 블루프린트(Anim Blueprint)와 스테이트 머신(State Machine)을 설계하여 캐릭터의 움직임에 생동감을 불어넣었다. 🏃♂️
GameMode, PlayerController, Character 클래스의 역할을 이해하고 C++로 생성하기Input Action(IA)과 Input Mapping Context(IMC) 설정하기게임의 핵심 구조를 담당하는 세 가지 클래스의 역할을 명확히 이해하는 것이 중요했다.
GameMode: 게임의 규칙과 흐름을 총괄하는 "감독"이다. "어떤 캐릭터(Pawn)를 스폰할 것인가?", "어떤 컨트롤러를 사용할 것인가?" 등을 결정한다.PlayerController: 플레이어의 입력을 받아 해석하는 "뇌"와 같다. 키보드, 마우스 입력을 감지하고, 이를 캐릭터가 알아들을 수 있는 명령으로 변환하여 전달한다. 캐릭터에 "빙의(Possess)"하여 조종하는 주체다.Character: 월드에 실제로 존재하며 움직이는 "몸"이다. PlayerController로부터 명령을 받아 실제로 이동하고 점프하는 등 행동을 수행한다. Character 클래스는 사람 형태의 움직임(걷기, 점프, 중력 등)을 위한 CharacterMovementComponent를 기본으로 내장하고 있어 매우 편리하다.언리얼 5의 새로운 입력 시스템으로, 입력을 매우 체계적으로 관리할 수 있게 해준다.
Input Action (IA): "점프", "이동", "공격" 등 추상적인 동작 그 자체를 의미하는 에셋이다. 어떤 키로 발동하는지는 신경 쓰지 않고, '점프'라는 행동의 속성(ex. 한 번 누르는 것)만 정의한다.Input Mapping Context (IMC): IA와 실제 키(물리적인 입력)를 연결해주는 "매핑 테이블"이다. 예를 들어 'IA_Jump는 Spacebar로 발동한다' 와 같이 설정한다. 이 덕분에 키 변경이 필요할 때 코드 수정 없이 IMC 에셋만 수정하면 된다.PlayerController가 IMC를 통해 입력을 감지하면, 이 입력을 Character의 특정 함수와 연결해줘야 실제 동작이 일어난다. 이 연결 과정을 "바인딩"이라고 한다. 이 작업은 Character 클래스의 SetupPlayerInputComponent 함수 안에서 BindAction 함수를 호출하여 이루어진다.
캐릭터의 움직임을 자연스럽게 만드는 핵심 요소다.
애니메이션 블루프린트 (Anim BP): 캐릭터의 뼈대(스켈레톤)를 기반으로 애니메이션을 제어하는 특수한 블루프린트다.이벤트 그래프 (Event Graph): Anim BP 내부의 로직 담당. 매 프레임 캐릭터의 속도, 점프 여부 같은 데이터를 C++ 클래스로부터 받아와 변수에 저장한다.애님 그래프 (Anim Graph): Anim BP 내부의 시각적인 부분 담당. 이벤트 그래프에서 계산된 변수 값을 바탕으로 어떤 애니메이션을 재생할지 결정한다.스테이트 머신 (State Machine): 애님 그래프의 핵심 로직. "대기(Idle)", "걷기/달리기(Walk/Run)", "점프(Jump)" 같은 '상태'를 만들고, '전환 조건'(예: 속도가 0보다 크면 Idle -> Walk/Run)에 따라 상태를 오가며 애니메이션을 자연스럽게 바꿔준다.#include "SpartaGameMode.h"
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
using namespace std;
ASpartaGameMode::ASpartaGameMode()
{
// 1. 게임이 시작될 때 기본으로 스폰될 캐릭터 클래스를 지정합니다.
DefaultPawnClass = ASpartaCharacter::StaticClass();
// 2. 이 게임에서 사용할 플레이어 컨트롤러 클래스를 지정합니다.
PlayerControllerClass = ASpartaPlayerController::StaticClass();
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SpartaPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
UCLASS()
class SPARTAPROJECT_API ASpartaPlayerController : public APlayerController
{
GENERATED_BODY()
public:
ASpartaPlayerController();
protected:
// 1. 게임이 시작되면 호출되는 함수. 여기서 IMC를 활성화합니다.
virtual void BeginPlay() override;
// 2. 블루프린트에서 설정할 수 있도록 UPROPERTY로 IMC와 IA 변수들을 노출합니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Input")
UInputMappingContext* InputMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* SprintAction;
};
#include "SpartaPlayerController.h"
#include "EnhancedInputSubsystems.h"
using namespace std;
ASpartaPlayerController::ASpartaPlayerController()
{
}
void ASpartaPlayerController::BeginPlay()
{
Super::BeginPlay();
// 3. LocalPlayer Subsystem을 가져와서 우리가 만든 IMC를 추가(활성화)합니다.
// 이 과정이 있어야 IMC에 설정된 키 매핑이 동작합니다.
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;
UCLASS()
class SPARTAPROJECT_API ASpartaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASpartaCharacter();
protected:
// 1. 입력을 처리할 함수들을 선언합니다.
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void Move(const FInputActionValue& value);
void Look(const FInputActionValue& value);
void StartJump(const FInputActionValue& value);
void StopJump(const FInputActionValue& value);
void StartSprint(const FInputActionValue& value);
void StopSprint(const FInputActionValue& value);
// 2. 3인칭 카메라를 위한 컴포넌트들
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
USpringArmComponent* SpringArmComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
UCameraComponent* CameraComp;
// 3. 스프린트를 위한 속도 변수들
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement")
float NormalSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Movement")
float SprintSpeedMultiplier;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement")
float SprintSpeed;
};
#include "SpartaCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputComponent.h"
#include "SpartaPlayerController.h"
#include "GameFramework/CharacterMovementComponent.h"
using namespace std;
ASpartaCharacter::ASpartaCharacter()
{
PrimaryActorTick.bCanEverTick = false;
// 4. 카메라와 스프링 암 컴포넌트를 생성하고 설정합니다.
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
SpringArmComp->TargetArmLength = 300.0f;
SpringArmComp->bUsePawnControlRotation = true; // 컨트롤러의 회전을 따라가도록 설정
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp);
CameraComp->bUsePawnControlRotation = false; // 스프링암을 따라가므로 자체 회전은 끔
// 5. 속도 변수 초기화
NormalSpeed = 600.f;
SprintSpeedMultiplier = 1.5f;
SprintSpeed = NormalSpeed * SprintSpeedMultiplier;
}
void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 6. Enhanced Input Component로 캐스팅하고 IA와 함수를 바인딩합니다.
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (ASpartaPlayerController* PlayerController = Cast<ASpartaPlayerController>(GetController()))
{
// 이동, 점프, 시점, 스프린트 액션을 각 함수에 연결
EnhancedInput->BindAction(PlayerController->MoveAction, ETriggerEvent::Triggered, this, &ASpartaCharacter::Move);
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Triggered, this, &ASpartaCharacter::StartJump);
EnhancedInput->BindAction(PlayerController->JumpAction, ETriggerEvent::Completed, this, &ASpartaCharacter::StopJump);
EnhancedInput->BindAction(PlayerController->LookAction, ETriggerEvent::Triggered, this, &ASpartaCharacter::Look);
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Triggered, this, &ASpartaCharacter::StartSprint);
EnhancedInput->BindAction(PlayerController->SprintAction, ETriggerEvent::Completed, this, &ASpartaCharacter::StopSprint);
}
}
}
// 7. 각 입력에 대한 실제 동작을 구현합니다.
void ASpartaCharacter::Move(const FInputActionValue& value)
{
const FVector2D MoveInput = value.Get<FVector2D>();
if (!Controller || MoveInput.IsNearlyZero()) return;
// 캐릭터의 앞/뒤, 좌/우 방향으로 이동 입력 추가
AddMovementInput(GetActorForwardVector(), MoveInput.X);
AddMovementInput(GetActorRightVector(), MoveInput.Y);
}
void ASpartaCharacter::Look(const FInputActionValue& value)
{
const FVector2D LookInput = value.Get<FVector2D>();
if (!Controller) return;
// 마우스 입력에 따라 컨트롤러의 Yaw, Pitch 회전
AddControllerYawInput(LookInput.X);
AddControllerPitchInput(LookInput.Y);
}
void ASpartaCharacter::StartJump(const FInputActionValue& value)
{
// Character 클래스에 내장된 점프 함수 호출
Jump();
}
void ASpartaCharacter::StopJump(const FInputActionValue& value)
{
StopJumping();
}
void ASpartaCharacter::StartSprint(const FInputActionValue& value)
{
// 캐릭터 이동 컴포넌트의 최대 걷기 속도를 스프린트 속도로 변경
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
}
void ASpartaCharacter::StopSprint(const FInputActionValue& value)
{
GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
}
UFUNCTION() 매크로 누락: 입력 바인딩에 사용할 함수(Move, Look 등)에 UFUNCTION() 매크로를 붙이지 않았더니 엔진이 함수를 찾지 못해 바인딩이 실패했다. 블루프린트에 노출하지 않더라도, 엔진 리플렉션 시스템이 인식하게 하려면 꼭 필요하다는 것을 배웠다.SetupPlayerInputComponent 함수 안에서 PlayerInputComponent를 UEnhancedInputComponent로 캐스팅하는 것을 잊어서 BindAction 함수를 호출할 수 없었다. Enhanced Input System을 사용하려면 이 캐스팅 과정이 필수적이다.UPROPERTY로 InputMappingContext나 InputAction 포인터 변수를 선언만 하고, 정작 PlayerController의 블루프린트 에디터에서 직접 만든 IMC, IA 에셋들을 할당해주지 않았다. 당연히 포인터가 nullptr이라 게임 시작 시 크래시가 발생했다. C++로 뼈대를 만들고, 블루프린트에서 내용을 채우는 언리얼의 작업 방식을 제대로 이해해야 하는 부분이었다.| 개념 | 설명 | 비고 |
|---|---|---|
| GameMode | 게임의 규칙(기본 캐릭터, 컨트롤러 등)을 총괄하는 감독. | DefaultPawnClass를 여기서 지정한다. |
| PlayerController | 플레이어의 입력을 받아 해석하고 캐릭터에 명령을 내리는 뇌. | Enhanced Input의 IMC를 여기서 활성화한다. |
| Character | 실제로 월드에서 움직이는 몸. CharacterMovementComponent가 핵심. | 이동, 점프 등의 실제 로직이 구현된다. |
| Enhanced Input | IA(동작)와 IMC(키 매핑)로 입력을 분리하여 관리하는 시스템. | 모듈화되어 있어 관리가 매우 용이하다. |
| Input Action (IA) | '점프', '이동' 같은 추상적인 동작 자체를 정의하는 에셋. | Value Type(Bool, Axis2D 등) 설정이 중요하다. |
| Input Mapping Context | IA와 실제 키보드/마우스 입력을 연결(매핑)하는 테이블. | 키 설정을 바꾸고 싶을 때 여기만 수정하면 된다. |
| BindAction | SetupPlayerInputComponent 함수 내에서 IA와 C++ 함수를 연결하는 과정. | ETriggerEvent로 누를 때, 뗄 때 등을 구분. |
| Anim Blueprint | 캐릭터의 스켈레톤을 기반으로 애니메이션을 제어하는 블루프린트. | 이벤트 그래프(로직)와 애님 그래프(시각)로 나뉜다. |
| State Machine | 캐릭터의 상태(대기, 달리기, 점프 등)에 따라 애니메이션을 전환하는 로직 구조. | 게임 캐릭터를 살아 움직이게 만드는 핵심. |