自由学习记录(207)

Animation Asset 编辑器操作

默认你点一个 Animation Asset,当前 Animation Editor 可能会被切换到那个资产;按住 Shift 再点,就会在新的 editor tab 里打开,不把你当前正在看的动画顶掉。

不想每点一次 ReloadLoad 就把 Montage 页面替换掉。

Montage Notify / Section 基础

Proportional: The Notify will be attached to the associated sequence even when reordering Sequences.

没有 Sequence

└ Section 只是空时间标签

Blueprint 里 Jump to Section 的目标

角色姿态与 Reload 动画

lying supine

└ 仰卧,脸朝上

lying prone

└ 趴着,俯卧

crouching

└ 蹲着

standing

└ 如果角色站着

└ 播放 standing reload Slot 里的动画

lying prone

└ 如果角色趴着

└ 播放 prone reload Slot 里的动画

you can now play the Montage from a Blueprint as long as your character actor is able to be referenced.

AI Assistant 上下文绑定

这更像 UE 5.8 Experimental AIAssistant 的设计限制/半成品接口,不一定算传统 bug。

从源码看,GetDockedContext() 不是问 Editor:"当前前台资产编辑器是谁?"

它问的是:"我这个 AIAssistant WebBrowser widget 被 dock 在谁里面?"

所以它必须从 SAIAssistantWebBrowser 往父级 Slate widget 树上爬:

复制代码
Current = BrowserWidget;
while (Current)
{
    if (Current->GetTypeAsString() == "SStandaloneAssetEditorToolkitHost")
    {
        StandaloneAssetEditorHost = Current;
        break;
    }
    Current = Current->GetParentWidget();
}

找不到这个父级,就直接返回空。也就是说它绑定的是 dock ownership/context,不是全局焦点。

为什么会这样:

  • 这样可以避免误读别的资产编辑器,尤其多个 Blueprint/Material editor 同时打开时。
  • 但代价是很硬:AI Assistant 不 dock 进目标资产编辑器,就没有上下文。
  • 注释里也承认这段不理想:This per-asset logic is not ideal, but it's the best we can do with the current AssetEditorInterface.
  • 插件路径是 Engine/Plugins/Experimental/AIAssistant,本身就是 Experimental。

所以准确说:

不是 MCP 没开启,也不是你没连上;是 UE 这套 "GetDockedContext" 只在 AI Assistant dock 到 Asset Editor 内部时才有上下文。

Layered Animation Blueprints

Layered Animation Blueprints / Linked Anim Layers 不是普通情况。它的模型更像:

复制代码
Character / Skeletal Mesh Component
└ Main Animation Blueprint
  ├ Full Body layer
  ├ Upper Body layer
  ├ IK layer
  └ 可链接到其他 Animation Blueprint class

Epic 文档说,Linked Anim Layers 的目的就是把复杂 Animation Blueprint 拆成多个 Animation Blueprint 文件:一个 Base Animation Blueprint 负责主结构,其他 Animation Blueprints 负责某些 gameplay differentiator,比如武器、车辆、特殊状态等。官方例子里甚至说,可以为 rifle、bow、rocket launcher 创建相似的 Animation Blueprint layer,只在对应武器启用时加载。

所以这里的 Layered Animation Blueprints 里的 s 是有意义的。

Montage 默认参数与 Blend

Asset Details 只告诉你"这个 Montage 如果被播放,它该怎么 blend、怎么 sync、怎么结束"。

UAnimMontage 构造函数里默认设置这些属性:

复制代码
BlendModeIn = EMontageBlendMode::Standard;
BlendModeOut = EMontageBlendMode::Standard;
BlendIn.SetBlendTime(0.25f);
BlendOut.SetBlendTime(0.25f);
BlendOutTriggerTime = -1.f;
bEnableAutoBlendOut = true;
SyncSlotIndex = 0;

位置是 UE 5.8 source:Engine/Source/Runtime/Engine/Private/Animation/AnimMontage.cpp:70-92。也就是说这就是 Montage asset 的默认播放参数,不是 debug 信息。

有用 └ 同一个 Montage 每次播放都应该有统一的默认 BlendIn/BlendOut └ 攻击、翻滚、Reload、HitReact 这种短动作需要稳定地进出 └ Montage Section 需要默认跳转/循环 └ 同步组、Root Motion、网络播放需要稳定参数

复制代码
角色正常跑步
└ 你按攻击
   └ Play Montage: Attack_Montage
      └ 进 DefaultSlot
         └ Slot node 覆盖上半身 / 全身

这时你发现攻击动画不是立刻出来,而是"软软地滑进去"。原因可能不是蓝图错了,而是 Montage 默认有:

复制代码
BlendIn.SetBlendTime(0.25f);

另一个很常见的场景:

复制代码
攻击 Montage 播完
└ 角色回到 locomotion
   └ 手臂/身体突然弹一下

这时查的是:

复制代码
BlendOut.SetBlendTime(0.25f);

Blend Out 太短,回基础姿势会 pop;太长,攻击结束后身体还拖着旧姿势,操作会感觉粘。这个时候 Asset Details 就有意义。

Blend Profile

Blend Profile 不需要你在 Content Browser 里单独做一个资产

它通常是存在 Skeleton 资产里面的一组配置

复制代码
Skeleton
└ Blend Profiles
   ├ UpperBody_Fast
   ├ LowerBody_Slow
   └ HitReact_Profile

UE 5.8 源码里也很明确:

复制代码
// List of blend profiles available in this skeleton
UPROPERTY(Instanced)
TArray<TObjectPtr<UBlendProfile>> BlendProfiles;

位置:Engine/Source/Runtime/Engine/Classes/Animation/Skeleton.h:522-526

USkeleton └ 内部持有 BlendProfiles 数组 └ Montage / Blend node / Motion Matching / BlendStack 只是引用其中某一个 UBlendProfile

A blend profile is a set of per-bone scales that can be used in transitions and blend lists to tweak the weights of specific bones.

