BPVM 小食包 #5 - SuperStruct: 基于指针的继承
蓝图类不使用 C++ 继承。它们通过 SuperStruct 使用基于指针的系统。这里解释为什么这种设计很重要。
BPVM 小食包 #5 - SuperStruct: 基于指针的继承
本文内容基于Unreal Engine 5.6.0
C++ 继承 vs 蓝图继承
当你从 AMyActor 创建蓝图时,编辑器说你正在创建 AMyActor 的子类。
从 API 的角度来看这是对的——它表现像一个子类。但实现与 C++ 继承完全不同。
真正的 C++ 继承是什么样的
1
2
3
4
5
6
7
// 真正的 C++ 继承
class AMyChildActor : public AMyActor // ✅ 真正的继承
{
// 编译器创建 vtable
// 内存布局包括父级的数据
// 链接器解析函数地址
};
使用真正的继承:
- 编译器在编译时将关系烘焙到二进制中
- vtable 是静态链接的
- 内存布局包括所有父级成员
- 一切都是静态解析的(快,但不灵活)
蓝图”继承”实际上是什么
1
2
3
4
5
6
7
8
// 蓝图的方法
class UBlueprintGeneratedClass : public UClass // 不是 AMyActor!
{
// 这是一个 UClass,不是你的 actor!
};
// 在编译期间的某个地方:
GeneratedClass->SetSuperStruct(AMyActor::StaticClass());
这是关键见解:
UBlueprintGeneratedClass从UClass继承(不是你的 actor!)- 它通过
SetSuperStruct()存储指向父级的指针 - 当你调用
GetSuperClass()时,它跟随那个指针
这是组合 + 委托,不是传统继承。
指针链
这是实际的关系:
1
2
3
4
5
6
7
8
9
10
11
12
UBlueprintGeneratedClass* GeneratedClass;
// |
// | SetSuperStruct()
// v
UClass* ParentClass = AMyActor::StaticClass();
// |
// | GetSuperClass()
// v
UClass* GrandParent = AActor::StaticClass();
// |
// v
UObject::StaticClass();
它是一个指针的链表,不是 C++ 继承!
为什么这很重要
问题 1: 属性查找
当你访问蓝图实例上的变量时:
1
2
// BP_MyActor 有变量 "Health"
float MyHealth = MyActor->Health;
底层:
- 在
GeneratedClass属性中查找Health - 没找到?跟随
SuperStruct指针到父级 - 重复直到找到或到达
UObject
这是运行时反射,不是编译时!
问题 2: 函数调用
当你调用函数时:
1
MyActor->Foo();
引擎:
- 检查
GeneratedClass是否覆盖Foo - 如果没有,跟随
SuperStruct链 - 在父类中找到函数
- 执行(可能是字节码或原生 C++)
再次,运行时查找!
好处
为什么使用指针而不是真正的继承?
1. 热重载
1
2
3
4
5
6
7
8
// 在游戏运行时重新编译蓝图
GeneratedClass->CleanAndSanitize(); // 清除旧数据
Compile(Blueprint); // 填充新数据
Reinstancer->UpdateInstances(); // 更新现有对象
// 仍然使用同一个 GeneratedClass 对象!
// 没有内存地址变化(有点...)
// 不需要指针修复
2. 动态类创建
1
2
3
4
// 在运行时创建蓝图类!
UBlueprint* NewBP = CreateBlueprint(...);
Compile(NewBP);
// 现在你有一个新的"类"
3. 循环依赖
1
2
3
4
5
BP_A->SetSuperStruct(BP_B); // A "继承自" B
BP_B->SetSuperStruct(BP_A); // 错误:会创建循环!
// 但指针系统可以检测到这一点
// 并创建骨架类作为中介
权衡
C++ 继承(快):
1
2
3
class Child : public Parent { };
// 编译时:vtable,内存布局
// 运行时:直接内存访问,无需查找
蓝图 SuperStruct(灵活):
1
2
3
Generated->SetSuperStruct(Parent);
// 编译时:没有烘焙进去
// 运行时:指针追逐,反射查找
蓝图用性能换取灵活性——经典的游戏开发权衡。
如何思考它
错误的心智模型:
1
BP_MyActor : public AMyActor // ❌ 不是正在发生的事
好的心智模型:
1
2
3
4
5
6
class BP_MyActor {
UClass* Parent = AMyActor::StaticClass(); // ✅ 指针关系
TArray<FProperty*> MyProperties;
TArray<UFunction*> MyFunctions;
TArray<uint8> Bytecode;
};
快速要点
- 蓝图类不使用 C++ 继承
- 它们使用
SetSuperStruct()/GetSuperClass()(指针链) - 这使得热重载和运行时类创建成为可能
- 权衡:更灵活,但比 C++ 继承慢
- 反射系统使它对开发者看起来像继承
抽象起作用
从你的蓝图代码来看,它的行为完全像继承:
1
2
3
// 在你的蓝图中,这就是有效的
Parent::MyFunction(); // 调用父级版本
Super::Tick(); // 调用父级 tick
但在底层,这都是指针追逐和反射查找。抽象非常好,以至于大多数开发者永远不需要知道区别。
想要更多细节?
有关代码的完整解释:
下一份小食:神秘的类默认对象 (CDO)!
🍿 BPVM 小食包系列
- ← #4: 骨架类
- #5: SuperStruct 魔法技巧 ← 你在这里
- #6: CDO 之谜 →
本文由作者按照
CC BY 4.0
进行授权