Unreal Engine 클래스 - PlayerController + MyCharacter 리펙터링

GwakItect·2025년 7월 11일

언리얼 엔진5 기초

목록 보기
9/9

PlayerController


언리얼 엔진에서 플레이어의 입력을 처리하고 게임 월드 내의 폰(Pawn) 클래스를 제어하는 핵심적인 역할을 담당하는 클래스이다. (Pawn) 혹은 캐릭터(Character) 클래스가 플레이어 혹은 AI 가 조종하는 아바타라면, PlayerController 는 조종하는 이의 의지나 제어를 나타낸다.

플레이어의 입력이나 시점, UI 등을 관리하지만 시각적으로 표현되지 않으며, 일반적으로 월드(레벨)에 직접 배치되지 않는다.

지난 포스트에서 Character 클래스 내에서 Enhanced Input System 과 연동하여 입력을 처리하고 동작을 수행했는데 이를 객체지향 프로그래밍단일 책임 원칙(SRP)에 따라서 PlayerController 클래스로 분리하는 리펙터링(Refactoring)을 진행해보자




클래스 생성


지난번 생성한 MyCharacter 클래스를 위한 커스텀 플레이어 컨트롤러(Custom PlayerController)를 만들기 위해서는 PlayerController 클래스를 상속받아 필요한 기능을 추가·확장하는 방향으로 작업을 진행해야한다.


PlayerController 를 기반으로 C++ 클래스를 생성해준다. (이름은 MyPlayerController)




GameMode 추가


MyCharacter 에게 MyPlayerController 를 적용하기 위해선 별도의 GameMode 가 필요하다. 일단 간단하게 추가를 하고 자세한 내용을 추후에 작성한다.

Gamemode 를 상속받아 MyGameMode 를 추가하고 이를 블루프린트 클래스로 생성해준다.



이후 Edit - Project Settings 에서 기본 게임모드에 생성한 GameMode 를 지정해주고 디폴트 폰 클래스플레이어 컨트롤러 클래스도 지정해준다.



설정이 끝나면 월드에 직접 배치해놨던 BP_MyCharacter 를 삭제하고 실행해본다.



정상적으로 BP_MyCharacterBP_MyPlayerController 가 생성된 모습을 아웃라이너에서 볼 수 있다.




리펙터링(Refactoring)


이제 MyCharacter 내부에 있던 입력 관련 변수들과 매핑 부분을 MyPlayerController 로 옮겨 SRP를 준수한 코드를 작성한다.




MyCharacter.h


#pragma once

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

class USpringArmComponent;
class UCameraComponent;
//class UInputMappingContext;		제거
//class UInputAction;				제거
struct FInputActionValue;

UCLASS()
class CHARPRAC_API AMyCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	AMyCharacter();

	/*********************
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputMappingContext* DefaultMappingContext;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* MoveAction;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* LookAction;
    
    		제거
    ****************************/

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	USpringArmComponent* SpringArmComp;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* CameraComp;
	
	virtual void BeginPlay() override;
	
	void Move(const FInputActionValue& Value);
	void Look(const FInputActionValue& Value);

public:	
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};




MyCharacter.cpp


#include "MyCharacter.h"
#include "MyPlayerController.h"		// ** 추가  **
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
//#include "EnhancedInputSubsystems.h"	제거
#include "EnhancedInputComponent.h"

AMyCharacter::AMyCharacter()
//	: DefaultMappingContext(nullptr),
//	  MoveAction(nullptr),
//	  LookAction(nullptr)
// 멤버 초기화 리스트 제거
{
	PrimaryActorTick.bCanEverTick = true;

	SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("Spring Arm"));
	SpringArmComp->SetupAttachment(RootComponent);
	SpringArmComp->TargetArmLength = 300.0f;
	SpringArmComp->bUsePawnControlRotation = true;
	
	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName);
	CameraComp->bUsePawnControlRotation = false;

}
void AMyCharacter::BeginPlay()
{
	Super::BeginPlay();
	/***********************************
	if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
			ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>
            		(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(DefaultMappingContext, 0);
		}
	}
    
    		제거
    *************************************/
}

void AMyCharacter::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(MoveInput.Y))
	{
		AddMovementInput(GetActorRightVector(), MoveInput.Y);
	}
}

void AMyCharacter::Look(const FInputActionValue& Value)
{
	FVector2D LookInput = Value.Get<FVector2D>();

	AddControllerYawInput(LookInput.X);
	AddControllerPitchInput(LookInput.Y);
}

void AMyCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

/**************************************************************************/
/********************************   변경  **********************************/
/**************************************************************************/
	if (UEnhancedInputComponent* EnhancedInputComponent = 
    		Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		if (AMyPlayerController* PlayerController = Cast<AMyPlayerController>(GetController()))
		{
			EnhancedInputComponent->BindAction(
            PlayerController->MoveAction, 
            ETriggerEvent::Triggered, 
            this, 
            &AMyCharacter::Move);
            
			EnhancedInputComponent->BindAction(
            PlayerController->LookAction, 
            ETriggerEvent::Triggered, 
            this, 
            &AMyCharacter::Look);
		}
	}
/**************************************************************************/
/**************************************************************************/
/**************************************************************************/
}




MyPlayerController.h


#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MyPlayerController.generated.h"

class UInputMappingContext;
class UInputAction;

UCLASS()
class CHARPRAC_API AMyPlayerController : public APlayerController
{
	GENERATED_BODY()

public:
	AMyPlayerController();
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputMappingContext* DefaultMappingContext;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* MoveAction;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* LookAction;

protected:
	virtual void BeginPlay() override;
};




MyPlayerController.cpp


#include "MyPlayerController.h"
#include "EnhancedInputSubsystems.h"

AMyPlayerController::AMyPlayerController()
	: DefaultMappingContext(nullptr),
	  MoveAction(nullptr),
	  LookAction(nullptr)
{
}

void AMyPlayerController::BeginPlay()
{
	Super::BeginPlay();

	/*
		- MyCharacter.cpp 에서의 이전 내용 -
		
	if (APlayerController* PlayerController = 
    		Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
			ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>
            					(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(DefaultMappingContext, 0);
		}
	}
	*/
	if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = 
        		LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
		{
			if (DefaultMappingContext)
			{
				Subsystem->AddMappingContext(DefaultMappingContext, 0);
			}
		}
	}
}




마무리



마지막으로 BP_MyPlayerController 내에서 입력 변수들의 내용을 채워주고 저장&컴파일 후 실행해본다.


동작이 달라지지 않았으나 결과적으로 하나의 클래스에서 담당하던 역할 2개를 성공적으로 두 개의 클래스로 분할하였다.

  • MyCharacter: 캐릭터 컴포넌트, 입력 바인딩 및 그에 대한 동작 함수
  • MyPlayerController: 플레이어의 입력 처리 및 빙의 등 전체적인 캐릭터 제어

이렇게 클래스 별로 역할을 확실하게 하여 책임을 분산한 더욱 안정적인 코드를 작성했다.

0개의 댓글