它的本质不是"动画资产",而是:

复制代码
每根骨骼的 blend 缩放表
└ 哪些骨头更快混合
└ 哪些骨头更慢混合
└ 哪些骨头权重更弱 / 更强

编辑器里你一般从 Skeleton Editor / Skeleton Tree 里创建它,而不是右键 Content Browser 创建一个普通 asset。

Open Skeleton └ Skeleton Tree └ Add └ Time Blend Profile └ Weight Blend Profile └ Blend Mask

Blend Profile In └ 选择 Skeleton 里已有的 Blend Profile Blend Profile Out └ 选择 Skeleton 里已有的 Blend Profile

Curve Asset

Curve asset └ 外部资产 └ 例如 Blend In / Blend Out 的 Custom Curve Animation Float Curve └ 存在 Animation Sequence / Montage 内部 └ 用名字查 └ Time Stretch Curve Name 找的就是这个 FTimeStretchCurve └ Montage 内部的 baked runtime data └ 存 SamplingRate / Markers └ 不是你手动画的源曲线

FCompositeSection / FAnimLinkableElement

struct FCompositeSection : public FAnimLinkableElement

它不是只显示 FCompositeSection 自己的字段,还显示了父类 FAnimLinkableElement 的字段。

文档里看到的这些:

复制代码
Slot Index
Link Method
Linked Sequence

来自父类:

复制代码
FAnimLinkableElement
└ SlotIndex
└ LinkMethod
└ LinkedSequence

位置:Engine/Source/Runtime/Engine/Classes/Animation/AnimLinkableElement.h:133-165

蓝图里常用:

复制代码
Montage Play
└ In Montage To Play = AM_Attack
Montage Jump to Section
└ Section Name = Combo_02
Montage Set Next Section
└ Section Name To Change = Combo_01
└ Next Section = Combo_02

Section 的编辑器维护语义

当 Sequence / Segment 变长、变短、移动时,Section 要不要跟着动

struct FCompositeSection : public FAnimLinkableElement

loop 被放回到 Animation Sequence 自身内部,而不是由 Montage 外层控制。

innate to the Animation

Slot Group

A single Montage can play animations on multiple Slots at the same time, but they must be within the same Slot Group.

文档把它放在 ABP 下面,大概是因为用户真正要让 Slot 生效,必须在 Animation Blueprint 的 AnimGraph 里放 Slot node 。没有 ABP 里的 Slot node,Montage 里写了 UpperBody 也只是写了一个名字,pose 没有明确插入口。

在 Timeline 上你只能看到三个 marker 排成一行;但是你不容易看出 Loop 是不是指向自己、有没有断链、是不是跳到别的 section。

UPROPERTY(Transient)

UPROPERTY(Transient) 的说明是:这个属性不会被保存或加载,带这个标记的属性在加载时会被 zero-filled,也就是清零/重置。UE 5.7 的 CPF_Transient 也写成"不应被保存或加载",但 Blueprint CDO 有特殊例外。

AnimNodeReference Tag

  • Tag 提供方:任何继承 UAnimGraphNode_Base 的 AnimGraph 节点都能设置 Tag
  • Tag 引用方:UK2Node_AnimNodeReference,菜单分类是 Variables
  • 它可以放在该 AnimBlueprint 的函数图、EventGraph 等 K2 图中,不要求放在 AnimGraph。
  • RequestTaggedNode() 在引擎源码中只有这个引用节点调用。因此出现这条错误,就能判断至少有一个 Anim Node Reference 请求了该 Tag。当前资产里实际有两个。

FAnimNode_Base 临时结构体

没有特殊的 Destroy() 操作,只是普通 C++ 临时结构体的生命周期结束。它内部也没有拥有资源,销毁只是丢弃两个成员:

复制代码
FAnimNode_Base* AnimNode;
UScriptStruct* AnimNodeStruct;

struct vs class / C++ 与 Blueprint 分工

struct └ 默认 public class └ 默认 private

程式設計師 └ 用 C++ 建立穩定的底層/基礎系統 └ 用 markup 選擇性地開放接口 └ 設計師用 Blueprint 繼承、配置、呼叫或覆寫

复制代码
C++ WeaponBase
├ 傷害計算
├ 彈藥驗證
├ Network Authority
└ Fire() 基礎流程
Blueprint BP_Rifle
├ Damage = 30
├ 換模型
├ 換音效
├ 換粒子效果
└ 實作 OnFired()

FMemberReference / Blueprint Rename

Blueprint Rename 後仍然能找到成員的重要原因

FMemberReference

Header Engine/Source/Runtime/Engine/Classes/Engine/MemberReference.h

Source Engine/Source/Runtime/Engine/Private/MemberReference.cpp

為什麼不用直接存 FProperty*

原因主要有三個:

复制代码
Blueprint Compile
        │
        ▼
Property 重新建立

指標會失效。

另外:

复制代码
Rename

名稱可能改變。

還有:

复制代码
Hot Reload
Blueprint Recompile

都可能讓 Field 重建。

因此 Unreal 選擇保存:

复制代码
Name
Guid
Scope
ParentClass

真正使用時再解析。

复制代码
Hot Reload
Blueprint Recompile

都可能讓 Field 重建。

因此 Unreal 選擇保存:

复制代码
Name
Guid
Scope
ParentClass

真正使用時再解析。

复制代码
UPROPERTY(EditAnywhere, Category = Settings,
    meta = (PinHiddenByDefault, ClampMin = "0"))
float BlendTime;

UPROPERTY(EditAnywhere, Category = Settings,
    meta = (PinHiddenByDefault, UseAsBlendProfile = true))
TObjectPtr<UBlendProfile> BlendProfile;

PinHiddenByDefault
→ 可以成为节点 Pin,但默认隐藏
PinShownByDefault
→ 默认显示成节点 Pin
ClampMin = 0
→ 不允许输入负数
UseAsBlendProfile
→ 使用 Skeleton Blend Profile 选择器

