본문 바로가기

개발 공부 기록/UnrealEngine5

이득우의 언리얼 C++ 게임 개발의 정석 UE5로 따라잡기 - 3

개요

UE4 학습 목적으로 옛날에 구매하고 잊고있던 '이득우의 언리얼 C++ 게임 개발의 정석' 책을 언리얼 엔진5 학습차원에서
UE5로 학습하면 어떤 부분이 달라지는 지와 공부를 하며 주의할 내용들에 대한 글입니다.

책의 내용이 궁금하다면 다른 분이 정리한 블로그나 자료를 찾아보는 걸 권장합니다.

 

알라딘 책 구매 링크

 

이득우의 언리얼 C++ 게임 개발의 정석

언리얼 엔진 학습에 목말라하는 게임 개발자에게 단비 같은 언리얼 엔진 프로그래밍 책이다. 에픽게임즈 본사의 개발자 프로그램 언리얼 데브 그랜트를 수상한 저자의 책으로, 언리얼 엔진의

www.aladin.co.kr

 

본문

생성된 폰을 직접 조작하기 위해 프로젝트의 입력을 설정하고 입력의 신호 값을 폰의 움직임으로 변환하도록 언리얼 엔진이 제공하는
폰 무브먼트 컴포넌트를 사용한다. 그리고 애니메이션 블루프린트를 사용해 우리가 제작한 폰에 애니메이션을 넣을 수 있다.

플레이어가 조작하여 움직일 플레이어 캐릭터 폰을 구현할 때 기본적으로 사용할 컴포넌트들은 아래와 같다.

  1. Capsule: 폰의 움직임을 담당하는 충돌 컴포넌트
  2. SkeletalMesh: 캐릭터 애셋을 보여주고 애니메이션을 담당하는 컴포넌트
  3. FloatingPawnMovement: 플레이어의 입력에 따라 캐릭터가 움직이도록 설정해주는 컴포넌트. 중력을 고려하지 않은 간단한 움직임을 구현할 수 있다.
  4. SpringArm: 삼인칭 시점으로 카메라 구도를 편리하게 설정할 수 있는 컴포넌트
  5. Camera: 폰에 카메라 컴포넌트를 부착하면 카메라가 바라보는 게임 세계 화면을 플레이어 화면으로 전송한다.

SpringArm 컴포넌트 공식 문서

 

*애셋을 사용할 경우 캐릭터가 누워있거나 똑바로 서있지 않는 경우가 존재한다.
이 경우 스켈레탈 메시 컴포넌트를 Z축으로 회전시켜서 원하는 각도로 맞춰주면 된다.

이러한 이유는 캐릭터를 제작할 때 사용했던 3D 소프트웨어와 언리얼 엔진의 3차원 좌표계가 달라서 발생하는 문제이다.

대표적인 게임 엔진들과 3D 모델링 개발 툴의 좌표계 분류 사진

 

기존 책은 UE4를 기준으로 작성되어 있어서 입력 동작과 축 매핑(Input Action and Axis Mapping)이란 기능을 기반으로 작성되어 있었지만
UE5에서의 입력은 ‘향상된 입력(Enhanced Input)’이라는 플러그인으로 대체 되었다.
따라서 변경된 향상된 입력 시스템을 토대로 학습하도록 하였다.

언리얼 엔진5에서 프로젝트 세팅 -> 입력에 가면 알림 텍스트가 나와있다.

 

프로젝트 세팅 → 입력 → Default Classes 에서 기본 플레이어 입력 클래스, 기본 입력 컴포넌트 클래스가 Enhanced 인지 확인하면 된다.
UE5.2부터는 기본으로 설정되어 있으며 그 이전은 직접 플러그인 세팅에서 Enhaced Input 플러그인을 설치하여 사용해야 한다.

 

Enhanced Input을 사용하는 법은 콘텐츠 브라우저의 추가를 누르고 입력에서 ‘입력 액션’과 ‘입력 매핑 컨텍스트’ 등을 사용하면 된다.

 

기본적으로 언리얼에서 구현되어 있는 예시를 확인해보면 입력 액션(Input Action)은
접두사 ‘IA_’를 붙이고 뒤에 어떤 행동이 나오는 지를 적는 것이 명명 규칙이다.

