自由学习记录(167)

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 / 其他场景组件

UCameraComponentMobility,不是因为"相机组件自己发明了这个字段",

而是因为它继承自 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

UCineCameraComponentUCameraComponent 的子类。

UCameraComponent 的职责很纯:

  • 后处理设置

  • 作为 View Target 时的基础出图参数

C++ 里很多合法结构,根本不适合图形化表达。

比如这些东西一旦图形化,都会迅速爆炸:

  1. 模板与泛型

  2. 宏与条件编译

  3. 复杂类型系统

  4. 指针、引用、生命周期控制

  5. 多层继承、虚函数、重载

  6. 各种容器、算法、lambda、回调

  7. 头文件依赖、模块边界、编译单元关系

这些内容如果硬画成节点图,不是"更直观",而是会变成一张巨大电路板,阅读负担比文本还大。

通用语言一旦复杂到一定程度,文本就是最紧凑、最可维护的表示法。

这和数学类似。

你当然可以把公式画成流程图,但公式之所以存在,就是因为符号文本比图更适合高密度表达。

更准确地说,蓝图在 UE 里是:

  1. 一种基于反射系统的可编辑资产

  2. 一种受限的图式逻辑语言

  3. 一种能序列化、能被编辑器追踪、能热重载、能和内容资产深度绑定的系统

如果 C++ 只是一个"可视化视图",会有三个大问题。

第一个问题:编辑器无法安全地限制用户。

蓝图现在能限制你只能用暴露出来的 UPROPERTY、UFUNCTION、特定节点、特定对象模型。

这意味着它天然有边界,不会让设计师随便操作裸指针、内存分配、模板元编程、线程同步。

如果直接可视化 C++,这些危险能力要不要暴露?

暴露了,系统立刻失控;

不暴露,那本质上还是在做"受限子集",也就是今天的蓝图。

只有进入 UE 反射系统的那些类,才会有对应的 UClass 元信息对象。

通常就是带 UCLASS() 的、继承自 UObject 体系的类。

让本地第 0 号玩家对应的 PlayerController,自动来占有这个 Pawn,并让这个 Pawn 接收这个本地玩家的输入。"

里面至少牵涉 4 层东西:

  1. Local Player(本地玩家槽位)

  2. PlayerController(玩家控制器)

  3. Pawn(被控制体)

  4. 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:生成谁

GameModeDefaultPawnClass 决定。

意思是:

  • 进入游戏

  • GameMode 为玩家生成一个默认 Pawn

这是"生成阶段"的规则。

机制 B:谁控制谁

Possess / AutoPossessPlayer 决定。

意思是:

  • 这个 Pawn 生成出来后

  • 谁来占有它

  • 输入怎么接到它身上

这是"控制绑定阶段"的规则。

所以常见情况是:

GameMode 生成 DefaultPawnClass,然后对应 PlayerController 自动 Possess 它标准流程

这时候往往你不需要自己写 AutoPossessPlayer = Player0

因为 GameMode 流程本来就会给默认玩家分配 Pawn。

若要在概念体系 S1(元系统/物理世界)中表达系统 S2(子系统/虚拟世界)的附属逻辑,必须在 S1 的谓词逻辑中确立一个具备延展性(Extensionality)的载体。

相关推荐
扣脑壳的FPGAer2 小时前
数字信号处理学习笔记--Chapter 1.4.1 时域采样定理基本概念
笔记·学习·信号处理
矢志航天的阿洪2 小时前
面目标 SAR 回波整体处理过程(教学技术文档)面目标 SAR 回波整体处理过程(教学技术文档)
学习
运维技术小记2 小时前
这个 MIT 学生用 AI 学习法两天搞定一门课的方法,颠覆认知!
人工智能·学习
Huangjin007_2 小时前
【C++ STL篇(四)】一文拿捏vector常用接口!
开发语言·c++·学习
渡我白衣2 小时前
触类旁通——迁移学习、多任务学习与元学习
人工智能·深度学习·神经网络·学习·机器学习·迁移学习·caffe
敢敢のwings2 小时前
NVIDIA Isaac GR00T与Cosmos:重塑机器人学习的合成数据革命
学习·机器人
凉、介2 小时前
从设备树到驱动源码:揭秘嵌入式 Linux 中 MMC 子系统的统一与差异
linux·驱动开发·笔记·学习·嵌入式·sd·emmc
Paxon Zhang2 小时前
JavaEE初阶学习web开发的第一步**计算机组成原理,操作系统,进程(基础扫盲)**
java·后端·学习·java-ee
weixin_443478513 小时前
Flutter学习之自定义组件
javascript·学习·flutter