1. PlayerController
PlayerController란
- 사용자가 키보드, 마우스, 게임 패드 등에서 입력을 받으면 해당 입력을 해석해서 캐릭터나 다른 오브젝트에게 동작을 명령하는 클래스 이다.
- 이전에 언급한 GameMode에서 지정해줘야할 한 가지 클래스이며,
- 언리얼 엔진의 철학인 "플레이어의 입력은 PlayerController에서 처리하고, 실제 움직임은 Character가 빙의해서 처리한다" 를 지키기 위해서 존재하며,
- 코드를 구조적으로 관리하기도 쉬워진다.
PlayerController의 주요 기능
- 입력 처리
- 다양한 입력장치(키보드, 마우스 등)의 이벤트 처리
- 언리얼5에서는 Enhanced Input System을 통해 더욱 개선된 방식으로 처리 가능
- C++에서는 SetInputComponent() 함수를 오버라이드해서 구현한다.
- 카메라 제어 로직
- 캐릭터의 시점 회전이나 줌인, 줌 아웃 등 카메라 동작을 수행
- HUD및 UI와 상호작용
- 버튼 클릭, 드래그, 터치 등 이벤트를 받아서 처리할 수 있다.
- 특정 명령을 UI에서 트리거 시킨 후, PlayerController가 이를 GameMode나 Pawn으로 전달할 수 있다.
- Possess / Unpossess
- PlayerController는 특정 Pawn에 Possess 되어서 해당 Pawn을 제어
- Unpossess() 함수를 통해서 Pawn과의 연결을 해제할 수도 있다.
PlayerController 등록 (C++)
- 아래와 같은 코드를 통해서 GameMode에 등록시킬 수 있다.
- StaticClass()는 클래스의 정보를 런타임에 참조할 수 있도록 제공되는 함수이다.
#include "SpartaGameMode.h"
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
ASpartaGameMode::ASpartaGameMode()
{
DefaultPawnClass = ASpartaCharacter::StaticClass();
PlayerControllerClass = ASpartaPlayerController::StaticClass();
}
2. Enhanced Input System
Enhanced Input System이란
- 언리얼5에서 제공하는 input 관리 시스템
- Input Mapping Context와 Input Action으로 나눠진다.
Input Action (IA)
- 캐릭터의 특정 동작을 추상화 하는 단위이다. (움직임, 점프, 공격 등)
- IA에는 다양한 설정이 존재한다. 기본적인 설정들을 살펴보면 다음과 같다
- Value Type은 기본적으로 설정하지만, 나머지는 좀 더 디테일한 구현을 할 때 사용한다.
- Value Type -> 어떤 유형의 값을 제공할지
- Bool : 단순 on/off 입력 (점프, 공격 등)
- Axis1D : -1 ~ 1 범위의 입력 (전진 후진, 자동차 시뮬레이션 등)
- Axis2D : x,y 축을 처리 (캐릭터의 이동, 마우스 이동)
- Axis3D : x,y,z축을 동시에 처리 (비행 시뮬레이션)
- Trigger -> 언제 입력이 활성화 될지
- Pressed : 키를 누르는 순간에만 작동
- Hold : 키를 일정시간 눌렀을 때 작동
- Released : 키를 뗄 때 작동
- Modifier -> 입력값을 수정하거나 변환
- Scale : 입력 값에 일정 배율 곱하기
- Invert : 입력 값을 반전
- DeadZone : 임계값보다 작은 값을 무시 (게임 패드)
- Value Type -> 어떤 유형의 값을 제공할지
- 세팅한 결과
- IA_Move와 IA_Look은 Axis2D로 설정
- IA_Jump와 IA_Sprint는 bool로 설정
- 이번 구현에 있어서는 Value Type만 설정하였다.
InputMappingContext (IMC)
- IMC는 여러개의 IA를 총괄해서 관리하는 파일이다.
- IMC 매핑 중 Modifier 설정
- 스위즐 입력 축 값
- 입력축을 변환하여, 재구성하는 기능 (이때 X, Y, Z의 우선순위를 선택한다)
- 부정
- 입력축 값을 반대로 바꿔주는 기능
- 스위즐 입력 축 값
에디터 세팅
- 주의점
- x축이 앞으로 가는(W) 방향이므로, 정렬을 "XZY"로 바꿔주기
- Look 에서 마우스를 위로 올리는 것과 카메라의 동작은 반대이므로, "부정" 모디파이어 적용하기
C++에서 설정
- PlayerController 헤더파일
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SpartaPlayerController.generated.h"
class UInputMappingContext;
class UInputAction;
UCLASS()
class SCC_PROJECT_API ASpartaPlayerController : public APlayerController
{
GENERATED_BODY()
public:
ASpartaPlayerController();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputMappingContext* InputMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* LookAction;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
UInputAction* SprintAction;
protected:
virtual void BeginPlay() override;
};
- PlayerController cpp파일
- 사용자 (Local Player)를 불러온 후
- 사용자의 Local Player Subsystem을 가져와서 Input Mapping Context를 등록하는 로직
#include "SpartaPlayerController.h"
#include "EnhancedInputSubsystems.h"
ASpartaPlayerController::ASpartaPlayerController()
:InputMappingContext(nullptr)
,MoveAction(nullptr)
,JumpAction(nullptr)
,LookAction(nullptr)
,SprintAction(nullptr)
{
}
void ASpartaPlayerController::BeginPlay()
{
Super::BeginPlay();
if(ULocalPlayer * LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem
= LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
if (InputMappingContext)
{
Subsystem->AddMappingContext(InputMappingContext, 0);
}
}
}
}
3. 실제로 캐릭터 움직이기
Character 클래스에 input action 연결의 개념
- 캐릭터 클래스의 Input Action 동작의 흐름
- PlayerController는 키 입력을 감지한 후,
- Local Player Subsystem에게 해당 키값이 무슨 뜻인지 물어본다.
- Local Player Subsystem이 해당 키 값이 무슨 동작인지 알려준다면
- PlayerController가 Character에게 해당 키 값에 맞는 함수를 호출하도록 명령한다.
- 마지막으로 Character가 해당 함수를 실행
- 지금까지 구현한 내용
- PlayerController를 만든 후
- Local Player Subsystem에게 우리가 어떤 IMC를 쓸 것인지 알려주었다.
- 그렇다면, 다음으로 할 것
- PlayerController가 어떠한 함수를 호출할 지 바인딩 해주어야 한다!
- BindAction() 함수를 사용하여, PlayerController와 바인딩을 해주어야 한다.
Character 클래스에 액션 바인딩 추가
- Character.h
- 주의할 점
- FInputActionValue는 크기가 큰 구조체이므로 각 함수의 인자로 전달할 때, 레퍼런스로 전달해서 복사하지 않도록 하기
- UFUNCTION을 붙여서 리플렉션 시스템에 등록해주기
- Jump와 Sprint와 같은 함수는 Start와 Stop으로 나눠서 구현해주는 것이 좋다.
- 주의할 점
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;
UCLASS()
class SCC_PROJECT_API ASpartaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASpartaCharacter();
protected:
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
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);
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
USpringArmComponent* SpringArmComp;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
UCameraComponent* CameraComp;
private:
float NormalSpeed;
float SprintSpeedMultiplier;
float SprintSpeed;
};
- Character.cpp 파일
- 액션 바인딩을 하는 코드와 각 액션에 해당하는 코드 추가
- 액션 함수
- AddMovementInput, Jump, AddControllerYawInput 등 이미 존재하는 함수를 사용했다.
- 이러한 함수를 쓸 수 있기에 우리가 Character 클래스를 통해 캐릭터를 만드는 것이다.
- 주의할 점
- Move 액션 함수에서 Controller의 유효성 체크를 하여, GetActorForwardVector와 같은 함수를 쓸 때 문제가 없도록 해야 한다.
- Jump나 Sprint의 액션 함수에서는 유효성 체크를 할 필요는 없다.
- 그 이유는 위에서 말한 GetActorForwardVector와 같이 Actor의 값을 가져올 때 유효성 체크를 해야 하지만,
- Jump나 Sprint의 경우 그러한 로직이 없고, 이미 구현된 함수인 Jump()나 StopJumping() 등에서 이미 유효성 체크를 해주기 때문이다.
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "EnhancedInputComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
ASpartaCharacter::ASpartaCharacter()
:NormalSpeed(600.f)
,SprintSpeedMultiplier(1.5f)
,SprintSpeed(NormalSpeed * SprintSpeedMultiplier)
{
PrimaryActorTick.bCanEverTick = false;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
SpringArmComp->SetupAttachment(RootComponent);
SpringArmComp->TargetArmLength = 300.f;
SpringArmComp->bUsePawnControlRotation = true; //카메라 회전 세팅
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(SpringArmComp, USpringArmComponent::SocketName); //끝부분에 붙이기
CameraComp->bUsePawnControlRotation = false;
GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
}
void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
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->JumpAction)
{
EnhancedInput->BindAction(
PlayerController->JumpAction
, ETriggerEvent::Triggered
, this
, &ASpartaCharacter::StartJump);
EnhancedInput->BindAction(
PlayerController->JumpAction
, ETriggerEvent::Completed
, this
, &ASpartaCharacter::StopJump);
}
if (PlayerController->LookAction)
{
EnhancedInput->BindAction(
PlayerController->LookAction
, ETriggerEvent::Triggered
, this
, &ASpartaCharacter::Look);
}
if (PlayerController->SprintAction)
{
EnhancedInput->BindAction(
PlayerController->SprintAction
, ETriggerEvent::Triggered
, this
, &ASpartaCharacter::StartSprint);
EnhancedInput->BindAction(
PlayerController->SprintAction
, ETriggerEvent::Completed
, this
, &ASpartaCharacter::StopSprint);
}
}
}
}
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(MoveInput.Y))
{
AddMovementInput(GetActorRightVector(), MoveInput.Y);
}
}
void ASpartaCharacter::StartJump(const FInputActionValue& value)
{
if (value.Get<bool>())
{
Jump();
}
}
void ASpartaCharacter::StopJump(const FInputActionValue& value)
{
if (!value.Get<bool>())
{
StopJumping();
}
}
void ASpartaCharacter::Look(const FInputActionValue& value)
{
FVector2D LookInput = value.Get<FVector2D>();
AddControllerYawInput(LookInput.X);
AddControllerPitchInput(LookInput.Y);
}
void ASpartaCharacter::StartSprint(const FInputActionValue& value)
{
if (GetCharacterMovement())
{
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
}
}
void ASpartaCharacter::StopSprint(const FInputActionValue& value)
{
if (GetCharacterMovement())
{
GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
}
}
'Unreal Engine' 카테고리의 다른 글
UE5 Issues : Additive Animation (animation sequence 색깔) (0) | 2025.01.26 |
---|---|
Unreal Engine - 캐릭터 만들기 (0) | 2025.01.24 |
Unreal Engine - GameMode, Pawn, Character (0) | 2025.01.22 |
Unreal Engine - 클래스와 변수의 리플렉션 (매크로 지정자 위주) (0) | 2025.01.22 |
Unreal Engine - Actor의 생성과 응용 (0) | 2025.01.21 |