所以 Details 不是根据变量名字随便猜出来的;它由 UHT 解析 UPROPERTYmeta,生成反射描述,然后 Property Editor 根据描述创建控件。

FAnimNode_BaseUnreal Engine 動畫運行時(Runtime)所有 Animation Node 的共同基類

Animation Blueprint (Editor) UAnimGraphNode_XXXX │ │ 編譯 ▼ Anim Blueprint Generated Class │ │ 包含 ▼ FAnimNode_XXXX (Runtime) │ ▼ FAnimNode_Base

Editor Node (UAnimGraphNode) │ 編譯後 ▼ Runtime Node (FAnimNode_Base)

AnimGraphNode 是編輯器用的,FAnimNode_Base 才是真正每幀執行動畫的東西。

FAnimNode_Base 最重要的是定義了 Animation Graph 每幀執行流程。

复制代码
Initialize_AnyThread()
    │
CacheBones_AnyThread()
    │
PreUpdate()        (Game Thread,可選)
    │
Update_AnyThread()
    │
Evaluate_AnyThread()
    │
GatherDebugData()

官方 API 幾乎就是照這個順序設計。


FAnimNode_Base 自己幾乎沒有動畫功能

它主要提供的是動畫節點生命週期與執行框架

复制代码
FAnimNode_Base
├─ Initialize
├─ CacheBones
├─ PreUpdate
├─ Update
├─ Evaluate
├─ GatherDebugData
├─ NodeData
├─ ExposedValueHandler
└─ Worker Thread 支援

真正的動畫功能都由子類實作,例如:

复制代码
FAnimNode_Base
│
├─ FAnimNode_SequencePlayer
│      └─ 播放 Sequence
│
├─ FAnimNode_BlendListBase
│      └─ Blend 多個 Pose
│
├─ FAnimNode_LayeredBoneBlend
│      └─ Layer Blend
│
├─ FAnimNode_Slot
│      └─ Montage Slot
│
├─ FAnimNode_StateMachine
│      └─ 狀態機
│
├─ FAnimNode_OffsetRootBone
│      └─ Root Bone Offset
│
└─ FAnimNode_SkeletalControlBase
       └─ IK、FABRIK、CCDIK、LookAt 等 Skeletal Control

這也是為什麼幾乎所有 Runtime Animation Node 最終都會繼承自 FAnimNode_Base

Motion Matching 不是先执行完搜索,再把结果交给一个完全外部的 Blend Stack;它本身继承了 FAnimNode_BlendStack_Standalone,Blend Stack 能力已经进入它的类型和内存布局。

AnimGraphNode_BlendStack.h (line 14)(E:/UE_5.8/Engine/Plugins/Animation/BlendStack/Source/Editor/Public/AnimGraphNode_BlendStack.h:14)

这里负责的是编辑器能力,例如:

  • 节点标题和颜色
  • 内部 AnimationBlendStackGraph_0
  • 内部 Graph 的创建与管理
  • 编译检查
  • 节点菜单行为
  • Debug 显示
  • Blend Stack Graph 的编辑入口

这些通常不会全部写入运行时 FAnimNode_MotionMatching,因为打包后的游戏不需要节点标题、画布位置和编辑器 Graph 对象。

├── Pencil(铅笔) ├── Pen(钢笔) ├── Brush(毛笔/画笔) ├── Airbrush(喷枪) ├── Watercolor Brush(水彩笔) ├── Oil Brush(油画笔) └── Charcoal(炭笔)

鼠标驱动

如果你的鼠标是:

  • Logitech(Logi Options+)
  • Razer Synapse
  • Corsair iCUE
  • SteelSeries GG

很多都支持针对单个程序修改按键。

例如:

复制代码
程序:sai2.exe
中键
↓
发送 Space

或者发送其他组合键。

Chooser 资产是这个 K2 节点自身保存的编辑器属性:

复制代码
UPROPERTY()
TObjectPtr<UChooserTable> Chooser;

当前值就是:

复制代码
CHT_PoseSearchDatabases

它被烘焙进节点类型/标题中。

Serialization 不是单纯"按序列排列数据"。

在 UE 中,序列化更准确的含义是:

把对象中需要持久保存的状态转换成可以写入 package 文件的数据,之后再从文件恢复这些状态。

在这里,以下成员带有 UPROPERTY()

复制代码
UPROPERTY()
TArray<FName> PropertyBindingChain;
UPROPERTY()
int ContextIndex = 0;
UPROPERTY()
bool IsBoundToRoot = false;

所以保存 Chooser 资产时,它们会被写入 .uasset

例如资产中长期保存的是:

复制代码
ContextIndex = 0
PropertyBindingChain
├─ "CharacterProperties"
├─ "MovementState"
└─ "bIsFalling"

但是下面这个没有 UPROPERTY()

复制代码
TSharedPtr<UE::Chooser::FCompiledBinding> CompiledBinding;

因此这个编译结果不是作为资产数据永久保存的。

迁移 UE 项目时,Chooser 资产中序列化的符号绑定关系会随 .uasset 保留;新机器不会沿用旧机器的内存连接,而是根据同样的类型和属性路径重新编译出连接。

可见内容中只有 GetDeltaSecondsBlueprint_GetMainAnimInstance 是 UE 5.8 引擎源码里的 UAnimInstance 原生函数;其余可见函数和属性都是该示例项目的蓝图层成员。

SandboxCharacter_CMC_ABP_C 后面的 _C 表示它是蓝图编译后生成的运行时 UClass。Chooser 看到的是这个完整类,而不是只看蓝图编辑器里当前作者创建的变量。

复制代码
C++ 内部函数名:Blueprint_GetMainAnimInstance
蓝图正常显示名:Get Main Anim Instance

但 Chooser 的这个绑定菜单拿到了较底层的反射成员名,所以显示成了 Blueprint_GetMainAnimInstance,没有完全采用普通蓝图节点的友好名称。

