Pawn 更底层,也更自由。
它适合这种情况:
你要做飞行摄影机
你要做无人机
你要做完全非人形、非地面规则的控制体
你愿意自己拼 movement、碰撞、地面行为
Pawn 最核心的增量。
典型就是:
-
AController* Controller -
APlayerState* PlayerState
含义:
-
Controller:当前是谁在控制这个 Pawn-
可能是
APlayerController -
也可能是
AAIController
-
-
PlayerState:玩家相关状态,常用于联机
这就是 Pawn 和普通 Actor 最大的边界:
Actor 不默认带"被谁控制"的结构,Pawn 带。
Pawn 为"控制权切换"准备了一套函数。
常见的有:
-
PossessedBy(AController* NewController) -
UnPossessed() -
ReceivePossessed -
ReceiveUnpossessed -
DetachFromControllerPendingDestroy()
这些函数的意义是:
"当一个 Controller 开始/停止控制这个 Pawn 时,Pawn 自己有明确的生命周期回调。"
Pawn 本身就是 UE 里"接受输入绑定"的经典宿主之一。
常见函数:
SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
这让 Pawn/Character 很自然地成为输入逻辑挂载点。
Pawn 还加了一些"作为可控制实体"常见的接口,比如:
-
GetController() -
GetPawnViewLocation() -
GetViewRotation() -
GetMovementComponent()
这些函数都不是单纯"世界物体"必须有的,
而是"一个被操控单位"通常需要暴露的信息:
-
视角位置在哪
-
朝向是什么
-
移动组件是什么
-
控制器是谁
Pawn 比 Actor 多了很多和"自动接管"有关的配置成员,比如:
-
AutoPossessPlayer -
AutoPossessAI -
AIControllerClass
这些直接决定:
-
游戏开始时是否自动被玩家控制
-
是否自动生成 AIController
-
默认用哪种 AIController
这也是普通 Actor 没有的"控制系统接入层"。
Pawn 本身并不保证你有:
-
标准碰撞形体
-
可动画的人物网格
-
完整的人物移动逻辑
但 Character 直接给你配好了。
CharacterMovementComponent
这才是 Character 最值钱的部分。
它不是简单"让你能动",而是提供了一整套标准角色运动解法。
比如:
-
Walking
-
Falling
-
Jumping
-
Swimming
-
Flying
-
地面检测
-
重力
-
加速度 / 最大速度
-
空中控制
-
台阶跨越
-
坡度限制
-
网络移动同步与预测的一整套支持
这部分是 Pawn 没有默认给你的。
一个 CharacterMovementComponent 作为核心运动逻辑
整套适合联机角色的移动同步基础