예시로 IA_Move의 경우 캐릭터의 기본 이동이 구현되어 있으며 액션의 ‘값 타입’에서 입력이 들어왔을 시 동작 유형에 따라 어떤 값을 다룰 지를 정하면 된다.

  • Digital(bool): 켜짐 or 꺼짐 상태가 있는 입력에 사용된다. ex) 아이템 줍기 or 웅크리기
  • Axis1D(float): 한 축으로만 이동할 때 사용된다.
  • Axis2D(Vector2D): 걷기와 같은 캐릭터의 동작을 구현할 때 사용
  • Acis3D(Vector3D): 모션 컨트롤러 정보와 같은 더 복잡한 데이터를 사용할 때 사용된다.

이렇게 네 종류가 존재한다.

 

‘트리거 상태’(Trigger State)는 액션의 현재 상태를 나타낸다.

  • Triggered: 모든 트리거 요구 사항이 완료되어 액션이 트리거 된 상태.
    예를 들어, 사용자가 키를 놓으면 “눌렀다 놓기” 트리거가 전송됨.
  • Started: 트리거를 시작한 이벤트 한 번 발생.
  • Ongoing: 트리거가 진행 중인 상태. 예를 들어 “누르고 있기” 동작 중 버튼을 누르고 있으면 발생하며 트리거에 따라 동작 중 모든 틱에 실행됨.
  • Completed: 트리거 프로세스가 완료됨.
  • Canceled: 트리거가 취소 된 경우 예를 들어, 사용자가 “누르고 있기” 동작이 트리거 되기 전에 버튼을 놓은 경우

입력 매핑 컨텍스트의 기본 구조는 최상위 수준에 입력 작업 목록(IA_동작)이 있는 계층 구조이다.

입력 작업 수준 아래에는 키, 버튼, 이동 축과 같이 각 입력 작업을 트리거 할 수 있는 사용자 입력 목록이 존재한다.

최하위에는 각 사용자 입력에 대한 입력 트리거 및 입력 수정자(모디파이어) 목록이 포함되어 있다.

입력 매핑 컨텍스트의 경우 ‘IMC_’ 접두사를 붙이는 명명 규칙을 사용한다.

아래는 기본으로 구현되어 있는 IMC_Default 이다.

 

향상된 입력의 이러한 하위 시스템 계층 구조를 통해서 컨텍스트 중 하나 이상을 플레이어에게 적용시킬 수 있으며
여기에 우선순위 지정을 하여 동일한 입력이 들어왔을 때 여러 작업 간의 충돌을 방지할 수 있다.

예를 들어 수영, 걷기, 운전을 할 수 있는 캐릭터에게 여러 개의 입력 매핑 컨텍스트를 제공하면
하나는 항상 동일한 사용자 입력에 매핑되는 일반적인 작업 컨텍스트로 설정하고
다른 것들은 각 개별 이동 모드에 대한 컨텍스트로 설정할 수 있게 된다.

'입력 수정자(모디파이어)'는 언리얼 엔진에서 입력 트리거로 보내기 전에 수신하는 원시 입력 값을 변경하는 사전 처리기를 의미한다.
축의 순서 변경, 데드 존 구현, 축 입력을 월드 공간으로 변환하는 것과 같은 작업을 수행하는 것들이 존재한다.

대표적인 예시로 2차원 입력에서 방향을 입력하는 것을 구현할 때 사용된다. 주로 방향키나 “WASD” 키 구성과 같을 때 적용할 수 있다.

기본적인 방향 입력은 "오른쪽 키를 입력 받을 때 X축, 양의 방향 1로 이동"가 기준이라 생각하면 편하다.
여기에서 스위즐 입력 축 값(Swizzle Input Axis Values)를 모디파이어에 제공하면 축의 정보를 Y축으로 변환할 수있다.
따라서 ‘W’키를 눌러 캐릭터를 기준으로 앞(Y축)으로 이동하고자 하면 W키에 설정된 모디파이어에 스위즐 입력 축 값을 넣어주면 된다.

반대로 ‘S’키를 눌러 뒤로 갈 경우에도 동일하게 스위즐 입력 축 값을 처리하고 다음
부정(Negate)을 넣어주면 방향에 대한 값이 음의 방향 -1로 변환되어 뒤로 갈 수 있게 된다.

이를 응용하면 ‘D’키는 기존 X축 + 양의 방향 1의 조합으로 오른쪽으로 가며, ‘A’키를 왼쪽으로 가게 하려면
스위즐 입력 축 값을 통해 Y축 전환 + 부정을 통해 음의 방향 -1 로 설정해주면 4방향 이동에 대한 설정이 완성된다.

 

더 자세한 향상된 입력과 관련된 프로세스와 설명은 아래 공식 문서 링크에 잘 나와있다.

향상된 입력 공식 문서

 