从资产根对象到目标数据之间,存在一条不中断的反射序列化链。链上的每一层要么是 UPROPERTY,要么是具有自定义序列化能力的反射结构。

为什么 UPROPERTY 最终真的会进入资产序列化

只看到 UPROPERTY() 还不算完整证明。还要继续追到引擎通用序列化代码。

1. UChooserTable::Serialize() 调用父类序列化

文件:

复制代码
Engine/Plugins/Chooser/Source/Chooser/Private/Chooser.cpp

第 210--219 行:

复制代码
void UChooserTable::Serialize(FArchive& Ar)
{
#if WITH_EDITORONLY_DATA
    if (Ar.IsSaving())
    {
        PopulateCookedData(Ar.IsCooking());
    }
#endif
Super::Serialize(Ar);
}

最重要的是:

复制代码
Super::Serialize(Ar);

如果一个 UObject 自定义了 Serialize(),却没有调用父类,通用 UPROPERTY 序列化链就可能被截断。

UObject 通用序列化进入属性系统

文件:

复制代码
Engine/Source/Runtime/CoreUObject/Private/UObject/Obj.cpp

第 1843--1859 行:

复制代码
// Serialize object properties which are defined in the class.
if (ObjClass != UClass::StaticClass())
{
    SerializeScriptProperties(
        Record.EnterField(TEXT("Properties"))
    );
}

所以链条已经从 Chooser 专用代码进入 CoreUObject:

复制代码
UChooserTable::Serialize
└─ Super::Serialize
   └─ UObject 通用序列化
      └─ SerializeScriptProperties

Chooser 不需要自己手写:

复制代码
Ar << ColumnsStructs;
Ar << PropertyBindingChain;

反射系统会统一处理。

SerializeScriptProperties() 交给 UStruct

随后会进入:

复制代码
ObjClass->SerializeTaggedProperties(...);

最终对应:

复制代码
Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp

第 1476--1497 行:

复制代码
void UStruct::SerializeTaggedProperties(...)
{
    if (Slot.GetArchiveState().UseUnversionedPropertySerialization())
    {
        SerializeUnversionedProperties(...);
    }
    else
    {
        SerializeVersionedTaggedProperties(...);
    }
}

这里说明 UE 有两条物理编码路线:

复制代码
Versioned Tagged Property Serialization
或
Unversioned Property Serialization

但两者处理的来源都是 UStruct 中已经注册的反射属性。

不是遍历 C++ 类里所有普通成员,而是遍历反射系统已经建立的 FProperty 链。

普通 C++ 成员 └─ 默认不在 PropertyLink UPROPERTY 成员 └─ UHT 生成反射描述 └─ 成为 FProperty └─ 进入 PropertyLink └─ 被通用序列化器遍历

FBoolContextProperty 是谁写的

这是 Epic 的 Chooser 插件作者写的普通 C++ 类型,不是 UE 自动凭空生成的,也不是你项目里自己写的。

名字可以拆成:

复制代码
F Bool Context Property
│ │    │       └─ 表示一个可访问的属性绑定
│ │    └─ 数据来自 Chooser Evaluation Context
│ └─ 最终希望读取的是 bool
└─ UE 普通结构体命名前缀

也就是说,它表达的是:

一个能够从 Chooser Context 中读取或写入 bool 的参数实现。

它写在:

复制代码
BoolColumn.h

而同一个插件还会有很多平行类型:

复制代码
FBoolContextProperty
FFloatContextProperty
FEnumContextProperty
FNameContextProperty
FObjectContextProperty

它们的区别主要是最终数据类型不同。

Epic 事先写好一批参数类型 └─ 编辑器允许你在 FInstancedStruct 中选其中一个

复制代码
FBoolContextProperty
└─ 继承 FChooserParameterBoolBase

这个父类提供了"Bool 参数"的统一接口,例如:

复制代码
virtual bool GetValue(
    FChooserEvaluationContext& Context,
    bool& OutResult
) const;

因此 Bool Column 不需要知道具体参数是:

复制代码
Context Property
Literal Value
Proxy Table Parameter
其他 Bool 数据源

它只需要面对共同基类:

复制代码
FChooserParameterBoolBase
└─ 你能不能给我一个 bool?

具体派生类再决定 bool 从哪里来。

Set the Proxy Asset's Type to the C++ base class of the asset object that this proxy is expected to resolve to.

关系是:

复制代码
Animation Blueprint
└─ UAnimBlueprint
   └─ 保存和编译动画逻辑
      └─ AnimGraph 节点需要读取某个动画资产对象
Proxy Asset
└─ 只是一个间接引用/查询键
└─ Type = 这个查询最终允许返回什么类型的对象
└─ 此例:AnimSequenceBase

所以这里发生的事情是:

复制代码
ABP 使用 Proxy Asset
        ↓
Proxy Asset 去 Proxy Table 中查询
        ↓
返回一个 AnimSequenceBase 类型范围内的动画资产
        ↓
交给 ABP 中的动画节点播放或处理

AnimSequenceBase 是引擎内部的 C++ 基类,不是你需要另外创建的一种 ABP:

复制代码
UAnimationAsset
└─ UAnimSequenceBase
   ├─ UAnimSequence
   └─ UAnimCompositeBase
      ├─ UAnimComposite
      └─ UAnimMontage

ABP 在另一棵继承树上:

plaintext 复制代码
UObject
└─ UBlueprint
   └─ UAnimBlueprint
cpp 复制代码
class UAnimBlueprint : public UBlueprint

而:

cpp 复制代码
class UAnimSequenceBase : public UAnimationAsset

ABP 甚至不是 UAnimationAsset 的子类

  • UAnimBlueprint:动画逻辑程序。

  • UAnimSequenceBase:可被逻辑引用和播放的动画数据对象。

  • UProxyAsset:把一个抽象键间接解析成动画数据对象。

Type 明确定义成 TObjectPtr<UClass>

