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 解析 UPROPERTY 和 meta,生成反射描述,然后 Property Editor 根据描述创建控件。
FAnimNode_Base 是 Unreal 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 保留;新机器不会沿用旧机器的内存连接,而是根据同样的类型和属性路径重新编译出连接。
可见内容中只有 GetDeltaSeconds 和 Blueprint_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
Typeto 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<FName> 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 主要用于:
- 反射系统 --- 让 C++ 结构体在运行时可以被枚举、序列化
- 蓝图通信 --- 结构体可以在蓝图中作为变量类型使用
- 网络复制 --- 结构体可以参与网络同步
- 序列化 --- 结构体可以保存/加载到文件
- 细节面板 --- 结构体属性可以在编辑器中显示和编辑
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;
X、Y、V、Pointer 都是 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++ 类和结构体的通用前缀。