TIL: Unreal C++ 25일차

박춘팔·5일 전

언리얼 TIL

목록 보기
24/26

누적 학습 시간 : 236시간 34분

📅 2026-05-06

UE-Handbook

기존 에픽게임즈 언리얼 공식문서가 사실 초보가 보기에는 너무 어렵게 되어있다.
그래서 Docusaurus이용해서 handbook을 만들어봤다.
아이디어는 예전에 TS handbook이 도움이 많이됐던게 기억나서 만들어봤다.
유지보수를 계속 할지는 모르겠지만 혹시 필요한사람들이 많이들 봤으면 좋겠다.

C++ 사용 IMC, IA 등록

언리얼에서 pawn, character가 사용자의 입력대로 이동하려면 Input Mapping ContextInput Action을 등록해주어야한다.

이전에는 블루프린트로 가볍게 했지만 C++에서는 어떻게 하는지 알아보자.

IMC, IA 생성

일단 IMC, IA생성은 블루프린트와 동일하다.

IMC

IA

C++ PlayerCharacter 클래스 생성

Character 클래스를 부모로 만든 PlayerCharacter 클래스를 생성해준다.

PlayerCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "APlayerCharacter.generated.h"

UCLASS()
class METAL3D_API APlayerCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	APlayerCharacter();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

PlayerCharacter.cpp

#include "Player/PlayerCharacter.h"

// Sets default values
APlayerCharacter::APlayerCharacter()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

// Called when the game starts or when spawned
void APlayerCharacter::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void APlayerCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

생성 해준 뒤에는 미리 만들어놓은 IMC와 IA를 연결하는 작업을 진행해준다.

PlayerCharacter.h 수정

#pragma once

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

#include "PlayerCharacter.generated.h"

class UInputMappingContext;
class UInputAction;

UCLASS()
class METAL3D_API APlayerCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	APlayerCharacter();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	// 인풋 컴포넌트 오버라이드
	virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")
	TObjectPtr<UInputMappingContext> DefaultMappingContext;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")
	TObjectPtr<UInputAction> MoveAction;
	
	virtual void NotifyControllerChanged() override;

public:
	// Called every frame
	virtual void Tick(float DeltaTime) override;

private:
	void Move(const FInputActionValue& Value);
};

해설 및 수정사항

뭐가 많이 추가됐는데 한줄씩 보겠다.

헤더 include

include "InputActionValue.h"

이 헤더는 아래서 정의한 void Move(const FInputActionValue& Value)에서
FInputActionValue타입을 헤더의 상수 시그니처에서 사용 중이기 때문에 필요하다.

include "PlayerCharacter.generated.h"

해당 헤더는는 언리얼 리플렉션 코드 생성용 헤더이다.
*.generated.h는 반드시 해당 헤더의 include중 마지막에 와야한다.

전방선언

class UInputMappingContext;
class UInputAction;

protected:에서 선언된

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")
TObjectPtr<UInputMappingContext> DefaultMappingContext;

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")
TObjectPtr<UInputAction> MoveAction;

에서 포인터로써만 들고있기 때문에 헤더에서는 전체 include가 아닌 전방선언만 해도 오케이다.

UCLASS()

언리얼 리플렉션 시스템에 해당 클래스를 등록한다.

이게 있어야 BP생성시 해당 클래스를 부모로 선택 가능하고 UPROPERTY,UFUNCTION 같은 시스템도 작동한다.

METAL3D_API

프로젝트 모듈 export/import 매크로
프로젝트명이 Metal3D라서 자동으로 붙은 것이다.

APlayerCharacter : public ACharacter

ACharacter를 상속받은 PlayerCharacter 클래스

AActor
자식 > APawn
의 자식 > ACharacter
의 자식 > APlayerCharacter

class 이름이 PlayerCharacter인데 왜 APlayerCharacter일까?

GENERATED_BODY()

언리얼이 생성한 리플렉션 코드를 클래스 안에 삽입하는 매크로
UCLASS() 에는 필수로 들어간다.

APlayerCharacter()

APlayerCharacter();

생성자

Beginplay

virtual void BeginPlay() override;

블프에서 많이 보던 BeginPlay 노드가 이 부분이다.

SetupPlayerInputComponent

virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;

Pawn/Character가 Possess되어 입력 컴포넌트가 준비될 때 호출

블프와는 다르게 여기서 IMC, IA를 연결한다.

UPROPERTY(...)

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Input")

UPROPERTY
일반 C++변수를 리플렉션 시스템에 등록하는 매크로
등록된 변수는 언리얼 에디터에서 노출되며 블루프린트 접근, 직렬화, GC 추적같은 언언리얼 기능의 대상이 된다.

EditDefaultsOnly
인스턴스마다 수정하는 값이 아닌
클래스 기본값, BP defaults에서만 수정 가능하게 한다.

BlueprintReadOnly
블루프린트에서 해당 값을 읽을 수는 있지만 수정할 수는 없다.

Category="Input"
언리얼 에디터 > Detail 패널 내에서 Input 카테고리 아래에서 보여준다.

TObjectPtr<UInputMappingContext>

TObjectPtr<UInputMappingContext> DefaultMappingContext;

UinputMappingContext 타입의 UObject를 가리키는 포인터.
IMC_PlayerCharacter같은 IMC 에셋을 참조한다.