AnimSequenceBase 不是 ABP 的额外类型,而是 Proxy Asset 承诺返回给 ABP 的动画资产对象类型。

没有继承的普通 Struct 通常意味着什么

假设你看到:

cpp 复制代码
USTRUCT()
struct FChooserPropertyBinding
{
    GENERATED_BODY()
UPROPERTY()
TArray&lt;FName&gt; PropertyBindingChain;
UPROPERTY()
int32 ContextIndex = 0;
};

它没有:

cpp 复制代码
: public SomeBase

那么它通常更倾向于:

plaintext 复制代码
纯数据结构
值对象
配置记录
中间描述

没有加入某个父类定义的多态体系。

假设写:

cpp 复制代码
FBoolContextProperty InputValue;

这意味着 InputValue 的具体类型已经在编译时固定:

plaintext 复制代码
这个位置永远只能装 FBoolContextProperty

不能后来改装:

不能后来改装:

这个位置永远只能装 FBoolContextProperty

而 Chooser Column 希望一个字段能够装不同派生参数类型,所以它使用:

复制代码
C++ FInstancedStruct InputValue;

可以先把 FInstancedStruct 理解成:

一个能够在运行时保存"某种具体 UScriptStruct 类型 + 该类型的一份实例数据"的通用盒子。

std::cout << "Hello";

等价于某种:

operator<<(std::cout, "Hello");

所以编译器会寻找适用于:

复制代码
FArchive 与 TObjectPtr<UScriptStruct>

operator<<

概念上等价于:

复制代码
C++ SerializeValue(Ar, SerializedScriptStruct);

但 UE 选择了 C++ 的流式写法:

复制代码
C++ Ar << Value;

其语义不是固定的"写出"。

它取决于 Archive 当前模式:

复制代码
Ar.IsSaving() == true └─ Value → Archive
Ar.IsLoading() == true └─ Archive → Value

大概率是位移

Ar << Value;

左边是 FArchive

复制代码
是序列化运算符

Writer << Text;

如果 Writer 是某种流或 Archive:

复制代码
通常是写入/读取协议

所以不要只根据符号判断,要先问:

左操作数是什么类型?

C++ 运算符的意义,是由操作数类型共同决定的。

位或:

复制代码
A | B

逐 bit 合并。

UE Flags 中大量使用位或:

复制代码
CPF_Transient |
CPF_DuplicateTransient |
CPF_SkipSerialization

这是把多个独立 flag 合并成一个 bitmask。

复制代码
if (PropertyFlags & SkipFlags)

这里的 & 是按位与:

复制代码
PropertyFlags 中是否至少包含一个需要跳过的 bit

unversioned property serialization 到底和普通方式差什么

这里的 versioned 不是指:

复制代码
UE 5.7
UE 5.8

也不是简单指 Git 版本。

它更接近:

每个属性值旁边是否附带足够的属性身份描述,让加载器可以按名字和类型匹配。

Tagged / versioned 方式

概念上保存成:

复制代码
Property
├─ Name = "ContextIndex"
├─ Type = IntProperty
├─ Size = 4
└─ Value = 0
Property
├─ Name = "PropertyBindingChain"
├─ Type = ArrayProperty<FName>
├─ Size = ...
└─ Value = [...]

也就是每个属性带标签。

加载时可以说:

复制代码
我读到一个叫 ContextIndex 的字段
└─ 去当前 Struct 中寻找同名 FProperty

优点:

复制代码
字段顺序变化时更有适应性
某些字段新增/删除时更容易跳过
能做属性重定向
更适合编辑器资产和版本演进

缺点:

复制代码
每个字段都保存名称、类型、尺寸等额外信息
文件更大
读取处理更复杂

Unversioned 方式

Unversioned 不再给每个值都写完整属性标签。

它更依赖一个已知的 schema:

复制代码
这个 Class/Struct 的第 0 个可序列化属性是什么
第 1 个是什么
第 2 个是什么

文件里更像:

复制代码
Header
├─ 哪些字段存在
└─ 哪些字段与默认值不同
Values
├─ 第 0 个字段的值
├─ 第 3 个字段的值
└─ 第 6 个字段的值

它不会为每个值重复写:

复制代码
Property Name
Property Type
Property Size

所以更紧凑、更快。

但它要求:

复制代码
保存端和加载端对同一 schema 有一致理解

可以类比:

Tagged

复制代码
{
  "ContextIndex": 0,
  "IsBoundToRoot": false
}

字段自己带名字。

Unversioned

复制代码
Schema:
0 = ContextIndex
1 = IsBoundToRoot
Data:
[0, false]

数据本身没有重复写名字,必须配合 schema 才能解释。


为什么叫 unversioned

它不是"完全不考虑版本"。

准确说是:

复制代码
不依靠每个 Property Tag 自描述来支持版本化解释
而是依靠外部 schema/layout 信息

UE 源码中分支是:

复制代码
if (Ar.UseUnversionedPropertySerialization())
{
    SerializeUnversionedProperties(...);
}
else
{
    SerializeVersionedTaggedProperties(...);
}

两者都在序列化 UPROPERTY。

区别不是:

复制代码
一个保存,一个不保存

而是:

复制代码
同样的数据,编码格式不同

FProperty 是不是系统都用的大概念

是。FProperty 是 CoreUObject 反射系统里极其核心的统一概念。

但要精确说:

FProperty 不是变量里的实际值,而是 UE 对"这个变量是什么、在哪里、怎样处理"的运行时描述对象。

例如 C++ 里写:

复制代码
UPROPERTY(EditAnywhere)
float MaxSpeed;

对象实例中真正保存的是:

复制代码
某个地址上的 4 字节 float 值
例如 600.0

FFloatProperty 描述的是:

复制代码
字段名 = MaxSpeed
字段类型 = float
字段在对象内偏移 = 0x1A8
字段大小 = 4
字段 flags = EditAnywhere ...
怎样构造
怎样复制
怎样序列化
怎样导入导出文本

