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; // 这是错的,应放 PlayerStateController
| 类 | 用途 |
|---|---|
APlayerController | 真人玩家:输入、HUD、相机管理 |
AAIController | AI:行为树、黑板 |
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、跨关卡存活"。
| 数据 | 放哪 |
|---|---|
| 分数、击杀数、队伍、昵称、Ping | PlayerState |
| 当前血量、当前武器 | 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 判定 |