Unreal Character Class(3)

정혜창·2025년 1월 23일
0

내일배움캠프

목록 보기
24/43

이제 PlayerController에 InputMappingContext까지 활성화를 시켰다. 이제 PlayerController은 입력에 맞는 IA를 인식을 하므로 Character클래스에 구현부를 호출하는 명령을 해주어야한다.

우선 전체적인 흐름은 다음과 같다고 볼 수 있다.

입력 흐름 개요 (Enhanced Input System 기준)

  1. 사용자가 A 입력 (입력 장치)

    • 키보드에서 "A" 키를 누르거나 게임패드에서 특정 버튼/축 입력 발생.
    • 입력은 운영 체제와 언리얼 엔진의 입력 시스템으로 전달.
  2. PlayerController에서 입력 인식

    • PlayerController가 Enhanced Input System을 통해 입력을 처리한다.
    • PlayerController는 자신의 LocalPlayer를 참조해 입력 처리에 필요한 서브시스템(UEnhancedInputLocalPlayerSubsystem)을 가져옴
  3. LocalPlayerSubsystem 호출

    • UEnhancedInputLocalPlayerSubsystem은 현재 활성화된 Input Mapping Context(IMC)를 기반으로 입력을 처리.
    • "A" 키가 어떤 Input Action(IA_Move)과 매핑되어 있는지 확인.
    • 결과: "A는 Move 동작과 연결됨"이라는 정보를 Enhanced Input System에서 확인.
  4. Input Action 호출 및 데이터 전달

    • "A" 키가 IA_Move Input Action과 매핑된 것으로 확인되면, Input Action이 트리거된다.
    • 트리거된 Input Action은 해당 데이터를 처리할 콜백(바인딩된 함수)으로 전달.
    • 예: PlayerController 또는 Pawn/Character에 바인딩된 Move 함수 호출.
  5. PlayerController 또는 Pawn/Character로 동작 전달

    • PlayerController 또는 Character에서 Input Action에 바인딩된 함수가 호출.
    • 예: "A는 Move 동작이므로 캐릭터를 왼쪽으로 이동해라"라는 명령 전달.
    • 이 과정에서 입력 데이터(Value)가 함수로 전달됌.
    • Value 예: FVector2D(-1, 0) (왼쪽 이동 벡터).
  6. 캐릭터에서 동작 함수 실행 (구현부 호출)

    • Character 클래스의 구현 함수(예: Move())가 호출되어 실제 이동 동작을 처리한다.
    • 예: 입력된 이동 벡터(FVector2D)를 기반으로 캐릭터가 이동하도록 AddMovementInput() 함수 호출.
  7. 화면에 결과 표시 (렌더링)

    • Character의 이동 동작에 따라 Transform(위치/회전)이 업데이트됌.
    • 결과적으로 캐릭터가 화면에서 왼쪽으로 이동하는 모습이 렌더링.

이러한 과정을 토대로 캐릭터 클래스에 액션 바인딩을 추가해보자


1. 캐릭터 액션 바인딩 추가

SpartaCharacter.h

#pragma once

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

class USpringArmComponent;
class UCameraComponent;
// Enhanced Input에서 액션 값을 받을 때 사용하는 구조체
struct FInputActionValue;

UCLASS()
class SPARTAPROJECT_API ASpartaCharacter : public ACharacter
{
		GENERATED_BODY()

public:
		ASpartaCharacter();

protected:
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
		USpringArmComponent* SpringArmComp;
		UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
		UCameraComponent* CameraComp;
	
    // 입력 바인딩을 처리하는 함수
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    // Enhanced Input System에서 Input Action 값은 FInputActionValue로 전달.
    // 참조를 쓰는 이유는 구조체 같은경우 복사비용이 크기 때문에 참조를 사용해서 최적화에 신경쓴다.
    // IA가 bool 값인 경우는 StartJump, StopJump로 나눠주는게 좋다. 
    // Enhanced Input System에서 이 함수들을 인식을 하려면 리플렉션시스템에 등록이 되어있어야한다..
    
    UFUNCTION()
    void Move(const FInputActionValue& value);
    UFUNCTION()
    void StartJump(const FInputActionValue& value);
    UFUNCTION()
    void StopJump(const FInputActionValue& value);
    UFUNCTION()
    void Look(const FInputActionValue& value);
    UFUNCTION()
    void StartSprint(const FInputActionValue& value);
    UFUNCTION()
    void StopSprint(const FInputActionValue& value);
};

SpartaCharacter.cpp

