Pawn 与 Controller 与 PlayerState

自动关联目录:Pawn 与 Controller 与 PlayerState

这三个类解决的是同一个问题:把"玩家"这个持久概念,和"角色"这个可被摧毁的载体分开。

一句话定位:PlayerController 是意图,Pawn 是能力,PlayerState 是身份。玩家死了换尸体再重生,Controller 和 PlayerState 都不变,只有 Pawn 换了一个。

三者分工

类回答什么生命周期存在于哪些端
APlayerController「我想做什么」整个连接服务端 + 仅拥有者客户端
APawn / ACharacter「我做得到吗」一次生命服务端 + 所有客户端
APlayerState「我是谁」整个连接(跨关卡)服务端 + 所有客户端

关键不对称:PlayerController 在别的客户端上不存在(你拿不到别人的 Controller),但别人的 PlayerState 存在。所以"显示所有玩家的分数/名字"必须用 PlayerState,不能用 Controller。

Pawn

类用途
APawn可被 Controller 控制的 Actor 基类
ACharacter带 CharacterMovementComponent 的行走角色
ADefaultPawn / ASpectatorPawn默认与观战

Pawn 上放的是能力:移动组件、技能组件、血量、碰撞、动画。它随时可能被销毁重生。

// Pawn 上不要存"这个玩家的账号信息"——重生就没了
UPROPERTY(Replicated)
int32 TeamId;      // 这是错的,应放 PlayerState

Controller

类用途
APlayerController真人玩家:输入、HUD、相机管理
AAIControllerAI:行为树、黑板
AController基类,Possess 能力在这
// 接管与释放
Controller->Possess(NewPawn);
Controller->UnPossess();

// Pawn 侧回调
virtual void OnPossessedBy(AController* NewController) override;   // 服务端
virtual void OnRep_Controller() override;                          // 客户端

Possess 之后不要立刻访问 Pawn 的成员,要等 OnPossessedBy / OnRep_Pawn 回调——这是最经典的"偶发空指针"。

PlayerController 上放什么

该放不该放
输入绑定与输入映射角色能力逻辑(放 Pawn/Ability)
HUD / UI 的创建与管理会被复制的状态(Controller 不在其它端)
相机管理玩家的持久化数据(放 PlayerState)
本地预测的输入缓存全局游戏状态(放 GameState)
void AMyPC::SetupInputComponent()
{
    Super::SetupInputComponent();
    UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(InputComponent);
    EIC->BindAction(FireAction, ETriggerEvent::Triggered, this, &AMyPC::OnFire);
}

PlayerState

PlayerState 的存在理由是"跨 Pawn、跨关卡存活"。

数据放哪
分数、击杀数、队伍、昵称、PingPlayerState
当前血量、当前武器Pawn(或 GAS 的 AttributeSet)
全局比分、剩余时间GameState
规则(胜负条件、重生时间)GameMode
UPROPERTY(ReplicatedUsing = OnRep_Score)
int32 Score;

void AMyPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutProps) const
{
    Super::GetLifetimeReplicatedProps(OutProps);
    DOREPLIFETIME(AMyPlayerState, Score);   // 见 [属性复制条件与 RepNotify](/b3e69e6c.html)
}

PlayerState 在无缝旅行(Seamless Travel)中会保留,这是它能承载"跨关卡数据"的原因(见 关卡与 World 流转)。

一次重生的完整时序

Pawn 死亡
  → Pawn::Destroy()
  → GameMode 定时器 / 玩家请求重生
  → GameMode::RestartPlayer(Controller)
  → ChoosePlayerStart → SpawnDefaultPawnFor → SpawnActor(Pawn)
  → Controller::Possess(NewPawn)
  → Pawn::OnPossessedBy(服务端)/ OnRep_Pawn(客户端)
  → Pawn::BeginPlay
  → PlayerState 里的分数、队伍保持不变

"重生后数据丢了"几乎总是因为数据放错了类——放 Pawn 上的必然丢,放 PlayerState 上的不会。

取用的正确姿势

// 在 Pawn 上
AController* C = GetController();
AMyPlayerState* PS = GetPlayerState<AMyPlayerState>();   // 模板版本,安全

// 在 Controller 上
APawn* P = GetPawn();
AMyPlayerState* PS = GetPlayerState<AMyPlayerState>();

// 从任意 Actor 找本地玩家
APawn* LocalPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
AMyPC* PC = Cast<AMyPC>(UGameplayStatics::GetPlayerController(GetWorld(), 0));

不要用 index 0 硬编码玩家——多本地玩家(分屏)和测试环境都会出问题。更稳的是从 ULocalPlayer 或 UGameInstance 拿。

与 AI 的对称性

AI 用的是 AAIController + 行为树(见 BehaviorTree)。设计上的好处是:

// 同一套能力代码,人控和 AI 都能用
Controller->GetPawn()->FindComponentByClass<UMyAbilityComp>()->TryActivate();

规则:需要"谁都能触发"的逻辑写在对 Pawn 生效的组件上,不要写在 APlayerController 里——否则 AI 用不了。

常见坑

坑说明
在别的客户端上取 PlayerController不存在,返回 null
玩家数据放 Pawn重生即丢
Possess 后立即用 Pawn 成员要等 OnPossessedBy / OnRep_Pawn
HUD 逻辑放 Pawn远程 Pawn 也会跑
用 GetPlayerController(0) 硬编码分屏与测试环境失效
在 Controller 上放复制属性其它端看不到这个 Controller
在 BeginPlay 里取 PlayerState可能还没复制过来,用 OnRep 或延迟
把胜负判定写进 Pawn应该由 GameMode 判定