BPVM 小食包 #9 - 变量变成属性:转变过程
当你在蓝图中创建一个变量时,它还不是真正的变量。它只是一个等待变成真正属性的描述。这就是它的蜕变过程。
BPVM 小食包 #9 - 变量变成属性:转变过程
本文内容基于Unreal Engine 5.6.0
BPVM 小食包 - 蓝图知识快速投喂!是蓝图到字节码系列的一部分。
变量的假象
在蓝图编辑器中,你点击”+变量”并创建 Health:
![蓝图编辑器中的变量创建]
你以为刚刚创建了一个变量。并没有。
你创建的是一个变量的描述。真正的变量还不存在!
认识 FBPVariableDescription
当你创建一个蓝图变量时,实际存储的是这个:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct FBPVariableDescription
{
FName VarName; // "Health"
FEdGraphPinType VarType; // Float
FString Category; // "Stats"
uint64 PropertyFlags; // EditAnywhere, BlueprintReadWrite, 等等
// 元数据
FString Tooltip; // "玩家的当前生命值"
FName RepNotifyFunc; // "OnRep_Health"
// 还不是真正的属性!
};
它只是关于变量的数据,不是变量本身!
编译转换
在编译期间,这些描述变成了真正的属性:
1
2
3
4
5
6
7
8
9
10
11
void CreateClassVariablesFromBlueprint()
{
// 遍历所有变量描述
for (FBPVariableDescription& Variable : Blueprint->NewVariables)
{
// 将描述转换为真正的属性!
FProperty* NewProperty = CreateVariable(Variable.VarName, Variable.VarType);
// 现在它是类上的真实属性了!
}
}
属性的诞生
这是神奇的时刻:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FProperty* CreateVariable(FName VarName, FEdGraphPinType& VarType)
{
// 确定属性类型
if (VarType.PinCategory == "Float") {
// 创建真实的浮点数属性
FFloatProperty* NewProp = new FFloatProperty(
NewClass, // 所有者类
VarName, // "Health"
RF_Public // 标志
);
// 它活了!真实的内存将被分配!
return NewProp;
}
}
为什么要两步流程?
为什么不立即创建真实属性?
1. 编辑器性能
1
2
3
4
5
6
7
8
9
10
11
12
// 坏做法:每次编辑都创建真实属性
点击 +变量 → 分配内存
输入名称 → 重新分配
更改类型 → 再次重新分配
设置工具提示 → 再次重新分配
// 好做法:只更新描述
点击 +变量 → 创建描述
输入名称 → 更新字符串
更改类型 → 更新枚举
设置工具提示 → 更新字符串
// 只在编译时创建真实属性!
2. 热重载安全
1
2
3
4
5
6
// 编辑期间(安全)
VariableDescription.VarName = "NewName"; // 只是数据
// 编译期间(小心!)
OldProperty->Destroy();
NewProperty = CreateProperty("NewName"); // 真实的内存操作
3. 先验证
1
2
3
4
5
6
// 创建属性之前检查所有描述
for (auto& Desc : Variables) {
if (IsDuplicate(Desc)) return; // 造成破坏前就停止!
if (IsInvalid(Desc)) return;
}
// 一切正常?现在创建真实属性
属性创建流水线
步骤 1: 收集描述
1
2
3
4
TArray<FBPVariableDescription> Descriptions;
Descriptions.Add("Health", Float);
Descriptions.Add("Armor", Int32);
Descriptions.Add("Name", String);
步骤 2: 按大小排序(优化!)
1
2
3
4
// 大属性在前,以获得更好的内存对齐
Descriptions.Sort([](auto& A, auto& B) {
return GetSize(A) > GetSize(B);
});
步骤 3: 创建真实属性
1
2
3
4
5
6
7
8
9
10
11
for (auto& Desc : Descriptions) {
FProperty* Prop = CreatePropertyOnScope(
NewClass, // 它生活的地方
Desc.VarName, // 它的名字
Desc.VarType // 它的类型
);
// 配置属性
Prop->SetPropertyFlags(Desc.PropertyFlags);
Prop->SetMetaData("Tooltip", Desc.Tooltip);
}
步骤 4: 链接到类
1
2
3
4
5
6
// 添加到类的属性链
NewClass->AddCppProperty(NewProperty);
// 计算内存偏移
NewProperty->Offset = CurrentOffset;
CurrentOffset += NewProperty->ElementSize;
特殊属性类型
一些变量需要额外的转换:
时间轴变量:
1
2
3
4
5
6
7
// 你在编辑器中创建一个时间轴
"MyTimeline"
// 编译器创建多个属性:
FTimelineComponent* MyTimeline; // 组件
FOnTimelineFloat MyTimeline_UpdateFunc; // 委托
FOnTimelineEvent MyTimeline_FinishFunc; // 委托
组件变量:
1
2
3
4
5
6
7
// 你添加一个组件变量
"MyMeshComp" (StaticMeshComponent)
// 编译器做额外的工作:
CreateComponentProperty("MyMeshComp");
RegisterComponent("MyMeshComp");
SetupComponentDefaults("MyMeshComp");
内存布局
所有属性创建完成后:
1
2
3
4
5
6
7
8
9
10
class BP_MyActor {
// 内存布局(按大小排序!)
0x0000: UStaticMeshComponent* MyMesh; // 8 字节
0x0008: FString Name; // 16 字节 (TArray)
0x0018: float Health; // 4 字节
0x001C: int32 Armor; // 4 字节
0x0020: bool bIsAlive; // 1 字节
0x0021: [填充] // 7 字节
// 总大小: 0x0028 (40 字节)
}
编译器优化布局以提高缓存效率!
快速要点
- 蓝图变量从 FBPVariableDescription 开始(只是元数据)
- 在编译期间,它们变成 FProperty 对象(真实内存)
- 这个两步流程实现了安全编辑和热重载
- 属性按大小排序以获得最佳内存布局
- 特殊类型(Timeline、Component)创建多个属性
- 转换发生在
CreateClassVariablesFromBlueprint()中
从描述到现实
下次你在蓝图中创建变量时,请记住:
- 你正在创建一个描述,而不是变量
- 真正的属性在编译期间诞生
- 两步流程使编辑器保持快速和安全
- 你的”简单”变量可能创建多个属性!
想要更多细节?
完整的属性创建分解:
接下来:函数是如何制造的!
🍿 BPVM 小食包系列
- ← #8: 清理和净化魔法
- #9: 变量变成属性 ← 你在这里
- #10: 函数工厂 →
本文由作者按照
CC BY 4.0
进行授权