언리얼/게임 프로젝트

GameplayAbilitySystem을 이용한 RPG 프로젝트 - (3) PlayerController의 설정과 GameMode

monstro 2024. 11. 16. 14:50
728x90
반응형

이번 포스트에서는 플레이어를 조작하기 위한 PlayerController 클래스의 설정과

게임을 운영하기 위한 GameMode 클래스 생성 및 부가설정을 진행하겠습니다.

 

1) GameMode 클래스

우선 게임을 운영하기 위한 GameMode 클래스를 생성하고 이를 블루프린트로 만들어

프로젝트의 기본 GameMode로 설정하겠습니다.

 

 

 

다음으로 플레이어를 조작하기 위한 PlayerController 클래스를 생성해보겠습니다.

 

2) PlayerController 클래스 - InputAction과 InputAcionMappingContext

언리얼 엔진에서는 입력을 처리하기 위한 2가지 방법을 제안합니다.

하나는 기존의 입력 시스템을 사용하는 것이고,

다른 하나는 EnhancedInput이라는 신형 시스템을 사용합니다.

이번 프로젝트는 후자인 신형 입력 시스템을 사용하겠습니다.

신형 입렷 시스템은 두 가지를 우선 구성해줘야 합니다.

하나는 입력을 통해 발생할 ActionInput Action이고,

다른 하나는 Action을 입력하는 키와 연결하는 InputMappingContext입니다.

 

그리고 위의 둘을 연결하는 것을 PlayerController에서 수행하겠습니다.

우선 언리얼 에디터에서 Action과 InputMappingContext를 생성하겠습니다.

 

이 프로젝트는 탑다운 뷰로 진행되는 게임이기에 최대한 간단하게 구성하겠습니다.

 

 

MappingContext에 대한 추가적인 설명이 필요할 것 같습니다.

위의 사진에서는 IA_Move에 대한 키를 설정하였습니다.

키는 W/S/A/D의 입력을 받게끔 설정하였습니다.

 

기본적으로 입력은 XYZ 순으로 설정되며 각각 (1 , 0, 0)으로 입력을 처리합니다.

이때 Swizzle Input Axis Values 옵션을 설정하면 위의 순서를 바꿀 수 있습니다.

W와 S의 경우 위, 아래를 움직이므로 XYZ가 아닌 YXZ 순서로 움직여야 하는 것이 맞습니다.

이때 좌표계에서 음수 방향으로 움직이는 입력은 옵션을 추가적으로 negate로 설정하여

(-1, 0, 0)의 값으로 움직이게끔 설정하였습니다.

정리하자면 다음과 같습니다.

 

  • W(상) : Swizzle Input Axis Values(YXZ)
  • S(하) : Swizzle Input Axis Values(YXZ) + Negate
  • A (좌): Negate
  • D(우) : 없음

InputAction과 InputMappingContext를 생성하였으므로 PlayerController를 생성하겠습니다.

 

3) PlayerController 클래스 - PlayerController

Enhanced Input을 사용하기 위해서는 우선 관련 플러그인을 추가해줘야 합니다.

build.cs 파일에 다음의 플러그인이 없다면 추가하셔야 합니다.

 

"InputCore",
"EnhancedInput"

 

엔진 버전이 5.3 이상인 경우 기본적으로 설치되어 있지만 없다면 추가를 해주시면 됩니다.

#pragma once

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

// 전방선언
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
class IEnemyInterface;

/**
 * 
 */
UCLASS()
class AURA_API AAuraPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	AAuraPlayerController();

protected:
	virtual void BeginPlay() override;

	virtual void SetupInputComponent() override;

	virtual void PlayerTick(float DeltaTime) override;

private:
	UPROPERTY(EditAnywhere, Category = "Input")
	TObjectPtr<UInputMappingContext> PlayerInputMappingContext;

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

	void Move(const FInputActionValue& InputActionValue);

	// 마우스 커서의 Trace를 구현하는 함수
	void CursorTrace();

	// 이전 프레임에서 커서가 가리킨 인터페이스와 현재 프레임에서 커서가 가리킨 인터페이스를 포인터로 저장
	// TScriptInterface는 Interface 포인터를 위한 래퍼
	TScriptInterface<IEnemyInterface> LastCursorHit;
	TScriptInterface<IEnemyInterface> CurrentCursorHit;
};

 

코드의 구성은 위와 같습니다.

인터페이스 부분은 PlayerController 이후에 작성하겠습니다.

또한 인터페이스를 위한 새로운 래퍼 템플릿인 TScriptableInterface를 사용하였습니다.

이 래퍼템플릿을 사용하면 인터페이스를 따로 캐스팅하지 않아도 되는 이점이 존재합니다.

 

#include "Player/AuraPlayerController.h"
#include "InputMappingContext.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "Interface/EnemyInterface.h"

.
.
.

 

