做Unreal Engine 4项目的时候,几乎每个教程都会提到一个概念:Pawn。但真正理解“Pawn”到底是个什么玩意儿、它和Actor、Character、Controller之间是什么关系,很多人一开始是懵的——包括我自己。这篇文章就专门把这个核心概念拆开揉碎,聊聊Pawn是什么、能做什么、怎么在蓝图和C++里快速实现一个可操控的Pawn。如果你是刚接触UE4的初学者,或者已经在做项目但总觉得输入控制部分的链路不太清晰,这篇文章应该能帮你把它彻底打通。
1. 先搞懂Pawn在UE4里到底是个什么角色
1.1 从Actor说起:Pawn到底多出了什么
在UE4的类继承体系里,最基础的“世界里的东西”是Actor。任何放在关卡里的物体、角色、灯光、触发器、摄像机,本质上都是Actor。Actor本身是一个很干净的容器:它有位置、旋转、缩放,可以挂在关卡中,可以响应生命周期事件(BeginPlay、Tick),可以被Spawn和Destroy。
Pawn则是在Actor基础上专门为“可以被控制”而设计的一层。它默认拥有:
- InputComponent:一套与输入系统对接的组件,用来接收玩家键盘、鼠标、手柄的输入信号。
- Possess机制:可以被一个Controller接管,接管之后Controller就能支配它。
- 更友好的移动钩子:比如AddMovementInput、GetActorForwardVector这些,让写控制逻辑时不需要从零搭输入和运动映射。
换句话说,Actor只是“存在”,而Pawn不仅“存在”,还被赋予了一个“可以被谁操控”的入口。这就好比一个舞台上的木偶(Actor)和一个由提线师控制的提线木偶(Pawn)——后者多出了一整套操控用的线(Controller和输入绑定)。
需要注意一个关键差异:Pawn本身并不负责“移动”。它提供的是接收输入和委托控制的框架,实际的位移由它内部的组件或自定义逻辑完成,比如CharacterMovementComponent、FloatingPawnMovement,或者你自己写的移动逻辑。这个点很多人会绕晕,后面我会专门展开。
1.2 Pawn、Character和Actor:三者该怎么选
UE4里还有个非常常用的类叫Character,它继承自Pawn。很多人会问:“我以后角色直接用Character不就行了?为什么还要单独学Pawn?”
Character本质上是为“人形角色”预装好了全套方案的Pawn。它自带:
- CapsuleComponent胶囊体碰撞
- CharacterMovementComponent角色运动组件(支持走路、跑步、跳跃、飞行等模式)
- 网格体骨骼动画支持(Mesh)
所以如果你要做的是第三人称/第一人称人形角色,直接新建Character最省事,省掉大量重复搭建。
但Pawn更适合这些场景:
- 载具、飞行器、无人机等非人形可操控对象
- 想要完全自定义运动模式的对象(比如一个按物理悬浮的球、一个靠自定义力移动的平台)
- 需要被AI控制的单位(AIController也可以Possess一个Pawn)
- 视觉上一个“可被上身的物体”,比如监控摄像头、炮台这类固定设备
我个人的建议:开局先用Pawn理解“输入接收→控制驱动→移动表现”这条链路,等这个链路清晰了,再切到Character会非常容易。反过来如果上来就用Character,很多逻辑会自动帮你做掉,反而容易对原理一知半解。
1.3 Pawn与Controller的配合:身体和大脑的关系
Pawn单独放着是动不了的,必须有一个“大脑”指挥它。UE4里这个“大脑”就是Controller。
Controller是一种特殊Actor,它不显示在世界中,没有网格体,也没有碰撞,它的任务就是作为控制者操作一个Pawn。Controller分为两种:
- PlayerController:代表真正的玩家,接收玩家的输入并发送给Pawn。
- AIController:代表AI逻辑,把行为树或黑板决策变成对Pawn的控制指令。
Pawn被Controller“接管”的过程叫Possess(占有),解除接管叫UnPossess。在蓝图里你可以随时执行Possess或UnPossess来切换控制目标。这个机制在“操控载具进去再出来”“死后切换观战目标”“附身到别的角色”这类玩法里就是核心基础设施。
用一句话概括:Controller是大脑,Pawn是身体,Possess就是大脑接管身体的那一刻。
2. 5分钟搭一个可操控Pawn:蓝图路径
2.1 创建Pawn蓝图并添加基础组件
在内容浏览器右键 → 蓝图类 → 选择Pawn作为父类,命名比如BP_MyPawn。打开蓝图后依次添加组件:
- RootComponent(自动生成的根组件):一般默认是SceneComponent,你也可以添加一个CapsuleComponent作为根,并设置胶囊体半径和半高。碰撞需要它。
- StaticMeshComponent或SkeletalMeshComponent:作为视觉身体。先用一个Cube或者Sphere模型占位,后面再换正式模型。
- SpringArmComponent:弹簧臂,用来挂摄像机,控制视角与Pawn之间的距离。
- CameraComponent:挂在SpringArm下,作为玩家的眼睛。
如果你做一个俯视角或第三人称demo,强烈建议把SpringArm的TargetArmLength设成300~500,bUsePawnControlRotation设为true,这样鼠标转动时视角会跟着Pawn抬头/转向。
需要注意:SpringArm和Camera组件挂到Pawn后,如果你希望视角完全跟随身体旋转,记得把SpringArm的Use Pawn Control Rotation勾上;如果你做的是“坦克那种炮塔独立转向”的玩法,就需要关掉这个选项,另外用Control Rotation控制炮塔组件。
2.2 配置项目输入映射:Axis与Action
要让Pawn接收输入,先得让引擎知道“W键给谁”。打开编辑 → 项目设置 → 输入(Input),展开Axis Mappings(轴映射)和Action Mappings(操作映射)。
轴映射适合连续量输入,比如W/S控制前后、鼠标控制视角;操作映射适合“一次性”或“按下/抬起”型输入,比如跳跃、开火。
我常用的基础配置:
| 映射名 | 类型 | 按键 | 描述 |
|---|---|---|---|
| MoveForward | Axis | W / S | 前后移动,正向W,负向S |
| MoveRight | Axis | A / D | 左右移动,正向D,负向A |
| Turn | Axis | Mouse X | 鼠标左右转向 |
| LookUp | Axis | Mouse Y | 鼠标上下视角 |
| Jump | Action | SpaceBar | 跳跃(如果之后切Character用) |
轴映射的Scale默认是1.0,S键需要设置Scale为-1.0。这个细节不要漏,否则S和W的输入方向就反了。
2.3 在蓝图里绑定输入与写移动逻辑
打开BP_MyPawn的Event Graph。选中Pawn,在Class Settings里找到InputComponent。实际上你不需要手动创建InputComponent,Pawn默认会生成。重点是重写SetupPlayerInputComponent这个事件——在蓝图中右键搜索“SetupPlayerInputComponent”可以拉出override节点。
我推荐在蓝图里这样写:
- 从SetupPlayerInputComponent接一根线,用BindAxis绑定MoveForward、MoveRight:
- MoveForward绑定到自定义事件MVP_MoveForward,传入AxisValue
- MoveRight绑定到自定义事件MVP_MoveRight,传入AxisValue
- 在MVP_MoveForward里:AddMovementInput(GetActorForwardVector(), AxisValue)
- 在MVP_MoveRight里:AddMovementInput(GetActorRightVector(), AxisValue)
- Turn绑定到AddControllerYawInput
- LookUp绑定到AddControllerPitchInput
关键点在于AddMovementInput只是发起移动请求,真正让身体位移的是Pawn身上的移动组件或你在Tick里写的逻辑。如果在蓝图里你看到Pawn根本没有MovementComponent,你需要添加一个FloatingPawnMovement或自定义Movement组件。FloatingPawnMovement适合简单测试:它会以恒定速度朝AddMovementInput的方向推。
如果你的移动不是基于自身的前后左右,而是基于摄像机方向(很多第三人称游戏会这样),你需要先把Control Rotation转成Forward Vector再用。具体做法:GetControlRotation → Rotator的Get Forward Vector,再交给AddMovementInput。这样按W永远是“离镜头更远的方向”,而不是“角色朝向”。
我自己做原型的习惯是:先不加移动组件,直接在Tick里用AddActorWorldOffset测试输入映射是否通了。输入通了再加FloatingPawnMovement,这样排查问题的范围会小很多。
3. C++视角深入:Pawn的输入绑定与移动原理
3.1 必要的头文件与类成员
蓝图能跑通后,我强烈建议你再用C++写一遍Pawn,因为C++能让你看到更多底层细节,踩坑也更少。创建一个继承自APawn的类MyPawn,关键成员通常包含:
// MyPawn.h #pragma once #include "CoreMinimal.h" #include "GameFramework/Pawn.h" #include "MyPawn.generated.h" class USpringArmComponent; class UCameraComponent; class UStaticMeshComponent; UCLASS() class MYPROJECT_API AMyPawn : public APawn { GENERATED_BODY() public: AMyPawn(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") USpringArmComponent* SpringArm; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UCameraComponent* Camera; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components") UStaticMeshComponent* Mesh; UFUNCTION() void MoveForward(float Value); UFUNCTION() void MoveRight(float Value); UFUNCTION() void Turn(float Value); };构造函数里创建组件并挂载。注意SpringArm要挂在RootComponent下,Camera挂在SpringArm下,Mesh也挂在RootComponent下。如果做俯视角Pawn,把SpringArm的TargetArmLength设为600,并开启RotationLag:
AMyPawn::AMyPawn() { PrimaryActorTick.bCanEverTick = true; RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root")); Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh")); Mesh->SetupAttachment(RootComponent); SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm")); SpringArm->SetupAttachment(RootComponent); SpringArm->TargetArmLength = 600.f; SpringArm->bUsePawnControlRotation = true; Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera")); Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName); }3.2 SetupPlayerInputComponent的两种绑定方式
SetupPlayerInputComponent是Pawn输入的核心回调。PlayerController会在成功Possess后调用它,把你的输入映射和具体回调函数绑在一起。
常用写法:
void AMyPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); PlayerInputComponent->BindAxis("MoveForward", this, &AMyPawn::MoveForward); PlayerInputComponent->BindAxis("MoveRight", this, &AMyPawn::MoveRight); PlayerInputComponent->BindAxis("Turn", this, &AMyPawn::Turn); PlayerInputComponent->BindAxis("LookUp", this, &AMyPawn::LookUp); }如果你有需要“按一下就触发”的逻辑,比如跳跃,用BindAction:
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyPawn::Jump);这里有几个容易踩的坑:
- 输入映射的名字必须和项目设置里Axis Mappings中的名字完全一致,大小写也要一样。引擎是按FName字符串查找的,一旦不一致,函数根本不会被调用。
- BindAxis会把轴值实时回调给你,默认每帧都会传一次。如果想做平滑或者死区处理,最好在回调里自己加逻辑。
- 蓝图和C++同时重写SetupPlayerInputComponent时要小心重复绑定,推荐只用其中一种。
3.3 移动的本质:AddMovementInput与移动组件
AddMovementInput是一个很有意思的设计。它并不直接移动Pawn,而是把“我希望往这个方向走”写入Pawn的ControlInputVector或AccumulatedInputVector,最后由移动组件消费。
如果你不挂任何移动组件,AddMovementInput等于白调。最常见的FloatingPawnMovement在Tick里读取输入向量,按MaxSpeed插值移动;CharacterMovementComponent则走更复杂的碰撞和地面逻辑。所以:
- 加了CharacterMovementComponent(Character自带),你才能用Character的移动模式。
- 加了FloatingPawnMovement,Pawn就能像“浮动”一样无视重力地朝输入方向漂移。
- 如果什么都不加,Pawn在Tick里就只能自己改ActorLocation或用物理组件控制。
我实际测试时最常用的调试方法是:在MoveForward回调里加上UE_LOG,打印输入的AxisValue。如果日志数值正常变化,但Pawn就是不移动,十有八九是移动组件缺失或MaxSpeed为0。这个排查逻辑比盲目改参数快得多。
void AMyPawn::MoveForward(float Value) { if (Controller != nullptr && Value != 0.f) { UE_LOG(LogTemp, Warning, TEXT("MoveForward Value: %f"), Value); AddMovementInput(GetActorForwardVector(), Value); } }4. 进阶玩法:从简单移动到复杂控制方式
4.1 基于摄像机方向移动 / 基于Actor方向移动
很多俯视角游戏要求W键始终让角色朝“屏幕上方”移动,这时候AddMovementInput的方向就不能用GetActorForwardVector,而应该用控制器的旋转方向。写法是:
FRotator ControlRot = Controller->GetControlRotation(); FVector ForwardDir = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::X); FVector RightDir = FRotationMatrix(ControlRot).GetUnitAxis(EAxis::Y); AddMovementInput(ForwardDir, Value);这是很多第三人称动作游戏的标准做法。你还需要决定:Pawn身体的朝向是否跟随控制方向。通常是在MoveRight/MoveForward的混合结果里用ControllerRotation设定Actor的朝向,或者用bUseControllerRotationYaw。
这个细节决定了手感:角色像“坦克”一样转向(身体朝向跟随移动方向)还是像“横版射击”一样保持侧对镜头(身体朝向固定)。两种都是Pawn在接受输入后结合自身朝向产生不同表现的典型案例。
我建议你分别试一下:把Pawn的bUseControllerRotationYaw打开、关掉各玩一会,感受移动方向的差异。很多游戏“上手不对劲”的感觉,就是这么一个小开关造成的。
4.2 载具、飞行器类Pawn的设计思路
如果你要做可操控的载具、飞机、无人机,通常分两类:
- 物理驱动型:使用VehicleMovementComponent或PhysicsConstraint等完成轮子、悬挂、推进力模拟。Pawn在这里的职责是“坐标原点”,输入会转成燃料、转向角、油门等参数。
- 逻辑驱动型:用FloatingPawnMovement或自写MovementComponent,输入控制速度向量。适合做爽快感为主、物理真实感不重要的游戏。
一个比较常见的坑:用VehicleMovementComponent时会发现Pawn的旋转被物理接管,手动SetActorRotation不起作用。此时不能再用Controller旋转控制Pawn,而是应该让输入去调节VehicleMovementComponent的Steer和Throttle。
如果你只是做原型,我非常推荐先用FloatingPawnMovement把飞行手感跑通,再加物理。飞行器的常用技巧:给Pawn额外挂一个阻尼参数,让AddMovementInput产生的速度变化不那么突兀,手感会柔和很多。
4.3 AI Pawn与Possess切换的实战用法
Pawn不只服务于玩家,AI也大量使用。AIController可以通过MoveToActor/导航网格驱动Pawn移动,也可以直接调用Pawn的MoveTo函数。在学习AI巡检、防御塔、敌人小队这些玩法时,基本套路就是:
- 在关卡里Spawn一个Pawn(比如敌人小车)
- 给它配一个AIController(在Pawn蓝图里设置AIControllerClass,或运行时SpawnDefaultController)
- 在AIController的BeginPlay里写巡逻逻辑,控制Pawn移动
如果要实现“玩家上车后AI下车、玩家下车后AI继续巡逻”这种切换,就是Possess/UnPossess的舞台:
- 玩家靠近载具Pawn时,调用Possess(载具),Pawn的Controller从AIController变成PlayerController。
- 下车时先Spawn或者保留原AI Controller,再调用UnPossess,然后让AIController重新Possess该Pawn。
注意点:Possess只在服务器端对Actor所有权敏感,单机测试没问题,多人联机时要注意RPC调用时机和ReplicatedController的同步。新手最容易遇到的问题就是“本地玩家能控制载具,但别人看它原地不动”——这通常在网络复制里没把Movement复制打开,或者Controller没有正确复制。
4.4 在关卡中Spawn Pawn:所有权与Auto Possess
默认情况下,如果你想在关卡开始时就让某个Pawn归玩家控制,可以在Pawn细节面板里把Auto Possess Player设为Player 0,或者用GameMode的Default Pawn Class指定。GameMode会自动在玩家出生时Spawn一个默认Pawn并Possess它。
手动Spawn时要注意传参:
FActorSpawnParameters SpawnParams; SpawnParams.Owner = this; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn; AMyPawn* NewPawn = GetWorld()->SpawnActor<AMyPawn>(MyPawnClass, Location, Rotation, SpawnParams); GetController()->Possess(NewPawn);如果Spawn出来的Pawn没有被Possess,检查两点:一是这个Pawn是否可被Possess(默认是),二是当前是否已经有别的Pawn被这个Controller持有。一个PlayerController同一时间只能Possess一个Pawn,要先UnPossess旧的。
5. 实战排坑:我踩过的Pawn相关坑,你直接绕过
5.1 接收不到输入:90%是这两点
我见过太多人在论坛问“明明绑定了输入,Pawn却不动”。排查顺序永远是:
- 确认Pawn被PlayerController Possess。最简单的方法:在Pawn的Event BeginPlay里打断点,看GetController指向的是不是PlayerController。如果Controller是空的,说明没有Auto Possess,也没有手动Possess。
- 确认输入映射名字和绑定代码完全一致。我曾因为把MoveForward写成了MoveForward(多了一个空格)排查了半小时。
另外还有一个隐藏点:如果你在Pawn里加了InputComponent但在构造函数里没有设置PrimaryActorTick.bCanEverTick,理论上不影响输入,但如果使用的移动组件依赖Tick,Pawn不动其实是Tick没跑的锅。
5.2 视角不跟随Pawn / 视角乱转
SpringArm的Use Pawn Control Rotation和Pawn自己的bUseControllerRotationYaw是两个独立开关。很多人只开了其中一个,表现就是“角色位移了但摄像机还是固定”或者“鼠标动一下视角飞到天上”。
我常用的组合是:
- 第三人称:SpringArm Use Pawn Control Rotation = true;Pawn bUseControllerRotationYaw = true(或者用Character的ControllerRotation Yaw,看手感)
- 俯视角RTS式:SpringArm Use Pawn Control Rotation = false;Pawn bUseControllerRotationYaw = false;Camera旋转靠固定位置和旋转,或者靠鼠标中键拖拽改变RotationLag。
如果视角出现“上下颠倒”或“X轴翻转”,通常不是Pawn的问题,而是Mouse Y轴的Scale或者SpringArm的InheritPitch设置问题。
5.3 碰撞穿了或卡住:碰撞预设不是随便选的
Pawn作为可操控对象,碰撞交互尤其重要。最常见问题:
- 角色穿过地面:检查Mesh或Capsule的Collision Preset是否设为Pawn/Character,而不是WorldStatic或BlockAll。
- 被自己的碰撞弹飞:静态网格体挂到Root后,如果Preset是BlockAll,就会和地面重叠产生巨大冲击力。正确做法是让Mesh设为Pawn,或者只保留Capsule作为碰撞体,Mesh设为NoCollision/OverlapAll。
- 在自定义Pawn中如果根组件不是Capsule,而是直接放StaticMesh,碰撞逻辑就容易混乱。我建议要么用Capsule做根,Mesh设为仅视觉;要么用盒体/球体碰撞体做根。
小技巧:调试碰撞时打开显示 → 碰撞可视化,或者临时用DrawDebugCapsule查看胶囊体实际碰撞位置,能省很多时间。
5.4 蓝图里的AddActorLocation和AddMovementInput打架
新手常犯的错误是“在Tick里用AddActorLocation直接移动Pawn,同时又用了AddMovementInput”。两种移动方式会同时生效,结果就是角色像抽风一样抖动。
规则很简单:
- 如果你有移动组件,就让它统一处理移动,外部不要直接改ActorLocation。
- 如果你要走纯逻辑移动(例如做幽灵穿墙效果),那就不挂移动组件,直接在Tick里根据输入向量计算新的ActorLocation。
- 这两种方式混用通常导致叠加或抖动。
如果你需要“自动转向+手动移动”结合,可以在Tick里先计算方向,再用SetActorRotation或AddActorWorldRotation,但不要在AddMovementInput之外额外调用AddActorLocation。
6. 我的一点总结与扩展建议
做了不少UE4项目后,我的个人体会是:Pawn这个类的设计哲学非常清晰——它把“世界里一个可被操控的东西”这个需求抽象成了一个标准框架。你用蓝图或C++写的Pawn,本质上就是在回答三个问题:我提供什么组件来表现这个对象?我如何接收玩家的输入?我如何处理输入对应的运动表现?
建议你从今天开始,自己动手做一个最小Pawn:一个球体、一根弹簧臂、一个摄像机、两个Axis绑定。然后一步步加功能:改成基于摄像机的移动、加一个跳跃、加一个开火、换成AI控制、再试试多人复制。这个流程走完,你对UE4整个“玩家控制链条”的认知会完整很多。
最后再分享一个小技巧:Pawn的Tick函数里不要做太重的逻辑。移动组件每帧都在跑,你如果又在Tick里查询一大堆东西,性能会很差。把“判断是否在地面”“计算朝向”这类高频逻辑尽量交给MovementComponent或者只在状态切换时计算,能明显减少掉帧。这是我在一个载具demo里测出来的教训——Pawn上的Tick加上大量蓝图节点之后,Draw Call没高多少,CPU时间却涨得离谱。
如果你后续要把Pawn扩展到网络同步,记住开启bReplicates和ReplicatedMovement,并确保Possess在服务器执行。熟练了Pawn,等于打好了UE4角色系统最扎实的地基。