作为常年跟UE4打交道的人,我经常被问到一个基础但很多人没真正吃透的概念:Pawn。标题里这个词算得上是UE4的看家概念之一,翻译过来叫“可操控化身”,听起来很玄乎,但说白了,它就是玩家或者AI在游戏世界里的“身体”、可以控制的那个实体。无论是做一个第一人称射击、第三人称冒险,还是一个简单的载具Demo,Pawn几乎是你绕不开的第一步。这篇文章不准备绕着概念打转,直接拆开讲清楚:Pawn到底是什么、它和Actor、Character有什么血缘关系、怎么从零写一个能被玩家操控的Pawn,以及我在实际项目里踩过哪些和它相关的坑。适合刚接触UE4的初学者,也适合那些蓝图用得很熟、但想搞明白底层逻辑的朋友。
1. Pawn是什么:先搞清它在UE4里的定位
很多人第一次看到“Pawn”是在UE4的C++类向导里,旁边往往还跟着Character、Actor、GameMode这些选项。当时我也是一脸懵,心想不就是做个角色吗,怎么还分出这么多说法。等真正做过几个项目后才明白,这些类不是随便分的,每一层都有它的设计意图。Pawn的核心定位是:能接收输入、能移动、能被人或AI控制的游戏实体。你可以把它理解成“一具可以被灵魂进驻的躯壳”,灵魂是谁不重要,可以是本地玩家、远程客户端玩家,也可以是AI控制器,但躯壳本身只知道怎么动、怎么响应指令,并不知道“我是谁”。
1.1 从Actor到Pawn:继承关系与代码视角
UE4里最基础的场景实体是Actor,所有能在关卡里摆出来的东西都从它派生。Pawn呢,它是Actor的子类,所以它天然拥有Actor的一切能力:Transform、组件、Tick、事件、网络复制等。但Pawn额外加了一套非常关键的能力,一是“被控制”,二是“输入处理”。看代码最直观。你在C++里建立一个Pawn,继承的父类是APawn,而APawn再往上又是AActor。APawn里面有几个特别重要的成员,比如Controller指针,它指向当前控制这个Pawn的AController;还有PlayerState,用来在网络环境下同步玩家状态;以及一个默认的移动组件。
这里有个容易搞混的点:Pawn本身不等同于“角色模型”,它更像一个容器。你可以把StaticMesh或SkeletalMesh挂上去当身体,也可以挂一个弹簧臂和相机,还可以通过AddMovementInput接收移动指令。换句话说,Pawn是“逻辑+表现+物理”的组合体,但它的灵魂来自Controller。这一点在多人联机中体现得非常明显:同一个Pawn,在这个客户端上被本地PlayerController控制,在其他客户端上则可能是模拟状态,Pawn自己并不知道它是“自己”还是“别人”。
1.2 Pawn和Character,到底该怎么选
这是新手最常见的选择困难。Character这个类,在UE4里其实是Pawn的一个特殊派生,它自带一个CharacterMovementComponent,也就是专门处理角色移动的组件,能实现走路、跑步、跳跃、飞行、攀爬、下蹲等能力,还带碰撞胶囊体。Pawn就没有这些默认能力,如果你用APawn做一个人形角色,需要自己挂移动组件(比如FloatingPawnMovement、UPawnMovementComponent),自己管碰撞,自己处理转向。那到底该选哪个?我的建议是:凡是人形、要走路跳跃带重力感觉的,直接用Character;凡是载具、炮塔、触手怪、飞行器、AI哨兵这类不按人形逻辑动的,用Pawn更自由。
举个实际例子,我做过一个无人机巡逻类的小Demo,如果当时用Character去改,会被它的胶囊体和重力逻辑束缚得很别扭;改成Pawn之后,只需要一个简单移动组件加一个球体碰撞,就能自由地在三维空间里转,重力、地面吸附这些全都自己控制。反过来,做一个第三人称开头游戏,用Character就快得多,它的移动组件已经处理好了绝大多数地面物理逻辑,比如斜坡、走路速度衰减、AirControl等,你自己去复刻这一套工程量不小。所以记住一句话:Character是“人体向”的Pawn,Pawn是“万物皆可为”的躯壳。
1.3 DefaultPawn与AI Pawn:引擎里现成的形态
新创建一个第三人称模板工程,你会在World Settings里看到GameMode设置了DefaultPawn类,指的是Character或Pawn类,专门作为出生时玩家获得的那个身体。UE4还自带了一个DefaultPawn,它是一个最简单的Pawn,带一个移动组件和一个悬浮球体碰撞,没有动画、没有网格体,往往只用来临时测试。建空白工程时,你按WASD可以移动,能看到准星旋转视野,实际上控制的就是这个DefaultPawn。我第一次看默认工程时也很好奇那个球是什么东西,抛到场景里按W就能动,后来才明白这就是官方用来快速验证输入和游戏模式的最小Pawn。另外有一个类叫AIController,它可以“Possess”(占据)一个Pawn,让AI接管这个躯壳,逻辑和玩家控制完全一致,只是输入来源从键盘变成了AI决策。参考AI交互逻辑时,APawn往往和AIController配套使用,这样可以复用同一套移动和物理逻辑,区别只是谁给这个躯壳下达指令。
2. 从零搭建一个自定义Pawn:组件选型与碰撞处理
光说不练假把式。下面我带你把一个自定义Pawn从创建到接入游戏完整跑通。这里以C++为例,因为看得最清楚,蓝图和C++在关键API上是对应的,只是C++能让你看到底层是怎么绑定的。准备好一个UE4项目,版本建议4.26以上,我用4.27验证过,5.x流程也差不多,不过某些构建工具链配置有区别,注意适配。
2.1 C++类搭建:继承APawn而不是AActor
在编辑器里点击Tools -> New C++ Class,父类选择Pawn,起个名字比如MyPawn。生成出来的头文件里,类声明是继承APawn的,构造函数和Tick都保留着。创建完之后,第一件事就是到构造函数里去搭建它的基本结构。一般做法是:建一个SceneComponent当根组件,因为Pawn不像Character那样默认带胶囊体,你必须自己给一个可移动的锚点。
// MyPawn.h UCLASS() class MYGAME_API AMyPawn : public APawn { GENERATED_BODY() public: AMyPawn(); protected: virtual void BeginPlay() override; virtual void Tick(float DeltaTime) override; virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") USceneComponent* RootScene; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") UStaticMeshComponent* MeshComp; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") USpringArmComponent* SpringArm; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components") UCameraComponent* Camera; void MoveForward(float Value); void MoveRight(float Value); void Turn(float Value); void LookUp(float Value); };构造函数里要创建组件并设置附件关系。要注意,Pawn的根组件如果不指定,引擎会报“RootComponent is NULL”相关的警告,而且可能导致Later Possess时位置异常。常规写法是:
AMyPawn::AMyPawn() { PrimaryActorTick.bCanEverTick = true; RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene")); RootComponent = RootScene; MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp")); MeshComp->SetupAttachment(RootScene); SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm")); SpringArm->SetupAttachment(RootScene); SpringArm->TargetArmLength = 500.0f; SpringArm->bUsePawnControlRotation = true; Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera")); Camera->SetupAttachment(SpringArm, USpringArmComponent::SocketName); }请注意bUsePawnControlRotation,如果你控制摄像机跟随鼠标视野,这个选项建议设为true,否则旋转变换会错位。这个参数是我在初学阶段最容易踩的坑,后面单独讲。
2.2 根组件、碰撞体与移动组件怎么配
有了根组件和视觉网格,下一步是让Pawn能受力、能移动。最简单的方式是挂一个UPawnMovementComponent或UFloatingPawnMovement。UFloatingPawnMovement给你提供加速度、最大速度、转向速度,适合飞行器、悬浮物这类不依赖重力的东西。如果是载具或者人形,则根据自己的需要自定义。
// 在头文件里加 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement") UFloatingPawnMovement* FloatingMovement; // 构造函数里 FloatingMovement = CreateDefaultSubobject<UFloatingPawnMovement>(TEXT("FloatingMovement")); FloatingMovement->MaxSpeed = 1200.f; FloatingMovement->Acceleration = 4000.f; FloatingMovement->Deceleration = 4000.f; FloatingMovement->TurningBoost = 8.f;移动组件虽然叫“Movement”,但它的本质仍然是改变Pawn的Location。Pawn没有重力,所以它不会自己下落,也不会受地形影响,除非你额外加物理约束。如果需要让Pawn被子弹击飞、被爆炸冲撞,就要考虑给网格体或者胶囊体加物理模拟。在用StaticMesh做载具类Pawn时,我推荐把静态网格体的碰撞设置为Query and Physics,并给它添加一个物理约束(Physics Constraint)或者直接在网格体上启用“Simulate Physics”,这样可以模拟出车身重量。
碰撞上有个原则:Pawn整体的碰撞响应不要过于死板。比如当一个Pawn被Controllers控制时,通常不希望它和世界里的静态物体产生过于强烈的物理碰撞,否则会在走廊里卡来卡去。此时可以借助碰撞预设:让胶囊体或网格体的Object Type设为Pawn,对WorldStatic、WorldDynamic都设为Block,但要给Camera组件设为Ignore,防止相机穿墙后把Pawn挡住。在做第三人称相机时,相机组件默认会对世界碰撞进行检测,如果SpringArm长度太长,它会被墙截断;这个机制叫Camera Collision,记得在SpringArm上勾选Probe Size,给它一个合适的探测半径,否则相机贴着墙面会有穿墙抖动。
2.3 让镜头跟随:弹簧臂与相机组件
关于第三人称镜头,这里有一个我常用的组合方案:SpringArm+Rotation+CameraOffset。SpringArm的作用不只是把相机拉远,它还处理了碰撞收缩和视角平滑。比如在狭窄走廊里,SpringArm检测到碰撞后会自动缩短TargetArmLength,这样相机不会钻到墙里面去。CameraOffset可以调整相机相对瞄准方向的上移,这对射击类游戏特别重要,因为准星通常在屏幕中心,相机如果和眼睛齐平,瞄准感觉会很奇怪。
如果你做的是第一人称Pawn,那就不需要SpringArm了,直接把Camera挂在Root或其他根组件上,位置设到眼睛高度。经典搭配是CameraPivot或Root组件放在Pawn的中心,Camera放在前上方。但要注意,第一人称Pawn通常不会让玩家看到自己的模型,否则会有穿模的烦恼。我做第一人称项目时,习惯把可见的Mesh隐藏,但保留一个碰撞体,这样玩家看不到自己的“身体”,第三人称的观众或观察者视角还能看到一个半透明的示意外形。
3. 输入与操控:让Pawn真正“可操控”
Pawn变成了“躯壳”,接下来就需要“灵魂”来控制它。这个灵魂在UE4里是Controller,更准确地说是PlayerController。PlayerController负责接收玩家的输入事件,然后把这些输入映射到Pawn身上。整个过程分为两段:Possess(操控权绑定)和InputComponent绑定。如果你不做这两步,Pawn就只是一个漂在世界里的模型,按键再猛也没反应。
3.1 PlayerController与Possess机制
Possess这个词,直译过来是“占据、附身”,意思很形象。当GameMode生成一个Pawn之后,会自动找到对应的PlayerController,并调用Possess(),把这个Pawn交给Controller控制。Controller通过SetPawn、PossessedPawn等指针知道当前控制的Pawn是谁。反过来,Pawn通过Controller成员知道自己被谁占据。这个关系是双向的。
在实际代码里,你想临时切换操控,比如从角色死亡到重新出生,可以调用PlayerController->Possess(NewPawn);想让Controller脱离当前Pawn,可以调用UnPossess(CurrentPawn)。这里有个细节:UnPossess之后,Controller通常还会带着Camera,如果你的Pawn是带摄像机的,要小心Camera所有者变化后,视图是否自动切换。在PlayerController的SetViewTargetWithBlend可以把观察目标切到另一个Actor,且支持混合过渡,这是死亡回放或观战系统的基础。
有一种常见的错误操作:在Pawn的BeginPlay中,直接写GetController()分成Cast<APlayerController>(GetController()),然后调用SetControlRotation或SetViewTarget,结果发现不生效。原因往往是Possess发生在BeginPlay之后,或者Controller还没有设置完成。正确的方式是重写PossessedBy()回调,在该函数里处理控制相关的初始化,或者依赖PlayerController->PawnPendingDestroy回调做延迟。
3.2 绑定输入的方法:InputComponent与增强输入
UE4经典的输入绑定流程是重写SetupPlayerInputComponent,在这个函数里通过PlayerInputComponent->BindAxis和BindAction把输入映射到Pawn的函数。很多人以为输入是Pawn自己绑的,其实是在Pawn被Controller控制后,Controller会为这个Pawn生成一个InputComponent,然后调用这个Pawn的SetupPlayerInputComponent,让它自己注册自己关心的输入。这块理解清楚,调试“按键无效”时就能少走弯路。
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); PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AMyPawn::StartJump); }输入轴线在Project Settings -> Input里面映射,比如"MoveForward"绑定W/S,"Turn"绑定鼠标X。注意,Axis绑定是每帧持续触发的,所以MoveForward(float Value)里Value是-1到1之间的浮点数。这个Value可以直接传给AddMovementInput。
如果你用的是UE4.26以上版本,官方开始推“增强输入”系统,它把输入处理变成了Input Action资产的方式,不再是一对一的轴线映射。好处是应对复杂组合键和按键优先级更自由,坏处是学习成本高,而且和相关旧资产兼容性要处理。我的建议是,老项目用旧系统没问题,但是新项目建议直接上增强输入,因为后续5.x版本默认走这条路,早晚要迁移。
3.3 前向向量与转向逻辑:让移动方向跟着相机走
这是Pawn操控里最具迷惑性的一个点。你在Pawn里写AddMovementInput(GetActorForwardVector(), Value),会发现角色确实是朝自己正前方移动,但如果你旋转了相机,角色并不会跟着转。为什么会这样?因为GetActorForwardVector()拿的是Pawn自身的方向,不是控制器/相机的方向。典型的表现是:鼠标转视角,按W,人物却往一个固定方向跑。
最稳的写法是用GetControlRotation(),把Pawn的朝向与Controller分开处理。我们有多种方案:一是直接把Pawn的Rotation设为Controller的Yaw,让Pawn的朝向跟着相机Yaw同步;二是只在输入层面用Control Rotation决定移动方向,Pawn的物理朝向另算。我的做法一般是:
void AMyPawn::MoveForward(float Value) { if (Controller != nullptr && Value != 0.0f) { FRotator ControlRot = Controller->GetControlRotation(); FRotator YawRot(0, ControlRot.Yaw, 0); FVector Forward = FRotationMatrix(YawRot).GetUnitAxis(EAxis::X); AddMovementInput(Forward, Value); } }这里要留意,Controller可以是AIController,也可以不是PlayerController。AIController也有GetControlRotation,只是通常和Pawn的朝向一致。这样写的好处是,AI控制的Pawn也能复用同一套移动逻辑,因为AI同样会设置它的ControlRotation。
Pawn自身转向也有讲究。如果你不做“根骨骼旋转”或“面向移动方向”处理,第三人称模型可能会出现角色平移但面朝前一动不动,动作诡异。解法有几种:APawn::FaceRotation或者SetActorRotation再加上TimeWarp。如果模型是SkeletalMesh,则要处理动画根骨骼与碰撞胶囊体间的方向同步,基本思路是让模型旋转和胶囊体旋转的目标一致。更细的转向插值可以用RInterpTo,让旋转平滑过渡,避免瞬间“扭脖子”。
4. 多Pawn管理与特殊能力扩展
你做游戏不可能只有一种Pawn。角色死后换新身体、骑乘载具、操控炮塔,都属于Pawn的切换。理解Possess和UnPossess机制后,这些功能就是排列组合。
4.1 在Pawn之间切换操控权
切换Pawn有两种场景。第一种是主动切换,比如按F进入载具,像《战地》或《GTA》那样。一种是死亡切换,玩家死后重新出生。死亡切换比较简单,GameMode负责延迟生成新Pawn然后Possess给Controller。主动切换的难点在于,怎样处理好“旧Pawn”的状态。如果你直接Possess新Pawn,旧Pawn会失去控制权,但它还在世界里,可能会原地站着成为一具“僵尸”。常见做法是将旧Pawn隐藏、禁用碰撞、停止Tick,或者直接销毁。如果载具和角色都使用相同Input映射,新Possess后输入默认会重新绑定,这个不用太担心。
这里有一个经验:切换Pawn时,最好把Pawn所属的PlayerState保持一致,尤其是多人对战里。比如击杀后重生,PlayerState要保证维持,否则评分和UI会乱套。游戏中涉及分数时,一般会把归Pawn所有的PlayerState转移到新的Pawn,而不是寄存在旧Pawn身上。代码上可以先得到旧Pawn的PlayerState,再显式分给新Pawn。
4.2 Pawn与交互组件:拾取、载具、炮塔等
一些特殊Pawn形态并不需要复杂动画。举个例子,做一个炮塔Pawn,它只有两个旋转轴:炮管俯仰和炮台旋转。这种Pawn不移动,只控制旋转和开火。你仍然可以用APawn做,但不需要挂FloatingMovement,只需要根组件加两个旋转于组件的子层级。输入绑定上,直接绑定Turn和LookUp,在函数里用AddControllerYawInput或AddControllerPitchInput来修改ControlRotation,然后用SetActorRotation配合插值去旋转炮管。这样一句话总结:炮塔是“不能移动但可以旋转”的Pawn。
载具稍微麻烦一点。常见做法是把整车做成一个Pawn,车身静态网格体加物理约束,底盘挂一个UVehicleMovementComponent或者自定义四轮物理组件。它的移动逻辑不走FloatingPawnMovement,通常用物理里的悬架力,所以输入映射依然是调用SetThrottleInput、SetSteeringInput而不是AddMovementInput。这种不同移动方式的Pawn可以并存,切换时Controller的Possess会自动接管一切。所以在设计Pawn的输入接口时,尽量把“输入值”和“移动方式”分开,才能让角色、载具、炮塔共用一套Input Mapping Context或旧Axis映射而互不干扰。
4.3 网络环境下的Pawn注意点
多人联网是Pawn最容易翻车的地方。Pawn的移动信息需要在客户端与服务器之间同步,服务器负责权威判定,客户端负责预测和表现。APawn默认复制启用吗?实际上bReplicates需要手动设置,或者你在构造函数里设bReplicates = true。移动复制上,Pawn的Transform默认不会自动同步给所有客户端,必须依赖移动组件或手动复制。很多初学者在联机时发现角色只在自己屏幕上动,其他人看到的是个残影,检查方向很多时候就是Pawn没设置bReplicates和移动复制模式。
输入这块更微妙:客户端Pawn在本地响应输入,并通过ServerMove等RPC把期望移动发给服务器。如果你自己做自定义移动组件,记得要重写移动预测和纠偏逻辑。许多人觉得Character默认Run能跑通多人就是因为引擎已经封装好了全套移动复制,而自定义Pawn则要把这个链路手工搭起来。写到这里我必须强调一句:如果只是单机项目,那可以无视网络复制,但如果是CO-OP或者对战,尽早设计好Pawn的ReplicatedMovement、Component复制和RPC入口,能省掉后面80%的抓狂时间。
网络环境下还有RemoteRole与LocalRole。Pawn在本地客户端上是AutonomousProxy,在服务器上是Authority,在其他客户端上是SimulatedProxy。这三个角色的行为差异,直接导致“玩家自己操纵顺滑,但别人看着瞬移”的问题。针对平滑效果,UE4为SkeletalMesh提供了NetworkSmoothingMode,开启线性插值平滑(LinearInterp),可以减少远程Pawn的位置抖动。这些设置不是默认最优的,需要按项目调制参数。
5. 常见问题与排查技巧
做Pawn的过程中,我遇到过很多让人头大的问题。这些问题在论坛上反复出现,但回答往往比较零散。我把最典型的几类整理一下,并给出我的排查思路。这部分是实操的精华,建议收藏。
5.1 Pawn不动/方向错乱的常见原因
表现之一是按WASD没反应。最常见的原因是:你的Pawn没有被任何Controller Possess,或者没有调用SetupPlayerInputComponent。排查顺序是先确认GameMode里DefaultPawn类有没有改成你的Pawn,确认Pawn运行时Controller指针是否非空。其次确认输入轴映射在项目设置里有没有配置对应名字,很多人在C++里写BindAxis("MoveForward"),但项目设置里却写成了"MoveForward"大小写不一致,或者配置成了Action,那就会静默失败。
方向错乱则多数与ControlRotation和ActorRotation混用有关。建议在Pawn内部封装函数,明确标明这段输入移动是依据控制器Yaw,还是Actor自身方向。不要指望GetActorRotation永远等于相机方向。调试时可以在屏幕打印Controller的GetControlRotation()和Pawn的GetActorRotation(),一眼看出差值。
5.2 Possess失败与PlayerController为空
还有一种坑是Pawn出生了,但Controller指针为空。原因可能是GameMode没有正确生成PlayerController,或者生成时机太早。像在Sub-Level加载或者切换流关卡时,PlayerController的Possess流程可能被多次调用,同一个Pawn被反复Possess也会造成混乱。如果Pawn在瞬移或动态生成后需要被某个Controller接管,务必确保该Controller存在且没有被其他Pawn占用。有些情况下,还需要手动设置PrimaryActorTick或延迟几帧再Possess,这是引擎生成Actor的先后顺序导致的,属于生命周期问题。
5.3 碰撞与物理上的一些坑
碰撞方面最容易出现的坑是:Pawn和角色使用的胶囊体互相碰撞,导致推挤。多人对战中,队友之间互相推搡很烦,一般把Pawn之间的碰撞响应设为Overlap或Ignore。物理上,如果静态网格体开了Simulate Physics,可能会和移动组件打架,比如FloatingMovement控制位置的同时,物理引擎也在模拟,表现就是Pawn颤抖或乱飞。解决办法是让移动只控制Root或一个非物理子组件,把视觉车身留在物理独立层,或者关闭物理模拟仅保留碰撞检测。我建议自定义Pawn在没有充分物理需求时,保持Mesh的Simulate Physics关闭,避免很多不可控的抖动。
6. 实际操作中的几点心得
聊得差不多,我把这几年和Pawn打交道的个人体会说一下。第一,Pawn设计一定要把“输入”和“行为”解耦,我一开始写Pawn时总喜欢把所有按键处理都堆在Pawn里,后来发现只要换成载具、炮塔、AI,代码就乱成一团。现在我的习惯是:Pawn只提供MoveForward、Turn、Jump这样的输入函数,具体怎么移动、旋转、加速度由Pawn内部实现决定;Controller决定哪些Pawn拥有操控权,这样切换和复用都很舒服。
第二,调试Pawn问题时,先确认“谁在控制它”和“它有没有收到输入”。只要设置了GetController()的调试打印、SetupPlayerInputComponent里的日志,就能把大半问题锁定到模块,而不是无止境地试参数。许多诡异的BUG,最后发现只是DefaultPawnClass没配置好,或关卡里放置的是老版本Pawn。
第三,如果你准备长期做UE项目,我强烈建议把旧输入系统和新增强输入都摸熟。Pawn将来必然要适配不同输入方案,一套蓝图或C++里能同时支持两种输入,在换工程或接多人合作时优势非常大。最后提醒一点,你在网上复制一个Pawn的代码示例时,大概率会踩到版本差异的坑,比如某些函数在旧版是APawn::GetActorRotation,新版本可能建议直接GetActorRotation(),或者组件改为SceneComponent。多动手打印日志、反复编译,才能把别人代码里的“暗雷”给排掉。
这个主题先到这里。如果你正在搭自己的Pawn并遇到了卡住的问题,不妨把工程里的DefaultPawnClass、Controller、Input配置重新排查一遍,往往90%的“怎么不动”都能解决。