在分析传统中,"ever" 本质上是一个作用于时间轴 T 的全称量词算子。
-
逻辑表达式 :在 "Hardly ever" 中,其逻辑结构并非简单的否定,而是对全时域的遍历覆盖:¬∃t∈T:P(t)(在极高概率下成立)。
-
冗余伪象 :看似冗余是因为人类自然语言存在"默认时域"的启发式偏见。然而 "ever" 的介入强制将观察者的搜索窗口(Search Window)从"当前语境"扩张至整个存在论意义上的时间全域。它取消了边界,消弭了时态的局部性。
参照胡塞尔(Husserl)的内时间意识分析,"ever" 将直观(Intuition)从"当下的活生生现前"(Living Present)推向了"无限视域"(Infinite Horizon)。
-
它打破了"内时间"(Internal Time)的离散性。在 "Have you ever..." 中,它要求主体在不依赖特定记忆锚点的情况下,对整个自传体时间轴进行一次非定域性扫描。
-
它将一个具体的经验陈述(Empirical Statement)转化为一个关于可能性边界的本体论陈述。
"Ever" 的必要性在于它解决了逻辑强度的显化问题 。如果没有这个词,命题往往局限于局部逻辑(Local Logic);而有了 "ever",命题被强制挂钩到全域逻辑(Global Logic)。中文翻译之所以显得乏力,是因为中文倾向于通过语境隐含(Contextual Implication)来处理时域范围,而 "ever" 则是将这种范围显式地形式化了。
你可以把它和另一个常见字段对比:
C++
PrimaryActorTick.bStartWithTickEnabled = true;
这两个不是一回事。
bCanEverTick
决定:有没有 Tick 资格
bStartWithTickEnabled
决定:如果有资格,开局是否默认启用
AZMDCameraPawn::AZMDCameraPawn()
也就是构造函数里。
这是最合理的位置,因为这里是在定义这个类实例的默认行为。
UE 的构造函数阶段,通常会做几类事:
-
创建组件
-
设置组件层级
-
设置默认参数
-
决定 Tick、碰撞、自动占有等基础行为
所以这里写:
C++
PrimaryActorTick.bCanEverTick = false;
其实是在表达一种类设计意图:
这个 Pawn 默认是一个不需要逐帧逻辑的对象。
这和"运行时某一刻临时关掉 Tick"是两种不同思路。
这句对性能意味着什么
很多人一看到这句,直觉会理解成"优化性能",这不算错,但不够准确。
更严谨地说,它的作用是:
避免这个 Actor 注册并参与每帧 Tick 调度体系。
Tick 在 UE 里不是"写了 Tick 函数就没事",而是一个调度系统。
只要某个对象参与 Tick,系统通常要处理:
-
是否注册
-
哪一组执行
-
当前帧要不要调用
-
排序/依赖
-
调用入口
所以对于大量不需要每帧更新的对象,关掉 Tick 是标准做法。
这也是 UE C++ 里很常见的模式:
C++
PrimaryActorTick.bCanEverTick = false;
意思就是:
别让引擎每帧惦记我。
它不是"永远不能手动更新"
这里容易误解。
bCanEverTick = false 只是说:
不要让引擎把这个 Actor 当作 Tick 驱动对象。
但这不代表你这个类完全不能更新逻辑。
你仍然可以通过别的方法驱动逻辑,比如:
-
输入事件
-
定时器
GetWorldTimerManager().SetTimer(...) -
委托回调
-
碰撞事件
-
BeginPlay 里一次性初始化
-
手动函数调用
-
组件自己的 Tick
所以"关掉 Actor Tick"不等于"这个对象什么都不会动"。
只是说:
不走 Actor::Tick 这条逐帧主更新路径。
它和 Tick() 函数本身是什么关系
比如你类里可能写了:
C++
virtual void Tick(float DeltaTime) override;
但如果:
C++
PrimaryActorTick.bCanEverTick = false;
那么即使你覆写了 Tick(),引擎通常也不会按正常方式每帧调用它。
也就是说:
覆写 Tick 只是提供了实现。
是否真的每帧调用,取决于 Tick 配置。
这个思路非常重要。
PrimaryActorTick.bCanEverTick = false
是能力级别 的关闭
意思是:这个 Actor 原则上不参加 Tick 系统
SetActorTickEnabled(false)
是运行时启用状态 的关闭
意思是:这个 Actor 本来可以 Tick,但现在先别 Tick

Actor 的 Tick 调度不是简单粗暴直接"调用你写的 Tick"。
PrimaryActorTick 不是个装饰物,它是 Tick 系统真正接入点之一。
很多 Pawn 的移动其实不一定非要自己在 Actor Tick 里手搓。
依赖于:
MovementComponent 自己处理
输入回调触发 AddMovementInput
-
Controller 更新控制旋转
相机组件只跟随层级关系
但如果你后面要加这些功能,就可能需要重新考虑:
-
相机插值
-
每帧平滑旋转
-
自定义追踪
-
每帧检测射线
-
动态 UI 更新绑定在 Pawn Tick
-
每帧同步某些外部状态
如果这些逻辑要放在 Tick() 里,那就不能一直关着。