NotifyControllerChanged()

virtual void NotifyControllerChanged() override;

pawn, character를 소유하거나 조종하는 Controller가 변경되었음을 알리는 함수

플레이어가 조종하면 APlayerController AI가 조종하면 AAIController가 붙는다.

해당 함수는 Controller가 변경되는 시점에 호출되는데 대표적으로

  • PlayerController가 Character에 Possess됐을 때
  • Controller가 바뀌었을 때
  • Possess, UnPossess 흐름에서 Controller 상태가 달라졌을 때

블프와 달리 IMC를 해당 함수에서 붙이게 되는데 이유가
BeginPlay()시점에 아직 Controller나 LocalPlayer가 준비되지 않았을 수 있기 때문이다.

Move()

이동 입력을 처리하는 사용자 정의 함수

PlayerCharacter.cpp 수정

#include "Player/PlayerCharacter.h"

#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

// Sets default values
APlayerCharacter::APlayerCharacter()
{
	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void APlayerCharacter::BeginPlay()
{
	Super::BeginPlay();


}

// Called every frame
void APlayerCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);

	if (!EnhancedInputComponent)
	{
		return;
	}

	if (MoveAction)
	{
		EnhancedInputComponent->BindAction(
			MoveAction,
			ETriggerEvent::Triggered,
			this,
			&APlayerCharacter::Move
		);
	}
}

void APlayerCharacter::NotifyControllerChanged()
{
	Super::NotifyControllerChanged();
	
	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (!PlayerController)
	{
		return;
	}

	ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
	if (!LocalPlayer)
	{
		return;
	}

	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LocalPlayer->GetSubsystem<
		UEnhancedInputLocalPlayerSubsystem>();
	if (InputSubsystem && DefaultMappingContext)
	{
		InputSubsystem->AddMappingContext(DefaultMappingContext, 0);
	}
}


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

	if (Controller == nullptr)
	{
		return;
	}

	const FRotator ControlRotation = Controller->GetControlRotation();
	const FRotator YawRotation(0.0f, ControlRotation.Yaw, 0.0f);

	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

	AddMovementInput(ForwardDirection, MovementVector.Y);
	AddMovementInput(RightDirection, MovementVector.X);
}

해설 및 수정사항

헤더

#include "EnhancedInputComponent.h"

헤더와는 다르게 EnhancedInputComponent를 포인터로 들고만 있는게 아니라
실제로 멤버 함수를 호출하거나 사용하기 때문에 필요하다.

SetupPlayerInputComponent

void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);

	if (!EnhancedInputComponent)
	{
		return;
	}

	if (MoveAction)
	{
		EnhancedInputComponent->BindAction(
			MoveAction,
			ETriggerEvent::Triggered,
			this,
			&APlayerCharacter::Move
		);
	}
}

pawn, character의 입력 바인딩 구성 함수

Super::SetupPlayerInputComponent(PlayerInputComponent)

Super::는 언리얼 C++에서 부모클래스의 별칭이다.
해당 클래스의 부모클래스에서 해야할 처리를 먼저 수행하겠다는 의미다.

언리얼 오버라이드 함수에서 특별한 이유가 없으면 Super::...()를 호출하는게 기본

Cast<UEnhancedInputComponent>

언리얼의 Cast<>는 c++의 dynamic_cast와 비슷한 목적이다.
다른 점은 Cast<>는 리플렉션 시스템을 기반으로 UObject 타입 변환을 수행한다.
블프에서 많이 보던 Cast to 다

캐스팅을 하는 이유는 함수 인자가 기본 타입이기 때문이다.

// 함수인자 UInputComponent > UEnhancedInputComponent로 캐스팅
UEnhancedInputComponent* EnhancedInputComponent = 
			Cast<UEnhancedInputComponent>(PlayerInputComponent);

실패할 수도 있기 때문에 바로 체크도 해준다.

	if (!EnhancedInputComponent)
	{
		return;
	}

MoveAction 바인딩

if (MoveAction)
	{
		EnhancedInputComponent->BindAction(
			MoveAction,
			ETriggerEvent::Triggered,
			this,
			&APlayerCharacter::Move
		);
	}

헤더에서 UPROPERTY로 등록한 MoveAction이 있다.
언리얼 에디터에서 실제로 IA_Move를 등록해준 IA를 MoveAction에 바인딩해준다.

MoveAction
어떤 Input Action을 감시할지 지정

ETriggerEvent::Triggered
Input Action이 어떤 상태일 때 호출할지 정함

this
호출 대상 객체, 현재 PlayerCharacter를 말함

&APlayerCharacter::Move
호출할 멤버함수 포인터

NotifyControllerChanged

void APlayerCharacter::NotifyControllerChanged()
{
	Super::NotifyControllerChanged();
	
	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (!PlayerController)
	{
		return;
	}

	ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
	if (!LocalPlayer)
	{
		return;
	}

	UEnhancedInputLocalPlayerSubsystem* InputSubsystem = LocalPlayer->GetSubsystem<
		UEnhancedInputLocalPlayerSubsystem>();
	if (InputSubsystem && DefaultMappingContext)
	{
		InputSubsystem->AddMappingContext(DefaultMappingContext, 0);
	}
}

어차피 여러번 할텐데 너무 길어져서 나중에 더 작성해야지

profile
이것 저것 다해보는 삶

0개의 댓글