cpp 파일의 헤더 파일 구성은 위와 같습니다.

 

생성자에서는 네트워크 관련 옵션인 bReplicates를 true로 설정하였습니다.

이 경우에, PlayerController는 업데이트를 연결된 모든 클라이언트에게 전달합니다.

void AAuraPlayerController::BeginPlay()
{
	Super::BeginPlay();
	check(PlayerInputMappingContext);

	// LocalPlayerSubSystem은 Action과 키를 연결하는 Mapping Context를 로컬 플레이어에게 적용함
	// 그리고 Mapping Context간의 우선순위를 지정하여 여러 액션 간의 충돌을 해결
	// LocalPlayerSubSystem은 싱글톤 클래스로서 하나의 인스턴스만 존재함
	UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
	check(Subsystem);

	// 현재 만든 PlayerInputMappingContext에 대해 우선 순위를 0으로 지정
	Subsystem->AddMappingContext(PlayerInputMappingContext, 0);

	// 마우스 커서에 대한 설정
	bShowMouseCursor = true;
	DefaultMouseCursor = EMouseCursor::Default;

	// UI에 대한 사용자 입력을 정의 -> 이는 뷰포트에도 동일하며 뷰포트에서 마우스를 숨기지 않음
	FInputModeGameAndUI InputModeData;
	InputModeData.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	InputModeData.SetHideCursorDuringCapture(false);
	SetInputMode(InputModeData);
}

 

BeginPlay 함수는 조금 복잡합니다.

제일 먼저 InputMappingContext가 존재하는지 확인합니다.

그리고 UEnhancedInputLocalPlayerSubsystem을 가져오는데,

언리얼 엔진의 플러그인에는 Subsystem이라는 플러그인의 여러 동작을 수행하는 요소가 존재합니다.

Subsystem에는 플러그인과 함께 행동하며 플러그인이 종료되면 같이 사라지는 역할을 수행합니다.

 

여기서는 EnhancedInput 플러그인의 Subsystem을 가져와

현재 존재하는 InputMappingContext를 제일 먼저 사용하게끔 설정하였습니다.

MappinContext가 여러 개 존재할 경우 가장 먼저 사용하는 순서를 지정하는 것입니다.

 

이후에는 마우스 커서에 대한 설정을 진행하겠습니다.

언리얼 에디터에서는 기본적으로 PlayMode시에 마우스를 감추는데 이를 막게끔 설정하였습니다.

또한 UI에 대해 입력을 처리하는 경우에도 마우스를 숨기지 않게끔 설계하였습니다.

 

void AAuraPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);

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

 

SetInputComponent 함수에서는

InputAction이 발동되면 호출할 콜백 함수를 연결하였습니다.

 

void AAuraPlayerController::PlayerTick(float DeltaTime)
{
	Super::PlayerTick(DeltaTime);

	CursorTrace();
}

 

PlayerTick에서는 마우스Trace를 진행합니다.

 

void AAuraPlayerController::Move(const FInputActionValue& InputActionValue)
{
	const FVector2D InputAxisVector = InputActionValue.Get<FVector2D>();
	const FRotator Rotation = GetControlRotation();
	const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);

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

	if (APawn* ControlledPawn = GetPawn<APawn>())
	{
		ControlledPawn->AddMovementInput(ForwardDirection, InputAxisVector.Y);
		ControlledPawn->AddMovementInput(RightDirection, InputAxisVector.X);
	}
}

 

InputAction에 연결되어 호출되는 콜백함수인 Move는 

회전 행렬을 이용하여 플레이어의 회전과 동시에 이동을 처리합니다.

 

void AAuraPlayerController::CursorTrace()
{
	FHitResult CursorHit;
	GetHitResultUnderCursor(ECC_Visibility, false, CursorHit);
	
	if (!CursorHit.bBlockingHit)
	{
		return;
	}

	// TScriptInterface를 사용한 경우 따로 캐스팅할 필요가 없음
	// 이전 프레임에서의 커서가 가리킨 Actor와 현재 프레임에서 커서가 가리킨 Actor를 초기화
	LastCursorHit = CurrentCursorHit;
	CurrentCursorHit = CursorHit.GetActor();

	if (LastCursorHit == nullptr)
	{
		// 지난 프레임에서의 커서가 가리킨 Actor가 null이고 현재 프레임에서의 커서가 가리킨 Actor가 유효한 경우
		if (CurrentCursorHit != nullptr)
		{
			CurrentCursorHit->HighlightActor();
		}
		// 지난 프레임에서의 커서가 가리킨 Actor가 null이고 현재 프레임에서의 커서가 가리킨 Actor도 null인 경우
		else
		{
			return;
		}
	}
	else
	{
		// 지난 프레임에서의 커서가 가리킨 Actor가 유효하고 현재 프레임에서의 커서가 가리킨 Actor가 null인 경우
		if (CurrentCursorHit == nullptr)
		{
			LastCursorHit->UnHighlightActor();
		}
		// 지난 프레임에서의 커서가 가리킨 Actor가 유효하고 현재 프레임에서의 커서가 가리킨 Actor도 유효한 경우
		// 두 가지로 분기할 수 있음 -> 두 Actor는 같다! 또는 두 Actor는 다르다!
		else
		{
			// 두 Actor는 다르다!
			if (LastCursorHit != CurrentCursorHit)
			{
				LastCursorHit->UnHighlightActor();
				CurrentCursorHit->HighlightActor();
			}
			// 두 Actor는 같다!
			else
			{
				return;
			}
		}
	}
}

 