关系是:

复制代码
C++ 成员变量
└─ 实例数据
FProperty
└─ 成员变量的类型和访问说明

UE 的类型树大致是:

复制代码
FField
└─ FProperty
   ├─ FBoolProperty
   ├─ FIntProperty
   ├─ FFloatProperty
   ├─ FDoubleProperty
   ├─ FNameProperty
   ├─ FStrProperty
   ├─ FObjectProperty
   ├─ FClassProperty
   ├─ FStructProperty
   ├─ FArrayProperty
   ├─ FMapProperty
   └─ FSetProperty

源码里:

复制代码
class FProperty : public FField

并保存:

复制代码
int32 ArrayDim;
EPropertyFlags PropertyFlags;
int32 Offset_Internal;
FProperty* PropertyLinkNext;

这几个字段已经显示其核心责任:

复制代码
ArrayDim
└─ 固定数组有多少元素
PropertyFlags
└─ Edit、Transient、Replicated 等属性规则
Offset_Internal
└─ 在宿主对象/Struct 内存中的位置
PropertyLinkNext
└─ 串入宿主类型的属性链

哪些系统会使用 FProperty

不是所有 UE 子系统都必须直接操作 FProperty,但大量上层系统建立在它之上:

复制代码
Serialization
└─ 按 FProperty 保存/加载值
Garbage Collection
└─ 找 UObject 引用属性
Editor Details Panel
└─ 根据 FProperty 类型生成控件
Blueprint
└─ 把反射属性暴露为变量和 Pin
Replication
└─ 识别需要网络复制的属性
Config
└─ 读写 Config 属性
Property Binding
└─ 按名称寻找字段并计算地址
Copy/Paste
└─ ExportText / ImportText
Diff
└─ 比较属性值
SaveGame
└─ 按 flags 过滤要保存的属性

仅写

复制代码
USTRUCT()
struct FMyStruct
{
};

却没有 GENERATED_BODY(),通常不能正常成为完整 UE 反射 Struct,UHT 也可能直接报错。

更准确地说:

复制代码
USTRUCT
└─ 向 UHT 声明:这个 C++ struct 要成为 UE 反射类型
GENERATED_BODY
└─ 把 UHT 生成的注册代码嵌入这个 struct
.generated.h
└─ 提供 UHT 为本文件生成的声明和宏内容

DisplayName

主要是编辑器 metadata。

C++ 类型名是:

复制代码
FBoolContextProperty

但编辑器界面显示:

复制代码
Bool Property Binding

它不会改变 C++ 类型身份。

编译器 / 运行时类型身份 └─ FBoolContextProperty 面向编辑器用户的显示文字 └─ Bool Property Binding

现代英语中,retarded 作为形容词指人的时候:

这是一个强烈的侮辱性用语,通常指智力障碍人士。

过去医学上曾使用:

  • mental retardation

但现在已经被更尊重的术语取代:

  • intellectual disability

    智力障碍

技术文档里看到 retarded 时,一般只取"延迟的、减慢的"意思,不带侮辱含义。比如:

  • retarded update

    = 延迟更新

  • retarded response

    = 延迟响应

打 Uber 车)。

Uber 在这里被当作一个可数名词,表示:

an Uber car / an Uber ride(一次 Uber 出行)

而不是公司名字本身。

She is a professional ballerina.

The ballerina performed on stage.

ballerina = 从意大利语 ballerina

ballerino

男性芭蕾舞者(较少用)

ballet dancer

芭蕾舞演员(更中性,可以指男性或女性)

ripped = very muscular and with clearly visible muscles

buff = 壮、有肌肉(口语)

be into something

She's really into fashion.

  • 他自己

  • 自己的利益

  • He only cares about his own ass.

    他只关心自己。

  • He is obsessed with his own ass.

    他非常自恋。

his own ass

  • He's doing crunches to build his abs.

    他正在做卷腹来练腹肌。

  • I do 50 crunches every morning.

    我每天早上做 50 个卷腹。

动作 主要特点
crunch 小幅度卷起上半身,重点刺激腹肌
sit-up 坐起幅度更大,涉及髋屈肌更多
plank 静态保持,训练核心稳定

表示"这很有态度"

例如有人穿一件非常夸张的衣服:

  • That outfit is a statement.

monogamy /məˈnɑː.ɡə.mi/

意思:

一夫一妻制;单偶制;只与一个伴侣建立婚姻或长期关系。

词源:

  • mono- = one(一个)

  • -gamy = marriage(婚姻)

  • polygamy /pəˈlɪɡ.ə.mi/

    多配偶制(一个人同时有多个配偶)

    • poly- = many(多个)
  • bigamy /ˈbɪɡ.ə.mi/

    重婚(一人在已有婚姻关系时又与另一人结婚)

  • Scientists study exotic phenomena in space.

    科学家研究太空中的奇异现象。

  • Quantum physics deals with many exotic phenomena.

    量子物理涉及许多奇特现象。

exotic 不一定表示"外国的",更多表示:

different from what is normally expected(不同于通常情况)

  • exotic particles

    奇异粒子(不是普通粒子)

  • exotic matter

    奇异物质(物理学中的特殊假设物质)

  • statute = 法令、成文法、法规

  • statutory = 与法令有关的、法律规定的

hooters 这个词本身还有其他含义(比如俚语里指女性胸部,或猫头鹰的叫声),但大写 Hooters 通常指餐厅品牌。

DUI 是缩写:

Driving Under the Influence

通常指:

  • 酒精影响驾驶(最常见)

  • drug或其他影响驾驶能力的物质导致驾驶

Sobriety /səˈbraɪ.ə.ti/ 是名词,意思:

清醒;戒酒状态;不受酒精或毒品影响的状态。

它来自形容词 sober

  • sober = 清醒的;没有醉的;理智的

  • sobriety = 清醒状态