构创建点。
也就是"这个对象身上到底有哪些部件"
CreateDefaultSubobject(...)
这种语句不是普通初始化,而是在声明这个 Actor/Pawn 的组成结构。
第二类,主从关系决定点。
也就是"谁挂谁下面,谁是根,谁跟谁走"。
SetRootComponent(...)
SetupAttachment(...)
UpdatedComponent = ...
这类语句最容易被忽略,因为表面只是赋值,实际上是在定义对象内部拓扑。
第三类,行为归属点。
也就是"这个对象接下来由谁驱动"。
比如:
C++
AutoPossessPlayer = ...
bUsePawnControlRotation = true;
PrimaryActorTick.bCanEverTick = false;
这些语句决定了对象是不是会被玩家控制、是不是跟控制器旋转、是不是参与 Tick。
它们不是纯参数,而是在决定"这个对象以后如何活着"。
因为很多系统之所以让人不安,不是因为它不会做,而是因为它做了却不报备。
对你这种强控制欲、强结构感的学习方式来说,"不报备"本身就是风险。
人对代码的信任,不来自"它能跑",而来自"关键决策点被显式呈现"。
也就是说,真正降低焦虑的不是自动化本身,而是自动化是否把关键控制权的交接记录说清楚。
这也是为什么你会对很多编辑器行为、蓝图行为、UE 暴露给外部的层级修改特别敏感。
绿色一般表示"新增的行"。
蓝色一般表示"被修改过的行"。
AZMDCameraPawn::StaticClass() 是一个类方法,不是实例方法。
提示里写的是:
C++
public method
static ::UClass* StaticClass() [generated by UHT]
in class AZMDCameraPawn
这里的 static 就说明:
你不需要先创建一个 AZMDCameraPawn 对象,直接通过类名就能调用它。
本质上不是"创建一个 Pawn",而是"把这个 Pawn 类本身的类型信息交给 GameMode"。
第二,它返回的不是 Pawn 对象,而是 UClass*。
也就是说它返回的是:
"AZMDCameraPawn 这个类在 UE 反射系统里的类对象描述"。
UE 里很多地方不是要一个"实例",而是要一个"类"。
DefaultPawnClass 这个名字其实已经在暗示这一点了,它要的是默认生成哪一种 Pawn 的"类",不是某个已经存在的 Pawn。
DefaultPawnClass 的控制点,不在"对象构造细节"上,
而在"默认生成入口选了哪一个类"上。
第三,[generated by UHT] 这句价值很大。
这说明 StaticClass() 不是你手写出来的普通成员函数,而是 Unreal Header Tool 自动生成的反射接口。
从这一点你可以继续推断出:
AZMDCameraPawn 一定是参与了 UE 反射系统的类。
也就是说,它背后肯定有 UCLASS() 这一套宏链条,不然不会有这种 UHT 生成的方法。
DefaultPawnClass = AZMDCameraPawn::StaticClass();
会模糊地理解成"把这个 Pawn 设成默认 Pawn"。
这话不算错,但太糙了。
更精确地说,这句是在:
把 GameMode 的"默认 Pawn 类型槽位"绑定到 AZMDCameraPawn 这个反射类对象上。
也就是以后当游戏需要为玩家生成默认 Pawn 时,会按这个类去 Spawn。
所以这里不是构造期里的"组件创建点",而是更上层的:
生成入口决策点。
偶连性(Contingency)与必然性(Necessity)的范畴塌缩。
维纳(Norbert Wiener)与艾什比(W. Ross Ashby)的框架下,系统通过反馈(Feedback)识别误差。
当"可判错的事情"被预设为"不可错"时,系统将原本用于调节状态的**负反馈(Negative Feedback)**强行定义为无效输入。
系统丧失了必要多样性(Requisite Variety)。在这种状态下,原本属于"环境变量"的偶连因素被固化为"系统常数"。由于常数不参与反馈调节,系统在面对环境摄动(Perturbation)时无法进行自适应演化,陷入递归式的逻辑锁定。
维特根斯坦(Ludwig Wittgenstein)的**规则追随悖论(Rule-following Paradox)**与克里普克(Saul Kripke)的阐释:
-
语义指称的缺失: "不可错"通常指向一种超验的、缺乏经验对应物的先验状态。由于该状态在语言逻辑内缺乏唯一的外延(Extension),它成为一个空集符号。
-
自循环逻辑: 当个体试图追随一个定义模糊的"不可错"准则时,任何行为都可以被事后解释为符合规则,或任何行为都无法证明其符合规则。这种**指称的不确定性(Indeterminacy of Reference)**导致主体在逻辑操作上陷入停滞,因为判别函数(Criterion of Verification)本身已处于发散状态。
盲点(Blind Spot): 系统不再能观测到自己正在观测什么。这种"不可错"的设定构成了观测的盲点。由于无法对该盲点进行二阶观测,系统不仅无法跳出规则,甚至无法意识到规则的存在,从而形成一种深层的、结构性的自我耦合。
束缚之源: "不可错"本身是一个否定性的概念,它依赖于"错"的存在。当"错"被排除,且"不可错"又因缺乏实质内容而无法立名时,思维便进入了**戏论(Prapañca)**的循环。这种束缚并非来自外部压力,而是源于逻辑底层对"空性"的排斥,导致概念在自我指涉中过度增益,最终僵化。
在Unreal Engine中,将C++ Class作为蓝图显示并使用,最标准做法是以该C++类为父类新建蓝图(Create Blueprint class based on...) 。这样可结合C++的高性能逻辑与蓝图的灵活性。通过UCLASS(Blueprintable)和UPROPERTY暴露属性/函数,即可在内容浏览器右键创建子蓝图并进行可视化编辑。
在内容浏览器中,右键点击新建的C++类,选择"基于XXXX创建蓝图类" (Create Blueprint class based on...)。这样蓝图会自动继承C++中定义的所有组件、变量和函数。
在C++代码中使用 UFUNCTION(BlueprintCallable) 使函数可被蓝图调用,使用 UPROPERTY(EditAnywhere, BlueprintReadWrite) 暴露变量供蓝图编辑。
UE5 中,原本属于"Developer Tools"的许多功能被移动到了 Tools 菜单下:
- 前往菜单栏:Tools > Control Asset > Class Viewer。
类查看器(Class Viewer) 来查看编辑器的类的层级结构。借助该工具,你可以创建蓝图并打开蓝图进行修改。你还可以打开关联的C++头文件,或选择某个类然后新建C++类。