마우스 트레이스함수인 CursorTrace() 역시도 로직이 조금 복잡합니다.

몬스터를 마우스 커서가 가리키는 동안 몬스터에 하이라이트를 주는 것이 목적인데 상황별로 구분이 필요합니다.

 

지난 프레임에서의 커서가 가리킨 Actor가 null, 현재 프레임에서의 커서가 가리킨 Actor가 유효한 경우

-> 현재 프레임에서 커서가 가리킨 Actor를 하이라이트 합니다.

 

지난 프레임에서의 커서가 가리킨 Actor가 null, 현재 프레임에서의 커서가 가리킨 Actor도 null인 경우

-> 아무것도 하지 않아도 됩니다.

 

지난 프레임에서의 커서가 가리킨 Actor가 유효, 현재 프레임에서의 커서가 가리킨 Actor가 null인 경우

-> 지난 프레임에서의 커서가 가리킨 Actor의 하이라이트를 끕니다.

 

지난 프레임에서의 커서가 가리킨 Actor가 유효, 현재 프레임에서의 커서가 가리킨 Actor도 유효한 경우
위의 경우는 두 가지 분기 사항으로 나뉩니다.

두 Actor는 다르다!

-> 지난 프레임에서의 커서가 가리킨 Actor의 하이라이트는 끄고,

현재 프레임에서의 커서가 가리킨 Actor는 하이라이트합니다.
두 Actor는 같다!

-> 아무것도 하지 않아도 됩니다.

 

4) UEnemyInterface : PlayerController와 몬스터 클래스의 의존성을 낮춤

소제목 그대로 인터페이스를 사용하는 이유는 클래스간의 의존성을 낮추기 위함입니다.

이를 모듈화라고 하는데, 이 경우 코드의 구성과 동작이 독립적으로 작용해 문제 상황을 낮추는 이점이 있습니다.

 

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "EnemyInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UEnemyInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class AURA_API IEnemyInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void HighlightActor() = 0;

	virtual void UnHighlightActor() = 0;
};

 

인터페이스를 상속받은 클래스들은 반드시 추상 함수를 재정의해야 할 필요가 있습니다.

따라서 Enemy 클래스를 다음과 같이 수정하겠습니다.

 

#pragma once

#include "CoreMinimal.h"
#include "Character/AuraCharacterBase.h"
#include "Interface/EnemyInterface.h"
#include "AuraEnemy.generated.h"

/**
 * 
 */
UCLASS()
class AURA_API AAuraEnemy : public AAuraCharacterBase, public IEnemyInterface
{
	GENERATED_BODY()

public:
	AAuraEnemy();
	
protected:
	virtual void HighlightActor() override;

	virtual void UnHighlightActor() override;
};

 

void AAuraEnemy::HighlightActor()
{
	GetMesh()->SetRenderCustomDepth(true);
	GetMesh()->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED);
	Weapon->SetRenderCustomDepth(true);
	Weapon->SetCustomDepthStencilValue(CUSTOM_DEPTH_RED);
}

void AAuraEnemy::UnHighlightActor()
{
	GetMesh()->SetRenderCustomDepth(false);
	Weapon->SetRenderCustomDepth(false);
}

 

위에서의 CUSTOM_DEPTH_RED의 경우, 임의의 원하는 값을 설정하시면 됩니다.

해당 값은 하이라이트의 굵기를 결정합니다.

 

5) 코드 작성 후

이렇게 코드를 작성하고 나면

하이라이트 효과를 연출하기 위한 Post Process Volume을 맵에 추가해야 합니다.

그리고 Post Process Volume의 Infinite Extent 값을 true로 설정해 레벨 전체에 영향을 주도록 설정합니다.

그리고 Post Process에서 사용할 머티리얼을 설정하겠습니다.

 

 

그리고 현재 코드를 보면 하이라이트 효과는 DepthStencilValue에 의해 이뤄지고 있습니다.

이를 위해서는 설정을 해줘야 하는데 Project Setting에서 Post Process 설정이 다음과 같이 되어야 합니다.

 

최종적인 실행결과를 확인해보겠습니다.

 

이제 기본적인 설정이 끝났으므로 GAS 프레임워크를 사용해보겠습니다.

728x90
반응형