常见用法:

  • He has maintained sobriety for five years.

    他已经保持清醒(戒酒)五年了。

  • a sobriety test

    清醒测试(例如警方检查驾驶者是否受酒精影响)

  • sobriety checkpoint

    酒驾检查站

Tase /teɪz/ 是一个动词,意思:

用电击枪(Taser)电击;用泰瑟枪制服。

它来自品牌名 Taser(一种电击武器)。

  • Taser (名词)

    电击枪品牌名,也常被泛指为电击枪。

  • stun gun

    电击器(更广泛的说法)

  • electroshock

    电击(更正式/技术用语)

在现代英语里,consent 经常用于强调:

某件涉及个人权利的事情,需要明确、自愿的同意。

例如:

  • Consent must be voluntary.

    同意必须是自愿的。

juggling = 同时应付、兼顾多个任务

例如:

  • She is juggling work and family.

    她同时兼顾工作和家庭。

  • I'm juggling several projects at the same time.

    我同时处理几个项目。

画面来自杂耍:

复制代码
ball 1 ─┐
ball 2 ─┼── constantly keeping many things in the air
ball 3 ─┘

对应现实:

复制代码
task 1 ─┐
task 2 ─┼── simultaneously managing many responsibilities
task 3 ─┘
  • wear = 穿戴、佩戴(表示"处于佩戴状态")

  • contacts = contact lenses 的简称,隐形眼镜

wear 不只是"穿衣服",它可以表示长期佩戴任何东西:

  • wear glasses = 戴眼镜

  • wear a watch = 戴手表

  • wear earrings = 戴耳环

  • wear a helmet = 戴头盔

pharyngeal /fəˈrɪn.dʒi.əl/

意思:

咽的;咽部的(与喉咽、咽腔有关)。

它来自:

  • pharynx /ˈfer.ɪŋks/ = 咽(连接口腔、鼻腔和喉部的通道)

  • pharyngeal = 与咽有关的

例如:

  • pharyngeal muscles

    = 咽部肌肉

  • pharyngeal cavity

    = 咽腔

  • pharyngeal sound

    = 咽音(发音学术语)

UCLASS + UObject 继承 └─ UE 对象类型 USTRUCT + 不继承 UObject └─ UE 可反射的 C++ 值类型

UStruct ├─ UClass ├─ UFunction └─ UScriptStruct

UScriptStruct 里的 Script 到底是什么意思

源码对 UScriptStruct 的注释:

UScriptStruct represents a structure exported to UnrealScript, and is the C++ version of a UStruct.

这里的 Script 指的是 UnrealScript(UE3 及之前的脚本语言)。

在 UE3 时代,UnrealScript 是游戏逻辑的主要编写语言。UE4 之后虽然用 C++ 和蓝图替代了 UnrealScript,但 UScriptStruct 这个名字保留了下来。

所以 UScriptStruct 的字面意思是:

曾经被 UnrealScript 使用的结构体类型

现在它泛指所有被 UHT(Unreal Header Tool)处理过的结构体,即所有用 USTRUCT() 宏标记的结构体。

UScriptStruct 和 UStruct 的关系

继承链:

cpp 复制代码
UObject
 └─ UField
     └─ UStruct
         └─ UScriptStruct

UStruct 是 UE 反射系统中所有"结构类型"的基类,包括:

  • UClass(类)
  • UScriptStruct(结构体)
  • UFunction(函数)
  • UEnum(枚举)

UScriptStruct 专门用于描述用 USTRUCT() 标记的 C++ 结构体。

UScriptStruct 的核心成员

UScriptStruct 继承自 UStruct,主要新增了以下内容:

StructFlags

cpp 复制代码
EStructFlags StructFlags;

结构体标志位,例如:

  • STRUCT_NoExport --- 不导出到其他语言
  • STRUCT_Native --- 原生 C++ 结构体
  • STRUCT_IdenticalNative --- 使用原生 Identical 函数
  • STRUCT_SerializeNative --- 使用原生 Serialize 函数
  • STRUCT_Atomic --- 原子类型(可被 memcpy)

CppStructOps

cpp 复制代码
TSharedPtr<ICppStructOps> CppStructOps;

这是一个指向原生 C++ 结构体操作函数的指针,包含:

  • 构造函数 / 析构函数
  • 拷贝赋值
  • 序列化 / 反序列化
  • 比较操作

如果 CppStructOps 为空,UE 会使用默认的 memcpy 方式处理。

UnrealScript 遗留成员

cpp 复制代码
// UE3 时代遗留,现在基本不用
TArray<uint8> Script;

这个 Script 成员变量(注意和类名里的 Script 不是一回事)在 UE3 时代用于存储 UnrealScript 字节码,现在基本废弃。

UScriptStruct 的用途

在 UE5 中,UScriptStruct 主要用于:

  1. 反射系统 --- 让 C++ 结构体在运行时可以被枚举、序列化
  2. 蓝图通信 --- 结构体可以在蓝图中作为变量类型使用
  3. 网络复制 --- 结构体可以参与网络同步
  4. 序列化 --- 结构体可以保存/加载到文件
  5. 细节面板 --- 结构体属性可以在编辑器中显示和编辑

UScriptStruct 和 FStruct 的关系

很多人会混淆这两个概念:

  • UScriptStruct --- 是 UE 反射系统中的类,描述结构体的元信息
  • FStruct(如 FVector) --- 是实际的 C++ 结构体类型

关系类似于:

UScriptStruct 是结构体的"类型描述",FVector 是结构体的"具体实现"。

每个用 USTRUCT() 标记的结构体,在运行时都有一个对应的 UScriptStruct 对象。

总结

  • UScriptStruct 名字里的 Script 来自 UnrealScript(UE3 脚本语言)
  • 它是 UStruct 的子类,专门描述 USTRUCT() 标记的结构体
  • 它包含结构体的反射信息、标志位、原生操作函数等
  • 它和 UClass 是同一套反射机制的不同侧面
  • 不要混淆 UScriptStruct(类名)和 UScriptStruct::Script(成员变量名)

