언리얼엔진 강의를 들으며 공부하다 Enhanced Input이 아닌 구버전 Input을 이용해 폰의 이동을 구현하길래 직접 알아보자 하고 공부했고, 그 내용을 정리하고자 한다.
참고로 UE 5버전 이후 엔진에서 기본 프로젝트를 생성하면 예시 기능이 구현되어 있는데, 그 부분을 많이 참고하여 학습했으니 혹시 이 글을 보시는 분들은 코드를 이해하기 어려우시면 직접 코드를 살펴보면서 옮겨 보시길 추천드립니다.
Input보다 더 많은 기능을 제공하고, 런타임에서 매핑정보를 바꿀 수 있다는 점 등 편리함 측면에서 많은 부분이 개선된 입력 체계이다.
크게 4가지 요소가 있으며 간단하게만 짚고 넘어가고자 한다.
Triggered, Pressed, Released 등 입력 시점 및 동작에 따라 보다 더 다양한 이벤트를 발생시킬 수 있다.
이전에 한번 해 본 바가 있고 워낙 흔한 예시라서 간단히 설정완료한 사진만 첨부한다.
먼저 블루프린트를 통해서 절차를 보기 쉽게 정리하고 이를 하나하나 C++ 코드로 옮기는 식으로 진행하는 것이 이해하기 쉬울 것이다.
IMC_Default를 player 0에 매핑하는 부분이다.
IA_Move 이벤트가 발생했을 때 그 데이터를 출력하는 부분이다.
그러면 이런 식으로 이벤트가 발생함에 따라 화면에 출력이 잘 되는 모습을 확인할 수 있다.
#include "CoreMinimal.h"
#include "BasePawn.h"
#include "InputActionValue.h"
#include "Tank.generated.h"
/**
*
*/
UCLASS()
class TOONTANKS_API ATank : public ABasePawn
{
GENERATED_BODY()
public:
ATank();
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;
private:
UPROPERTY(VisibleAnywhere, Category = Input)
class UInputMappingContext* DefaultContext;
UPROPERTY(VisibleAnywhere, Category = Input)
class UInputAction* MoveAction;
protected:
void Move(const FInputActionValue& Value);
};
IMC_Default를 할당할 UInputMappingContext
클래스 포인터와 IA_Move를 할당할 UInputAction
클래스 포인터를 전방 선언하여 선언했다.
전방 선언이란 특정 클래스 객체를 사용하기 위해 헤더 파일에 그 클래스의 헤더 파일을 include 하지 않고 cpp 파일에만 include 함으로써 컴파일 시간을 줄일 수 있는 방법이다.
그리고 void Move()
함수를 만들었다. 인자로 받을 FInputActionValue
참조자는 전방 선언할 수 없으므로 include 해 주었다.
#include "Tank.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
ATank::ATank()
{
static ConstructorHelpers::FObjectFinder<UInputMappingContext>DEFAULT_CONTEXT
(TEXT("/Game/Inputs/IMC_Default"));
if (DEFAULT_CONTEXT.Succeeded())
{
DefaultContext = DEFAULT_CONTEXT.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction>IA_MOVE
(TEXT("/Game/Inputs/IA_Move"));
if (IA_MOVE.Succeeded())
{
MoveAction = IA_MOVE.Object;
}
}
void ATank::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* SubSystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
SubSystem->AddMappingContext(DefaultContext, 0);
}
}
void ATank::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void ATank::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ATank::Move);
}
}
void ATank::Move(const FInputActionValue& Value)
{
if (Value.Get<bool>())
UE_LOG(LogTemp, Warning, TEXT("Input Action Triggered"));
}
코드가 다소 길지만 하나하나 뜯어보면서 알아보자.
먼저 ConstructorHelpers::FObjectFinder
를 통해 포인터 변수를 채워 넣는다. 괄호 안에 들어가는 Path 부분은 해당 파일에 마우스 오버 해 보면 쉽게 찾을 수 있다.
여기 나와있는 Path 말고 우클릭 -> Copy File Path로 하면 필자처럼 피똥을 쌀 수 있다.
만약 클래스를 가져오는 데 성공했다면 이를 각각 DefaultContext
, MoveAction
에 넣어준다.
여기서 헤더파일 InputMappingContext
와 EnhancedInputComponent
가 사용된다.
블루프린트 이벤트그래프에서 이 부분과 대응되는 부분이다.
GetController()
을 APlayerController로 캐스트해서 PlayerController를 가져온다.PlayerController->GetLocalPlayer()
을 Enhanced Input Local Player Subsystem으로 캐스팅해서 GetSubSystem
한다.SubSystem
에 DefaultContext
를 매핑한다.이때 UEnhancedInputLocalPlayerSubSystem 클래스를 사용하기 위해 EnhancedInputSubsystems.h
를 추가한다.
원래 Input 시스템을 사용할 때는 PlayerInputComponent로 바로 매핑이 가능했지만 EnhancedInputComponent로 캐스트해주는 작업을 거쳐야 한다.
이후 EnhancedInputComponent
에서 IA_Move
를 ATank::Move
함수에 바인딩 한다.
SetupPlayerInputComponent에 의해 IA_Move
이벤트가 발생하면 실행될 함수이다. 아직은 이동까지 구현해보기 앞서 함수가 실행됐음을 알기 위해 로그만 찍어본다.
WASD키를 누르면 ATank::Move 함수가 호출되어 로그가 잘 찍히는 것을 확인할 수 있다.