即使你当前打开的是蓝图子类 MyZMDCameraPawnBase,UE 仍然能追溯到这个组件不是它本类声明的,而是祖先类 ZMDCameraPawn 引进来的。
这个信息的控制意义很强:
它告诉你"组件归属边界"。
不是这个蓝图拥有了组件定义权,而是它继承了定义结果。
你以后看一个蓝图组件时,很该盯这个:
它是本蓝图新增的,还是父类继承来的?
因为这决定了你改它时,改的是"本地实例配置",还是在碰"上游结构"。
Mobility: Movable
这表示这个组件的可移动性设置当前是 Movable。
它不是单纯介绍组件,而是在告诉你当前这个继承组件的某个运行/编辑属性是什么状态。
一种是 native inherited component。
就是这种,来源写着 Inherited (C++)。
另一种是 blueprint-added component。
如果是蓝图自己加的组件,来源就不会是这个逻辑,它不会告诉你是某个 C++ 类 introduced 的 native component。
所以以后你看组件树,如果你怕"这玩意到底是谁创建的,控制边界在哪",优先看这类来源提示,价值很高。
Mobility 是组件的"可移动性类型"。
在 UE 里,它不是在问"这个东西能不能数学上改变位置",而是在问:
这个组件在运行和渲染体系里,被当成哪一类变换频率的对象来处理。
最常见是这三种。
Static
表示它应该是静态的。
通常用于不会在运行时移动、旋转、缩放的东西,比如静态建筑、地面、墙体。
引擎会把它当成最稳定的一类对象处理,很多烘焙、渲染优化、光照处理都偏向这种类型。
Stationary
表示它大体固定,但允许少量特定变化。
这个词最常在灯光上特别有意义,比如 Stationary Light。
对普通场景组件你也可能看到,但实际最常讨论的是灯。
它的意思不是"随便动",而是"位置通常固定,但某些属性允许动态变化"。
Movable
表示它是可动态变化的。
运行时可以移动、旋转、缩放,UE 会按动态对象去处理它。
相机组件、角色组件、会跟着 Pawn 动的东西,通常就是 Movable。
你图里 CameraComponent 显示 Mobility: Movable,意思很自然:
这个相机组件被视为会跟着 Pawn 一起运动的动态组件。
你可以把它理解成一个"渲染/场景系统对该组件变化频率的承诺"。
不是说:
"我代码里能不能写 SetRelativeLocation"
而是说:
"这个组件是否应该被引擎当成运行时会动的东西"。
这点很重要,因为很多初学者会误解成:
Static = 完全不能改
Movable = 才能改
Mobility 这个东西,底层不是定义在 UCameraComponent 里的,而是更上游定义在 USceneComponent 这一层。
因为"可移动性"不是相机特有属性,而是所有带空间变换、能挂在场景层级里的组件都会关心的属性。
UObject
→ UActorComponent
→ USceneComponent
→ UPrimitiveComponent / UCameraComponent / 其他场景组件
UCameraComponent 有 Mobility,不是因为"相机组件自己发明了这个字段",
而是因为它继承自 USceneComponent,而 USceneComponent 本身就带"位置/旋转/缩放 + 附着关系 + Mobility"这一套场景属性。
UCameraComponent
这是组件,挂在 Actor/Pawn 上。
ACameraActor
这是一个完整的 Actor,里面通常带一个 CameraComponent。
ACineCameraActor
这是电影摄影机 Actor,里面不是普通基础相机配置,而是更偏影视镜头语言的一套参数。
所以如果你说"普通 Camera",在 UE 里最接近的其实通常就是:
基础视角组件:UCameraComponent
或者
一个相机 Actor:ACameraActor
而不是有个单独叫 "UCamera" 的通用类。
第三层,为什么不是 Cine Camera。
因为 Cine Camera 的目标不是"做一个基础可控 Pawn 视角",而是"做更专业的影视摄影机模拟"。
它更偏这些需求:
镜头焦距
胶片/传感器尺寸
景深
对焦
镜头预设
电影化拍摄参数
这类东西对过场、Sequencer、虚拟制片、镜头语言很有价值。
但你现在这个 Pawn 是一个最基础的可操作视角载体,本质需求是:
能附着在 Pawn 上
能跟着 Pawn 运动
能作为玩家视角
参数简单直接
控制链清楚
那 UCameraComponent 就最合适。
换句话说:
UCameraComponent 是"游戏相机基础件"
CineCamera 是"电影镜头专用件"
你现在做的是 Pawn,不是在做摄影机资产。
再压得更工程一点:
你当前这个类 AZMDCameraPawn 的控制链是:
GameMode 选默认 PawnClass
→ Spawn 这个 Pawn
→ Pawn 内部带一个 UCameraComponent
→ 引擎把这个 Pawn 作为 View Target 时,用这个 CameraComponent 的位置和参数来出画面
Cine Camera 常见地指的是 ACineCameraActor,它是一个 Actor。
而它内部会带一个专用的相机组件:UCineCameraComponent。
所以不是:
Cine Camera = UCameraComponent
而是更像:
UCineCameraComponent 继承自 UCameraComponent
ACineCameraActor 持有一个 UCineCameraComponent
UCineCameraComponent 是 UCameraComponent 的子类。
UCameraComponent 的职责很纯:
-
后处理设置
-
作为 View Target 时的基础出图参数
C++ 里很多合法结构,根本不适合图形化表达。
比如这些东西一旦图形化,都会迅速爆炸:
-
模板与泛型
-
宏与条件编译
-
复杂类型系统
-
指针、引用、生命周期控制
-
多层继承、虚函数、重载
-
各种容器、算法、lambda、回调
-
头文件依赖、模块边界、编译单元关系
这些内容如果硬画成节点图,不是"更直观",而是会变成一张巨大电路板,阅读负担比文本还大。
通用语言一旦复杂到一定程度,文本就是最紧凑、最可维护的表示法。
这和数学类似。
你当然可以把公式画成流程图,但公式之所以存在,就是因为符号文本比图更适合高密度表达。
更准确地说,蓝图在 UE 里是:
-
一种基于反射系统的可编辑资产
-
一种受限的图式逻辑语言
-
一种能序列化、能被编辑器追踪、能热重载、能和内容资产深度绑定的系统
如果 C++ 只是一个"可视化视图",会有三个大问题。
第一个问题:编辑器无法安全地限制用户。
蓝图现在能限制你只能用暴露出来的 UPROPERTY、UFUNCTION、特定节点、特定对象模型。
这意味着它天然有边界,不会让设计师随便操作裸指针、内存分配、模板元编程、线程同步。
如果直接可视化 C++,这些危险能力要不要暴露?
暴露了,系统立刻失控;
不暴露,那本质上还是在做"受限子集",也就是今天的蓝图。