StaticStruct() 又是谁

对一个 UHT 识别的 USTRUCT:

cpp 复制代码
USTRUCT()
struct FExample
{
    GENERATED_BODY()
    UPROPERTY()
    int32 Value;
};

UHT 会生成一个 StaticStruct() 静态成员函数。

这个函数返回一个 UScriptStruct*,指向运行时注册好的结构体描述对象。

调用方式:

cpp 复制代码
UScriptStruct* Struct = FExample::StaticStruct();

它返回的 UScriptStruct* 是一个运行时对象,包含:

  • 结构体名称(FName)
  • 属性列表(FProperty 链表)
  • 结构体大小(sizeof)
  • 序列化 / 反序列化函数指针
  • 结构体对齐信息

这个对象在模块加载时由 __InitImp 注册函数创建并缓存。

所以 StaticStruct() 是 UHT 为每个 USTRUCT 生成的静态成员函数,返回一个全局唯一的 UScriptStruct* 运行时对象。

它和 StaticClass() 是同一套机制------一个是给 UCLASS 用的,一个是给 USTRUCT 用的。

UHT 生成的代码大致如下:

cpp 复制代码
// 生成代码中
UScriptStruct* FExample::StaticStruct()
{
    static UScriptStruct* Singleton = nullptr;
    if (!Singleton)
    {
        // 创建并注册 UScriptStruct 对象
        Singleton = NewObject<UScriptStruct>(...);
        // 添加属性
        Singleton->AddCppProperty(...);
        // 注册到引擎
        Singleton->Register();
    }
    return Singleton;
}

注意:StaticStruct() 本身是静态成员函数,没有 this 指针。它不依赖某个 FExample 实例就能调用。

StaticStruct() 内部访问的 Singleton 是一个函数内的 static 局部变量,具有静态存储期,在首次调用时初始化。

所以 StaticStruct() 返回的 UScriptStruct* 指向一个全局唯一的运行时对象,这个对象在进程生命周期内只创建一次。

StaticStruct() 返回的是运行时对象

很多人会混淆:

StaticStruct() 返回的是一个 UScriptStruct*,它是一个真正的 C++ 对象,存在于堆上(或静态存储区)。

这个 UScriptStruct 对象本身也是 C++ object(按 C++ 标准定义)。

C++ 标准中的 object 定义:

"一段具有类型、大小、存储位置和生命周期的存储"

所以:

cpp 复制代码
int X;
float Y;
FVector V;
UMyObject* Pointer;

XYVPointer 都是 C++ object。

但不能说:

int 本身是一个 object。

应该说:

int 是 object type;某个 int 变量是 object。

cpp 复制代码
UMyObject::StaticClass()

最终得到的是一个真正存在于当前进程内存里的:

cpp 复制代码
UClass instance

这个 UClass 本身也是运行时对象。

StaticStruct() 中的 static

cpp 复制代码
static UScriptStruct* StaticStruct();

这是:

静态成员函数

它只表示:

不依赖某个 FMyMovementData 实例

它本身并不保存那个 UScriptStruct 对象。

生成代码中的 static 变量

生成代码还会在别处产生类似:

cpp 复制代码
static FStructRegistrationInfo
    Z_Registration_Info_UScriptStruct_FMyMovementData;

这是:

真正拥有静态存储期的数据变量

它内部有:

cpp 复制代码
UScriptStruct* InnerSingleton = nullptr;
UScriptStruct* OuterSingleton = nullptr;

UE 5.8 实际定义位于:

复制代码
Engine/Source/Runtime/CoreUObject/Public/UObject/UObjectBase.h
约 513--522 行

源码:

cpp 复制代码
template <typename T, typename V>
struct TRegistrationInfo
{
    using TType = T;
    using TVersion = V;
    TType* InnerSingleton = nullptr;
    TType* OuterSingleton = nullptr;
    TVersion ReloadVersionInfo;
};

FMyMovementData::StaticStruct() 调用链:

  • 静态成员函数,没有 this
  • 调用生成的注册函数
  • 访问一个静态 FStructRegistrationInfo 变量
  • 里面缓存 UScriptStruct 指针

StaticStruct() 不是那个 singleton 对象本身。StaticStruct() 是取得 singleton 对象的入口函数。

虚幻引擎(UE)中结构体以 F 开头,源自其创始人 Tim Sweeney 早期的代码习惯。F 最初代表 "Float"(浮点数),用于区分浮点数数学类。随着引擎发展,F 演变成原生 C++ 类和结构体的通用前缀。

相关推荐
AOwhisky1 小时前
Python 学习笔记(第四期)——字符串:格式化、操作与文本处理——核心知识点自测与详解
开发语言·笔记·python·学习·列表·元组·字典
光学补偿2 小时前
微服务学习 --- Spring Cloud概述
学习·spring cloud·微服务
AOwhisky2 小时前
Python 学习笔记(第六期)——函数:定义、参数传递与作用域
笔记·python·学习·云原生·运维开发·函数·参数传递
网小鱼的学习笔记2 小时前
Agent学习001
学习·迁移学习
白帽小阳3 小时前
我的AI辅助开发工具链2026版:从编码到部署的全栈智能实践
网络·人工智能·学习·安全·自动化
有Li3 小时前
通过渐进式域变换实现组织分类的无监督单域泛化文献速递/基于多模态的医学影像分割与理解
人工智能·学习·分类·数据挖掘·文献·医学生
kdxiaojie3 小时前
Linux 驱动研究 —— V4L2 (2)
linux·运维·笔记·学习
神秘的MT4 小时前
SQL Server 分页查询 ROW_NUMBER () OFFSET...FETCH游标(Cursor) 数据插入
数据库·sql·学习·c#·winform
~kiss~4 小时前
大模型的PEFT~ 参数高效微调 Parameter-Efficient Fine-Tuning
学习