▣ PlayerController는 현재 자신이 제어할 Pawn을 변경할 수 있습니다.
예를 들어 게임을 만들다 보면
플레이어 캐릭터
↓
차량 탑승
↓
차량 조작
↓
차량에서 내림
↓
다시 플레이어 캐릭터 조작
처럼 플레이어의 입력 주체를 바꿔야 하는 경우가 있습니다.
또는
캐릭터 사망
↓
새로운 캐릭터 생성
↓
새로운 캐릭터 조종
이나
Player
↓
드론 조종
↓
Player 복귀
같은 시스템에서도 같은 구조를 사용할 수 있습니다.
이때 사용하는 대표적인 함수가
Possess()
와
UnPossess()
입니다.
이번 글에서는 Unreal Engine 5.8을 기준으로
PlayerController에서 현재 Pawn 확인
Possess로 다른 Pawn 제어
UnPossess로 제어 해제
OnPossess와 OnUnPossess
Pawn 교체
Auto Possess Player 주의점
멀티플레이에서 Possess 사용할 때 주의점
을 알아보겠습니다.
Possess란?
Possess()는 Controller가 특정 Pawn의 제어권을 가져오는 함수입니다.
Possess(TargetPawn);
구조는 간단합니다.
PlayerController
↓
Possess
↓
Pawn
Possess가 완료되면 PlayerController가 해당 Pawn을 제어하게 됩니다.
Character도 Pawn을 상속하기 때문에 Character 역시 Possess할 수 있습니다.
APawn
↑
ACharacter
따라서 일반적인 TPS Character에도 그대로 사용할 수 있습니다.
현재 제어 중인 Pawn 확인하기
PlayerController가 현재 Possess하고 있는 Pawn은
GetPawn()
으로 가져올 수 있습니다.
APawn* CurrentPawn = GetPawn();
if (IsValid(CurrentPawn))
{
UE_LOG(
LogTemp,
Log,
TEXT("Current Pawn : %s"),
*CurrentPawn->GetName()
);
}
PlayerController가 아무 Pawn도 Possess하고 있지 않다면 nullptr일 수 있습니다.
UnPossess란?
현재 Pawn에 대한 제어권을 해제하려면
UnPossess();
를 사용합니다.
구조는
PlayerController
↓
UnPossess
↓
Pawn 제어 해제
입니다.
중요한 것은
UnPossess
≠
Pawn Destroy
라는 점입니다.
UnPossess()는 Pawn 자체를 삭제하는 함수가 아닙니다.
단순히 Controller와 Pawn의 제어 관계를 끊습니다.
따라서
PlayerController
X
Pawn
상태가 되더라도 Pawn 자체는 World에 계속 존재할 수 있습니다.
PlayerController 클래스 만들기
이번에는 직접 만든 PlayerController에서 Pawn 제어 기능을 구현해보겠습니다.
부모 클래스는
PlayerController
를 선택합니다.
클래스 이름은 예제로
MRGPlayerController
를 사용하겠습니다.
MRGPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "MRGPlayerController.generated.h"
class APawn;
UCLASS()
class YOURPROJECT_API AMRGPlayerController
: public APlayerController
{
GENERATED_BODY()
public:
// 지정한 Pawn의 제어권을 가져옵니다.
//
// 정상적으로 Possess했다면 true를 반환합니다.
UFUNCTION(BlueprintCallable, Category = "Pawn")
bool PossessPawn(APawn* NewPawn);
// 현재 Pawn의 제어를 해제합니다.
UFUNCTION(BlueprintCallable, Category = "Pawn")
void ReleasePawn();
// 현재 Possess 중인 Pawn 이름을 출력합니다.
UFUNCTION(BlueprintCallable, Category = "Pawn")
void PrintCurrentPawn() const;
protected:
// 새로운 Pawn을 Possess했을 때 호출됩니다.
virtual void OnPossess(APawn* InPawn) override;
// 현재 Pawn을 UnPossess할 때 호출됩니다.
virtual void OnUnPossess() override;
};
YOURPROJECT_API는 실제 프로젝트의 API Macro로 변경합니다.
MRGPlayerController.cpp
#include "MRGPlayerController.h"
#include "GameFramework/Pawn.h"
bool AMRGPlayerController::PossessPawn(
APawn* NewPawn
)
{
// 전달받은 Pawn이 유효한지 확인합니다.
if (!IsValid(NewPawn))
{
UE_LOG(
LogTemp,
Warning,
TEXT("PossessPawn : NewPawn is invalid")
);
return false;
}
// 이미 현재 제어 중인 Pawn이라면
// 다시 Possess할 필요가 없습니다.
if (GetPawn() == NewPawn)
{
return true;
}
// Possess는 Authority에서 실행해야 합니다.
//
// 싱글플레이에서는 일반적으로
// 로컬이 Authority 역할을 하므로 그대로 실행됩니다.
if (!HasAuthority())
{
UE_LOG(
LogTemp,
Warning,
TEXT("PossessPawn : Authority required")
);
return false;
}
// 새로운 Pawn의 제어권을 가져옵니다.
Possess(NewPawn);
// 실제로 Possess가 성공했는지 확인합니다.
return GetPawn() == NewPawn;
}
void AMRGPlayerController::ReleasePawn()
{
// Possess와 마찬가지로
// Authority에서 처리합니다.
if (!HasAuthority())
{
return;
}
// 현재 Possess 중인 Pawn이 없다면
// 아무 작업도 하지 않습니다.
if (!IsValid(GetPawn()))
{
return;
}
// 현재 Pawn의 제어권을 해제합니다.
UnPossess();
}
void AMRGPlayerController::PrintCurrentPawn() const
{
const APawn* CurrentPawn = GetPawn();
if (!IsValid(CurrentPawn))
{
UE_LOG(
LogTemp,
Log,
TEXT("Current Pawn : None")
);
return;
}
UE_LOG(
LogTemp,
Log,
TEXT("Current Pawn : %s"),
*CurrentPawn->GetName()
);
}
void AMRGPlayerController::OnPossess(
APawn* InPawn
)
{
Super::OnPossess(InPawn);
UE_LOG(
LogTemp,
Log,
TEXT("Possess : %s"),
*GetNameSafe(InPawn)
);
// 새로운 Pawn을 제어하기 시작했을 때 필요한
// 초기화 코드를 여기에 넣을 수 있습니다.
}
void AMRGPlayerController::OnUnPossess()
{
// Super 호출 전에 기존 Pawn을 기억합니다.
APawn* OldPawn = GetPawn();
Super::OnUnPossess();
UE_LOG(
LogTemp,
Log,
TEXT("UnPossess : %s"),
*GetNameSafe(OldPawn)
);
// 기존 Pawn의 제어가 끝났을 때 필요한
// 정리 코드를 여기에 넣을 수 있습니다.
}
PossessPawn 함수
핵심 부분은 다음 코드입니다.
Possess(NewPawn);
PlayerController가 현재 다른 Pawn을 제어하고 있었다면 새로운 Pawn으로 제어 대상이 변경됩니다.
사용할 때는
PlayerController->PossessPawn(TargetPawn);
처럼 호출할 수 있습니다.
Possess할 Pawn은 이미 존재해야 한다
Possess()는 Pawn을 생성하는 함수가 아닙니다.
Possess(TargetPawn);
에서 TargetPawn은 이미 존재하는 Pawn이어야 합니다.
즉
Pawn 생성
↓
Pawn Pointer 획득
↓
Possess
순서입니다.
예를 들어 레벨에 이미 배치되어 있는 Character를 Possess할 수도 있고,
게임 도중 생성한 새로운 Pawn을 Possess할 수도 있습니다.
Pawn 생성 자체는 이후 SpawnActor() 글에서 따로 알아보겠습니다.
현재 Pawn과 같은 Pawn인지 확인
이번 코드에서는
if (GetPawn() == NewPawn)
{
return true;
}
도 추가했습니다.
이미 해당 Pawn을 제어하고 있다면 다시 Possess할 필요가 없기 때문입니다.
OnPossess
PlayerController가 새로운 Pawn을 Possess했을 때 처리해야 할 코드가 있다면
OnPossess()
를 Override할 수 있습니다.
void AMRGPlayerController::OnPossess(
APawn* InPawn
)
{
Super::OnPossess(InPawn);
UE_LOG(
LogTemp,
Log,
TEXT("Possess : %s"),
*GetNameSafe(InPawn)
);
}
여기서 InPawn은 새롭게 제어하게 된 Pawn입니다.
OnPossess는 언제 사용할까?
예를 들어 Pawn을 바꿀 때마다
HUD 갱신
현재 Character 저장
카메라 관련 초기화
입력 상태 초기화
Pawn별 UI 변경
같은 작업이 필요할 수 있습니다.
그럴 때 Possess()를 호출한 모든 곳에 같은 코드를 반복해서 작성하기보다
OnPossess()
에서 공통 처리를 할 수 있습니다.
OnUnPossess
반대로 Pawn의 제어를 해제할 때는
OnUnPossess()
가 호출됩니다.
void AMRGPlayerController::OnUnPossess()
{
APawn* OldPawn = GetPawn();
Super::OnUnPossess();
UE_LOG(
LogTemp,
Log,
TEXT("UnPossess : %s"),
*GetNameSafe(OldPawn)
);
}
OnUnPossess()에는 Pawn Parameter가 따로 없기 때문에 기존 Pawn이 필요하다면 Super 호출 전에 Pointer를 받아둘 수 있습니다.
GetPawn은 PlayerController의 현재 제어 대상
구조를 정리하면
PlayerController
↓
GetPawn()
↓
현재 제어 중인 Pawn
입니다.
예를 들어 현재 Character가 필요한 경우
APawn* CurrentPawn = GetPawn();
으로 가져올 수 있습니다.
Character가 확실한 경우에는
ACharacter* Character =
Cast<ACharacter>(GetPawn());
처럼 사용할 수도 있습니다.
하지만 PlayerController는 Character뿐만 아니라 일반 Pawn도 Possess할 수 있기 때문에 무조건 Character라고 가정해서는 안 됩니다.
Pawn을 바꾸는 실제 흐름
예를 들어 Character A를 조종하고 있다고 하겠습니다.
PlayerController
↓
Character A
이 상태에서
PossessPawn(CharacterB);
를 실행합니다.
그러면 최종적으로
PlayerController
↓
Character B
를 제어하게 됩니다.
Character A는 삭제되는 것이 아닙니다.
World에 그대로 남아 있을 수 있습니다.
차량 탑승에도 같은 구조를 사용할 수 있다
예를 들어
BP_PlayerCharacter
BP_CarPawn
이 있다고 하겠습니다.
차량에 탑승하면
PossessPawn(CarPawn);
을 호출합니다.
구조는
PlayerController
│
│ Possess
▼
CarPawn
이 됩니다.
차량에서 내릴 때 다시 Player Character의 Pointer를 가지고 있다면
PossessPawn(PlayerCharacter);
를 호출할 수 있습니다.
이런 식으로
Character
Vehicle
Drone
Turret
등 서로 다른 Pawn 사이에서 플레이어의 제어권을 이동시킬 수 있습니다.
이전 Pawn을 다시 사용하려면 Pointer가 필요하다
차량에서 다시 Character로 돌아오려면 이전 Character를 알고 있어야 합니다.
예를 들어 PlayerController에 다음과 같은 변수를 둘 수 있습니다.
TWeakObjectPtr<APawn> PreviousPawn;
새로운 Pawn을 Possess하기 전에
PreviousPawn = GetPawn();
으로 기존 Pawn을 저장할 수 있습니다.
예를 들어
bool AMRGPlayerController::PossessPawn(
APawn* NewPawn
)
{
if (!IsValid(NewPawn))
{
return false;
}
if (GetPawn() == NewPawn)
{
return true;
}
if (!HasAuthority())
{
return false;
}
// 현재 Pawn을 나중에 다시 사용할 수 있도록 저장합니다.
PreviousPawn = GetPawn();
Possess(NewPawn);
return GetPawn() == NewPawn;
}
그리고 다시 돌아갈 때
if (APawn* Pawn = PreviousPawn.Get())
{
Possess(Pawn);
}
처럼 사용할 수 있습니다.
이 경우 TWeakObjectPtr을 사용하면 이전 Pawn을 기억하더라도 그 Pawn의 수명을 이 Pointer가 유지하지 않도록 만들 수 있습니다.
UnPossess만 호출하면 어떻게 될까?
현재
PlayerController
↓
PlayerCharacter
상태에서
UnPossess();
를 호출하면
PlayerController
X
PlayerCharacter
가 됩니다.
PlayerController는 존재하지만 제어하는 Pawn이 없는 상태입니다.
따라서
GetPawn()
은 이후 nullptr을 반환할 수 있습니다.
UnPossess 후 입력은?
PlayerController 자체는 여전히 존재합니다.
하지만 현재 조종하는 Pawn이 없다면 Pawn 쪽의 이동이나 공격 입력으로 제어할 대상이 없습니다.
따라서 일반 게임에서는
UnPossess
↓
아무것도 하지 않음
상태를 오래 유지하기보다
UnPossess
↓
Spectator
또는
새 Pawn Possess
같은 흐름으로 이어지는 경우가 많습니다.
Pawn의 Controller 확인하기
반대로 Pawn 입장에서
나를 누가 제어하고 있는가?
를 확인할 수도 있습니다.
AController* CurrentController =
GetController();
Character 안에서 현재 PlayerController가 필요한 경우
APlayerController* PlayerController =
Cast<APlayerController>(
GetController()
);
처럼 사용할 수 있습니다.
Player가 제어 중인지 확인
Pawn에는
IsPlayerControlled()
함수도 있습니다.
if (IsPlayerControlled())
{
UE_LOG(
LogTemp,
Log,
TEXT("Player Controlled Pawn")
);
}
사람이 사용하는 PlayerController가 현재 이 Pawn을 제어하고 있는지 확인할 때 사용할 수 있습니다.
Auto Possess Player
Pawn이나 Character Blueprint를 선택하면 Details에서
Pawn
↓
Auto Possess Player
설정을 볼 수 있습니다.
예를 들어
Player 0
으로 지정하면 게임 시작 시 해당 Pawn이 첫 번째 PlayerController에 의해 자동으로 제어될 수 있습니다.
C++에서도
AutoPossessPlayer =
EAutoReceiveInput::Player0;
처럼 설정할 수 있습니다.
GameMode의 DefaultPawnClass와 차이
앞에서 알아본 GameMode에서는
DefaultPawnClass
를 설정했습니다.
GameMode는 플레이어가 게임에 참여할 때 사용할 기본 Pawn 클래스를 결정할 수 있습니다.
반면
Auto Possess Player
는 이미 Level에 배치되어 있거나 생성된 Pawn이 특정 PlayerController의 제어를 자동으로 받도록 설정하는 기능입니다.
일반적인 플레이어 시작 구조에서는
GameMode
↓
DefaultPawnClass 생성
↓
PlayerController
↓
Possess
방식을 사용하는 것이 자연스럽습니다.
테스트용 Pawn에서 Auto Possess 주의
Pawn 두 개를 Level에 배치해서 Possess 전환을 테스트한다고 하겠습니다.
BP_PlayerA
BP_PlayerB
두 Pawn 모두
Auto Possess Player
→ Player 0
으로 설정하면 시작 제어권이 의도와 다르게 잡힐 수 있습니다.
따라서 수동으로 Possess할 두 번째 Pawn은
Auto Possess Player
↓
Disabled
로 두는 것이 좋습니다.
PlayerController를 GameMode에 등록
직접 만든
AMRGPlayerController
를 사용하려면 GameMode가 해당 PlayerController를 사용하도록 설정해야 합니다.
C++에서 설정한다면
PlayerControllerClass =
AMRGPlayerController::StaticClass();
처럼 지정할 수 있습니다.
또는 기존 방식처럼
C++ GameMode
↓
BP_GameMode
↓
Class Defaults
↓
Player Controller Class
에서 Blueprint PlayerController를 지정할 수도 있습니다.
예를 들어
BP_MRGPlayerController
를 만들어
Player Controller Class
↓
BP_MRGPlayerController
로 설정합니다.
간단한 테스트 방법
기존 Character를 기본 Pawn으로 시작한다고 하겠습니다.
PlayerController
↓
BP_PlayerCharacter
그리고 Level에 테스트용 Pawn을 하나 더 배치합니다.
BP_TestPawn
BP_TestPawn의
Auto Possess Player
는 Disabled로 둡니다.
테스트 시 해당 Pawn Reference를 PlayerController의
PossessPawn()
에 전달합니다.
실행 전 로그는
Current Pawn : BP_PlayerCharacter
입니다.
BP_TestPawn을 Possess하면
Possess : BP_TestPawn
이 출력되고
GetPawn()
도 BP_TestPawn을 반환합니다.
ReleasePawn 테스트
다음으로
ReleasePawn();
을 호출합니다.
Output Log에서
UnPossess : BP_TestPawn
을 확인할 수 있습니다.
이후
PrintCurrentPawn();
을 호출하면
Current Pawn : None
이 출력됩니다.
Possess와 SetPawn 차이
AController에는
SetPawn()
이라는 함수도 있습니다.
하지만 일반 Gameplay 코드에서 Pawn의 제어권을 바꾸기 위해
SetPawn(NewPawn);
을 직접 사용하는 것은 권장되는 방식이 아닙니다.
공식 API에서도 SetPawn()은 일반적으로 Possess / UnPossess 과정 내부에서 사용하는 함수로 설명합니다.
플레이어가 실제 Pawn의 제어권을 가져와야 한다면
Possess()
를 사용합니다.
Possess와 입력
Possess가 변경되면 PlayerController가 제어하는 Pawn도 변경됩니다.
다만 새 Pawn에서 실제 이동과 공격이 정상적으로 작동하려면 해당 Pawn에도 필요한 입력 처리와 이동 기능이 구현되어 있어야 합니다.
예를 들어
Character A
→ 이동 기능 있음
Pawn B
→ 이동 기능 없음
상태에서 Pawn B를 Possess했다고 해서 Character A의 이동 코드가 자동으로 Pawn B에 생기는 것은 아닙니다.
Possess는
누구를 제어할 것인가?
를 연결하는 기능입니다.
실제 Pawn의 행동은 각 Pawn 클래스가 구현해야 합니다.
OnPossess와 Pawn 쪽 이벤트
Controller에서는
OnPossess()
를 Override할 수 있습니다.
Pawn 쪽에서도 Controller에게 Possess되었을 때
PossessedBy()
를 Override할 수 있습니다.
예를 들어
void AMyCharacter::PossessedBy(
AController* NewController
)
{
Super::PossessedBy(NewController);
UE_LOG(
LogTemp,
Log,
TEXT("Character Possessed")
);
}
처럼 사용할 수 있습니다.
역할을 구분하면
PlayerController
→ OnPossess
Pawn
→ PossessedBy
입니다.
UnPossess도 Pawn에서 확인 가능
Pawn에서는 제어권을 잃었을 때
UnPossessed()
를 Override할 수 있습니다.
void AMyCharacter::UnPossessed()
{
Super::UnPossessed();
UE_LOG(
LogTemp,
Log,
TEXT("Character UnPossessed")
);
}
따라서
Controller 관점
OnPossess
OnUnPossess
와
Pawn 관점
PossessedBy
UnPossessed
을 각각 사용할 수 있습니다.
Possess 상태 변경을 감지하는 Delegate
AController에는
OnPossessedPawnChanged
Delegate도 있습니다.
현재 Possessed Pawn이 바뀌었을 때 호출됩니다.
특히
HUD
UI
카메라 시스템
Pawn 상태 표시
처럼 PlayerController가 어떤 Pawn을 제어하는지 감지해야 하는 시스템에서 활용할 수 있습니다.
처음에는 OnPossess()와 OnUnPossess()부터 익히고, 여러 시스템에 상태 변경을 알려야 할 때 Delegate를 사용하는 식으로 확장하면 됩니다.
Possess는 멀티플레이에서 주의
Possess()를 사용할 때 중요한 점이 있습니다.
UE 5.8 공식 API에서 AController::Possess()는
Network Authority
에서 실행되는 함수입니다.
즉 멀티플레이에서 클라이언트가 임의로
Possess(NewPawn);
를 호출해서 서버의 Pawn 제어권을 바꾸는 구조로 만들면 안 됩니다.
일반적인 흐름은
Client
→ Pawn 변경 요청
↓
Server
↓
Possess
↓
Replication
↓
Client에 결과 반영
입니다.
이번 글은 싱글플레이 또는 서버 Authority에서 실행되는 기본 구조를 기준으로 합니다.
멀티플레이 Possess와 Replication은 이후 네트워크 파트에서 따로 다루겠습니다.
PlayerController가 유지되는 이유
PlayerController와 Pawn을 분리해두면 Pawn이 바뀌어도 플레이어 자체의 제어 주체는 유지할 수 있습니다.
예를 들어
PlayerController
↓
Character A
↓
Character A 사망
이후 새로운 Pawn을 준비해서
PlayerController
↓
Character B
로 다시 Possess할 수 있습니다.
즉
PlayerController
→ 플레이어의 제어 주체
Pawn
→ 현재 플레이어가 조종하는 게임 월드의 대상
으로 분리되어 있기 때문에 차량, 드론, 캐릭터 교체, 사망과 Respawn 같은 기능을 만들기 쉬워집니다.
실무에서 자주 사용하는 구조
Possess는 다음과 같은 기능에서 자주 사용합니다.
Character Respawn
Vehicle 탑승 / 하차
Drone 조종
Turret 조종
Spectator 전환
캐릭터 교체
여러 영웅 전환
사망 후 관전 Pawn 전환
기능은 달라도 핵심 구조는 같습니다.
PlayerController
↓
현재 Pawn
↓
UnPossess / Possess
↓
새로운 Pawn
정리
이번 글에서는 PlayerController에서 Pawn의 제어권을 변경하는 방법을 알아봤습니다.
현재 제어 중인 Pawn은
GetPawn();
으로 가져올 수 있습니다.
새로운 Pawn을 제어하려면
Possess(NewPawn);
을 사용합니다.
현재 Pawn과의 제어 관계를 해제하려면
UnPossess();
를 사용합니다.
중요한 점은
UnPossess
→ 제어권 해제
Destroy
→ Actor 제거
로 서로 다른 기능이라는 것입니다.
PlayerController에서 Pawn 변경 시점을 처리하려면
OnPossess()
OnUnPossess()
를 Override할 수 있습니다.
Pawn에서는 반대로
PossessedBy()
UnPossessed()
를 사용할 수 있습니다.
전체 흐름을 정리하면
PlayerController
↓
GetPawn
현재 Pawn 확인
↓
Possess(NewPawn)
↓
새로운 Pawn 제어
↓
OnPossess
↓
UnPossess
↓
현재 Pawn 제어 해제
↓
OnUnPossess
입니다.
그리고 Possess()는 Pawn을 생성하는 함수가 아닙니다.
Pawn 준비
↓
PlayerController
↓
Possess
↓
입력과 제어권 연결
순서로 이해하면 됩니다.
이 구조를 이해해두면 이후
사망 / Respawn
Vehicle
Drone
Spectator
멀티플레이 Pawn 전환
같은 기능을 구현할 때 Gameplay Framework의 구조를 훨씬 이해하기 쉬워집니다.
'[ Unreal Engine ] > - UE5 실습' 카테고리의 다른 글
| 언리얼엔진5 C++ GameInstance와 GameInstanceSubsystem으로 전역 데이터 관리하기 (0) | 2026.09.22 |
|---|---|
| 언리얼엔진5 C++ ActorComponent로 재사용 가능한 게임 기능 만들기 (0) | 2026.09.18 |
| 언리얼엔진5 C++ Widget Blueprint 버튼 찾고 클릭 후 2초 비활성화하기 (0) | 2026.09.17 |
| 언리얼엔진5 Static Mesh 분리하기 Modeling Mode Split 사용법 (0) | 2026.09.08 |
| 언리얼엔진5 Time Remaining 애니노드 알아보기 (0) | 2026.09.07 |
댓글