#include "SpartaCharacter.h"
// IA를 가져오기 위한 헤더선언
#include "SpartaPlayerController.h"
// EnhancedInputComponent로 캐스팅을 해줘야하기 때문
#include "EnhancedInputComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"

ASpartaCharacter::ASpartaCharacter()
{
		PrimaryActorTick.bCanEverTick = false;

    SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArmComp->SetupAttachment(RootComponent);
    SpringArmComp->TargetArmLength = 300.0f;
    SpringArmComp->bUsePawnControlRotation = true;

    CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
    CameraComp->bUsePawnControlRotation = false;
}

void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
    
     // Enhanced InputComponent로 캐스팅
    if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
    	if (ASpartaPlayerController* PlayerController = Cast<ASpartaPlayerController>(GetController()))
        {
        	if (PlayerController->MoveAction)
            {
            	EnhancedInput->BindAction(
                	PlayerController->MoveAction,
                    ETriggerEvent::Triggered,
                    this,
                    &ASpartaCharacter::Move
                );
            }
            
            if (PlayerController->LookAction
            {
            	EnhancedInput->BindAction(
                	PlayerController->LookAction,
                    ETriggerEvent::Triggered,
                    this
                    &ASpartaCharacter::Look
                );
            }
            
            if (PlayerController->JumpAction
            {
            	EnhancedInput->BindAction(
                	PlayerController->JumpAction,
                    ETriggerEvent::Triggered,
                    this
                    &ASpartaCharacter::StartJump
                );
                
                EnhancedInput->BindAction(
                	PlayerController->JumpAction,
                    ETriggerEvent::Completed,
                    this,
                    &ASpartaCharacter::StopJump
                );
            }
            
            if (PlayerController->SprintAction
            {
            	EnhancedInput->BindAction(
                	PlayerController->SprintAction,
                    ETriggerEvent::Triggered,
                    this,
                    &ASpartaCharacter::StartSprint
                );
                
                EnhancedInput->BindAction(
                	PlayerController->SprintAction,
                    ETriggerEvent::Completed,
                    this,
                    &ASpartaCharacter::StopSprint
                );
            }       
        }
    }
}

코드분석

  • if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    처음부터 UEnhancedInputComponet를 들고오면 되지 않냐라고 생각할 수 있지만 AActor나 Pawn에서 정의되어 있는 함수의 시그니처를 유지해야 오버라이딩이 가능하고, PlayerInput은 큰 범위이기때문에 EnhancedInput으로의 타입 변환이 필요하다. 따라서 캐스팅작업을 해주어야 함.

  • if (ASpartaPlayerController* PlayerController = Cast<ASpartaPlayerController>(GetController()))
    Unreal Engine은 모든 ACharacter가 AController를 통해 제어되도록 설계되어 있다..
    즉, GetController()는 항상 유효한 컨트롤러(플레이어 또는 AI)를 반환하고, 이를 기반으로 특정 타입으로 캐스팅하여 사용하도록 만들어졌다.
    처음부터 ASpartaPlayerController를 가져오려고 하면 엔진의 기본 흐름과 맞지 않을 수 있고, 이런 작업은 더 많은 커스텀 작업과 유지보수를 필요로 하게 되므로 타입캐스팅을 한다.
    또한 ASpartaPlayerController는 프로젝트에서 정의한 커스텀 컨트롤러 클래스이다.
    기본 APlayerController를 확장해서 커스텀 로직을 추가했기 때문에, ASpartaPlayerController의 고유 기능을 사용하려면 캐스팅이 필요하다.

if (PlayerController->JumpAction
            {
            	EnhancedInput->BindAction(
                	PlayerController->JumpAction,
                    ETriggerEvent::Triggered,
                    this
                    &ASpartaCharacter::StartJump
                );
                
                EnhancedInput->BindAction(
                	PlayerController->JumpAction,
                    ETriggerEvent::Completed,
                    this,
                    &ASpartaCharacter::StopJump
                );
            }

눌렸을 때와 입력이 해당 입력이 완료되었을 때의 구현부를 나누었기 때문에 TriggerEvent::TriggeredTriggerEvent::Completed로 나누었다. Enhanced Input에서 BindAction은 바인딩을 위한 필수 함수이다. 따라서 코드를 풀어서 해석해보면 EnhancedInput의 BindAction을 통해 플레이어 컨트롤에서 JumpAction이 트리거 되면 ASpartaCharacter의 StartJump함수를 호출하겠다. 는 의미이다.


2. 구현부

2-1 Move 구현

ASpartaCharacter.cpp

void ASpartaCharacter::Move(const FInputActionValue& value)
{
	if (!Controller) return;
    
    const FVector2D MoveInput = value.Get<FVector2D>();
    
    if (!FMath::IsNearlyZero(MoveInput.X))
    {
    	AddMovementInput(GetActorForwardVector(), MoveInput.X);
    }

	if (!FMath::IsNearlyZero(Movement.Y))
    {
    	AddMovementInput(GetActorRightVector(), MoveInput.Y);
    }
}

2-2 Jump 구현

ASpartaCharacter.cpp

void ASpartaCharacter::StartJump(const FInputActionValue& value)
{
	if (value.Get<bool>())
    {
    	Jump();
    }
}

void ASpartCharacter::StopJump(const FInputActionValue& value)
{
	if (!value.Get<bool>())
    {
    	StopJumping();
    }
}
  • Jump(), StopJumping()은 Character 클래스에서 기본 제공되는 함수다. 그리고 if(!Controller) return; 이 이미 포함되어 있기 때문에 따로 적지 않음.

2-3 Look 구현

ASpartaCharacter.cpp

void ASpartaCharacter::Look(const FInputActionValue& value)
{
	FVector2D LookInput = value.Get<FVector2D>();
    
    AddControllerYawInput(LookInput.X);
    AddControllerPitchInput(LookInput.Y);
}

2-4 Sprint 구현

ASpartaCharacer.cpp

/* 
Spirnt를 구현하기 위해 헤더파일에서 부분에
NormalSpeed, SprintSpeedMultiplier, SprintSpeed 변수를 
선언해주고 .cpp파일에서 생성자 부분에 초기화를 해준다.

GetCharacterMovement() 함수는 CharacterMovemnetComponent를 가져온다. 
여기안에 있는 MaxWalkSpeed 변수는 캐릭터의 이동속도.
아래에선 NormalSpeed로 초기화한 모습
*/

NormalSpeed = 300.0f;
SprintSpeedMultiplier = 2.0f;
SprintSpeed = NormalSpeed * SprintSpeedMultiplier;

GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;

// 생성자에서 선언 이후 구현함수

void ASpartaCharacter::StartSprint(const FInputActionValue& value)
{
	if(GetCharacterMovement())
    {
    	GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
    }
}

void ASpartaCharacter::StopSprint(const FInputActionValue& value)
{
	if(GetCharacterMovement())
    {
    	GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
    }
}
  • Spirnt를 구현하기 위해 헤더파일에서 부분에 NormalSpeed, SprintSpeedMultiplier, SprintSpeed 변수를 선언해주고 .cpp파일에서 생성자 부분에 초기화를 해준다.

  • GetCharacterMovement() 함수는 CharacterMovemnetComponent를 가져온다.
    MaxWalkSpeed 변수의 값을 바꾸면 즉시 캐릭터의 이동속도가 변경된다.
    위의 생성자에서 MaxWalkSpeed를 NormalSpeed로 초기화한 모습을 볼 수 있고 Sprint가 시작되면 SprintSpeed로 바뀌는 로직을 구현했다.


회고

이제 확실히 캐릭터 클래스를 상속받아 캐릭터를 만들 때 어떤식으로 구현해야되는지 알 것 같다. 바인딩, IMC 활성화 등 처음 보는 코드들이 너무 많고 원래 언리얼 엔진에 등록되어있는 함수명, 변수명등이 비슷한것도 있고 너무 긴 것도 많아서 혼란스럽고 어려웠던 것 같다. 그래도 천천히 정리해보고 하니깐 자신감이 붙는다.

위의 움짤에서 보듯이 이제 막 기능들을 구현한 것 뿐이고 거기에 맞는 애니메이션이 없어서 매우 딱딱하게 움직이는 것을 볼 수 있다. 이제 Animation Bluprint를 활용해서 정말 움직이는 것처럼 구현해야한다. 그러나 이미 강의를 한번 봤는데 이해가 잘안되었었는데 다시 본다고 잘 이해할 수 있을까 겁이 난다.

그리고 과제는 캐릭터를 상속받는게 아니라 Pawn을 상속받아 캐릭터를 구현하는 것이기 때문에 실제 구현부에서 Character에 있던 AddMovementInput, Jump(), GetCharacterMovement()를 쓰지 못한다. 직접 구현해야하는데 잘할 수 있겠지?.. 조금 더 힘내봐야겠다.

profile
Unreal 1기

0개의 댓글

관련 채용 정보