[DAY36] Unreal Engine C++ 3D Game Dev(2)

베리투스·2025년 9월 23일

TIL: Today I Learned

목록 보기
37/93

오늘은 3인칭 캐릭터를 완성했다. 먼저 게임모드(GameMode)플레이어 컨트롤러(PlayerController), 캐릭터(Character) 클래스의 관계를 이해하고 C++로 기본 뼈대를 만들었다. 그 후 Enhanced Input System을 사용해 입력 체계를 구축하고, 이를 C++ 함수에 바인딩(Binding)하여 실제로 캐릭터를 움직이고, 점프하고, 달리게 구현했다. 마지막으로 애니메이션 블루프린트(Anim Blueprint)스테이트 머신(State Machine)을 설계하여 캐릭터의 움직임에 생동감을 불어넣었다. 🏃‍♂️


📌 목표

  • GameMode, PlayerController, Character 클래스의 역할을 이해하고 C++로 생성하기
  • 3인칭 시점을 위한 스프링 암카메라 컴포넌트 추가하기
  • Enhanced Input System을 사용하여 Input Action(IA)Input Mapping Context(IMC) 설정하기
  • 사용자 입력을 C++ 함수에 바인딩하여 이동, 점프, 시점 회전, 스프린트 기능 구현하기
  • 애니메이션 블루프린트State Machine을 통해 캐릭터 상태에 맞는 애니메이션 적용하기

📖 이론

1. 게임의 3요소: GameMode, PlayerController, Character

게임의 핵심 구조를 담당하는 세 가지 클래스의 역할을 명확히 이해하는 것이 중요했다.

  • GameMode: 게임의 규칙과 흐름을 총괄하는 "감독"이다. "어떤 캐릭터(Pawn)를 스폰할 것인가?", "어떤 컨트롤러를 사용할 것인가?" 등을 결정한다.
  • PlayerController: 플레이어의 입력을 받아 해석하는 "뇌"와 같다. 키보드, 마우스 입력을 감지하고, 이를 캐릭터가 알아들을 수 있는 명령으로 변환하여 전달한다. 캐릭터에 "빙의(Possess)"하여 조종하는 주체다.
  • Character: 월드에 실제로 존재하며 움직이는 "몸"이다. PlayerController로부터 명령을 받아 실제로 이동하고 점프하는 등 행동을 수행한다. Character 클래스는 사람 형태의 움직임(걷기, 점프, 중력 등)을 위한 CharacterMovementComponent를 기본으로 내장하고 있어 매우 편리하다.

2. Enhanced Input System (IA & IMC)

언리얼 5의 새로운 입력 시스템으로, 입력을 매우 체계적으로 관리할 수 있게 해준다.

  • Input Action (IA): "점프", "이동", "공격" 등 추상적인 동작 그 자체를 의미하는 에셋이다. 어떤 키로 발동하는지는 신경 쓰지 않고, '점프'라는 행동의 속성(ex. 한 번 누르는 것)만 정의한다.
  • Input Mapping Context (IMC): IA와 실제 키(물리적인 입력)를 연결해주는 "매핑 테이블"이다. 예를 들어 'IA_Jump는 Spacebar로 발동한다' 와 같이 설정한다. 이 덕분에 키 변경이 필요할 때 코드 수정 없이 IMC 에셋만 수정하면 된다.

3. 입력 바인딩 (Action Binding)

PlayerController가 IMC를 통해 입력을 감지하면, 이 입력을 Character의 특정 함수와 연결해줘야 실제 동작이 일어난다. 이 연결 과정을 "바인딩"이라고 한다. 이 작업은 Character 클래스의 SetupPlayerInputComponent 함수 안에서 BindAction 함수를 호출하여 이루어진다.

4. 애니메이션 블루프린트 & State Machine