위의 내용들을 바탕으로 하여 만들어둔 폰 클래스에 Input Action들과 Input Mapping Context를 할당하여
설정하려면 아래의 내용을 헤더파일과 cpp에 추가를 하면 된다.

//ABPawn.h

#include "InputActionValue.h"

//... 생략 ...//

// 각 액션의 로직이 들어가 수행될 함수 전방 선언
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);

// ... 생략 ... //

private:
// InputAction과 InputMappingContext를 선언
	UPROPERTY(EditAnywhere, Category = Input, meta = (AllowPrivateAccess = true))
	class UInputMappingContext* DefaultContext;

	UPROPERTY(EditAnywhere, Category = Input, meta = (AllowPrivateAccess = true))
	class UInputAction* MoveAction;

	UPROPERTY(EditAnywhere, Category = Input, meta = (AllowPrivateAccess = true))
	class UInputAction* LookAction;

 

// ABPawn.cpp
// 향상된 입력과 관련된 헤더 파일 추가 및 입력 매핑 컨텍스트 헤더 추가
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"

AABPawn::AABPawn()
{
// ... 생략 ... //

// 생성자에서 헤더파일에 선언한 InputAction들의 클래스 포인터 변수에 각 애셋의 레퍼런스를 입력하여 할당 
	static ConstructorHelpers::FObjectFinder<UInputMappingContext> IMC_MOVE(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/ThirdPerson/Input/IMC_PlayerMove.IMC_PlayerMove'"));
	if (IMC_MOVE.Succeeded())
	{
		DefaultContext = IMC_MOVE.Object;
	}

	static ConstructorHelpers::FObjectFinder<UInputAction> IA_MOVE(TEXT("/Script/EnhancedInput.InputAction'/Game/ThirdPerson/Input/IA_Player_Move.IA_Player_Move'"));
	if (IA_MOVE.Succeeded())
	{
		MoveAction = IA_MOVE.Object;
	}

	static ConstructorHelpers::FObjectFinder<UInputAction> IA_LOOK(TEXT("/Script/EnhancedInput.InputAction'/Game/ThirdPerson/Input/IA_Player_Look.IA_Player_Look'"));
	if (IA_LOOK.Succeeded())
	{
		LookAction = IA_LOOK.Object;
	}
	// 폰의 Yaw 회전을 가능캐 함
	this->bUseControllerRotationYaw = true;
}

void AABPawn::BeginPlay()
{
	Super::BeginPlay();
	
    // 향상된 입력 로컬 플레이어의 시스템에 추가한 입력 매핑 컨텍스트를 추가함.
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* SubSystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			SubSystem->AddMappingContext(DefaultContext, 0);
		}
	}
}

void AABPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
    // 향상된 입력 컴포넌트에 만들어 둔 InputAction들을 바인딩
	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AABPawn::Move);
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AABPawn::Look);
		
	}
}

void AABPawn::Move(const FInputActionValue& Value)
{
	// 이동에 사용될 방향과 속도
	const FVector2D MovementVector = Value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		// 플레이어의 현재 방향
		const FRotator Rotation = Controller->GetControlRotation();
		
		// Yaw만을 사용해 새로운 회전 생성
		const FRotator YawRotation(0, Rotation.Yaw, 0);
		
		// Yaw회전을 기준으로 전방 방향 계산
		const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
		// 전방 방향과 Y값(전방 이동속도)을 이용하여 이동
		AddMovementInput(ForwardDirection, MovementVector.Y);

		// Yaw회전을 기준으로 우측 방향 계산
		const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
		// 우측 방향과 X값(우측 이동속도)을 이용하여 이동
		AddMovementInput(RightDirection, MovementVector.X);
	}
}

void AABPawn::Look(const FInputActionValue& Value)
{
	// 회전량
	const FVector2D LookVector = Value.Get<FVector2D>();
	// X값(수평 회전값)을 사용해 Yaw 회전 -> 수평 시선 회전
	AddControllerYawInput(LookVector.X);
	// Y값(수직 회전량)을 사용해 Pitch 회전 -> 수직 시선 회전
	AddControllerPitchInput(LookVector.Y);
}

 

폰이 생성되었을 때 마우스를 이용한 회전을 하지 않는다면 생성된 폰의 디테일창에서 ‘컨트롤러 회전 요 사용’이 true로 되어있는지 확인한다.
내 코드에서는 생성자에서 아래와 같이 사용해줘서 허용을 해주고 있다.

// 폰의 Yaw 회전을 가능캐 함
// this는 APawn
	this->bUseControllerRotationYaw = true;