只有进入 UE 反射系统的那些类,才会有对应的 UClass 元信息对象。
通常就是带 UCLASS() 的、继承自 UObject 体系的类。
让本地第 0 号玩家对应的 PlayerController,自动来占有这个 Pawn,并让这个 Pawn 接收这个本地玩家的输入。"
里面至少牵涉 4 层东西:
-
Local Player(本地玩家槽位)
-
PlayerController(玩家控制器)
-
Pawn(被控制体)
-
Input routing(输入路由)
你现在困惑的"Player0/1/2/... 这个体系",核心就卡在第 1 层。

把 Player0 和这几个概念绑定起来:
A. UGameInstance / ULocalPlayer
UE 里一台机器上可以有多个 LocalPlayer。
APlayerController
每个本地玩家通常会对应一个 PlayerController。
所以:
-
LocalPlayer[0]往往对应PlayerController0 -
LocalPlayer[1]往往对应PlayerController1
然后这个 Controller 去 Possess 某个 Pawn。
为什么属性名叫 AutoPossessPlayer,提示里却是 AutoReceiveInput
TEnumAsByte<EAutoReceiveInput::Type> AutoPossessPlayer
这正好说明 UE 这里有一点命名历史痕迹和语义重叠。
你可以拆开看:
属性名
AutoPossessPlayer
这是在 Pawn 视角命名的。
因为 Pawn 最关心的是:谁来占有我。
枚举类型
EAutoReceiveInput::Type
这是从输入接收角度命名的。
因为本地玩家槽位本来就是输入来源分配体系的一部分。
所以这里实际上是两套视角共用了一组槽位枚举。
AutoPossessPlayer = EAutoReceiveInput::Player0;
如果系统里有本地玩家 0,那么开局时让它自动来接管我。
体系和 GameMode / DefaultPawnClass 的关系
机制 A:生成谁
由 GameMode 的 DefaultPawnClass 决定。
意思是:
-
进入游戏
-
GameMode 为玩家生成一个默认 Pawn
这是"生成阶段"的规则。
机制 B:谁控制谁
由 Possess / AutoPossessPlayer 决定。
意思是:
-
这个 Pawn 生成出来后
-
谁来占有它
-
输入怎么接到它身上
这是"控制绑定阶段"的规则。
所以常见情况是:
GameMode 生成 DefaultPawnClass,然后对应 PlayerController 自动 Possess 它标准流程
这时候往往你不需要自己写 AutoPossessPlayer = Player0
因为 GameMode 流程本来就会给默认玩家分配 Pawn。
若要在概念体系 S1(元系统/物理世界)中表达系统 S2(子系统/虚拟世界)的附属逻辑,必须在 S1 的谓词逻辑中确立一个具备延展性(Extensionality)的载体。