캐릭터의 움직임을 자연스럽게 만드는 핵심 요소다.

  • 애니메이션 블루프린트 (Anim BP): 캐릭터의 뼈대(스켈레톤)를 기반으로 애니메이션을 제어하는 특수한 블루프린트다.
  • 이벤트 그래프 (Event Graph): Anim BP 내부의 로직 담당. 매 프레임 캐릭터의 속도, 점프 여부 같은 데이터를 C++ 클래스로부터 받아와 변수에 저장한다.
  • 애님 그래프 (Anim Graph): Anim BP 내부의 시각적인 부분 담당. 이벤트 그래프에서 계산된 변수 값을 바탕으로 어떤 애니메이션을 재생할지 결정한다.
  • 스테이트 머신 (State Machine): 애님 그래프의 핵심 로직. "대기(Idle)", "걷기/달리기(Walk/Run)", "점프(Jump)" 같은 '상태'를 만들고, '전환 조건'(예: 속도가 0보다 크면 Idle -> Walk/Run)에 따라 상태를 오가며 애니메이션을 자연스럽게 바꿔준다.

💻 코드

SpartaGameMode.cpp

#include "SpartaGameMode.h"
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"

using namespace std;

ASpartaGameMode::ASpartaGameMode()
{
	// 1. 게임이 시작될 때 기본으로 스폰될 캐릭터 클래스를 지정합니다.
	DefaultPawnClass = ASpartaCharacter::StaticClass();

	// 2. 이 게임에서 사용할 플레이어 컨트롤러 클래스를 지정합니다.
	PlayerControllerClass = ASpartaPlayerController::StaticClass();
}

SpartaPlayerController.h

#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;
};

SpartaPlayerController.cpp

#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);
	}
}

SpartaCharacter.h

#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;
};

SpartaCharacter.cpp

#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 함수 안에서 PlayerInputComponentUEnhancedInputComponent로 캐스팅하는 것을 잊어서 BindAction 함수를 호출할 수 없었다. Enhanced Input System을 사용하려면 이 캐스팅 과정이 필수적이다.
  • 블루프린트에서 에셋 할당 누락: C++ 코드에서 UPROPERTYInputMappingContextInputAction 포인터 변수를 선언만 하고, 정작 PlayerController의 블루프린트 에디터에서 직접 만든 IMC, IA 에셋들을 할당해주지 않았다. 당연히 포인터가 nullptr이라 게임 시작 시 크래시가 발생했다. C++로 뼈대를 만들고, 블루프린트에서 내용을 채우는 언리얼의 작업 방식을 제대로 이해해야 하는 부분이었다.

✅ 핵심 요약

개념설명비고
GameMode게임의 규칙(기본 캐릭터, 컨트롤러 등)을 총괄하는 감독.DefaultPawnClass를 여기서 지정한다.
PlayerController플레이어의 입력을 받아 해석하고 캐릭터에 명령을 내리는 뇌.Enhanced Input의 IMC를 여기서 활성화한다.
Character실제로 월드에서 움직이는 몸. CharacterMovementComponent가 핵심.이동, 점프 등의 실제 로직이 구현된다.
Enhanced InputIA(동작)와 IMC(키 매핑)로 입력을 분리하여 관리하는 시스템.모듈화되어 있어 관리가 매우 용이하다.
Input Action (IA)'점프', '이동' 같은 추상적인 동작 자체를 정의하는 에셋.Value Type(Bool, Axis2D 등) 설정이 중요하다.
Input Mapping ContextIA와 실제 키보드/마우스 입력을 연결(매핑)하는 테이블.키 설정을 바꾸고 싶을 때 여기만 수정하면 된다.
BindActionSetupPlayerInputComponent 함수 내에서 IA와 C++ 함수를 연결하는 과정.ETriggerEvent로 누를 때, 뗄 때 등을 구분.
Anim Blueprint캐릭터의 스켈레톤을 기반으로 애니메이션을 제어하는 블루프린트.이벤트 그래프(로직)와 애님 그래프(시각)로 나뉜다.
State Machine캐릭터의 상태(대기, 달리기, 점프 등)에 따라 애니메이션을 전환하는 로직 구조.게임 캐릭터를 살아 움직이게 만드는 핵심.
profile
Shin Ji Yong // Unreal Engine 5 공부중입니다~

0개의 댓글