BPVM 小食包 #6 - CDO 之谜:你的类的秘密模板 | ebp BPVM 小食包 #6 - CDO 之谜:你的类的秘密模板 | ebp BPVM 小食包 #6 - CDO 之谜:你的类的秘密模板 | ebp
文章

BPVM 小食包 #6 - CDO 之谜:你的类的秘密模板

每个蓝图类都有一个没人谈论的隐藏模板对象。认识 CDO——定义'默认'真正含义的神秘实例。

BPVM 小食包 #6 - CDO 之谜:你的类的秘密模板

本文内容基于Unreal Engine 5.6.0

BPVM 小食包 - 快速蓝图知识投放蓝图到字节码系列的一部分。

神秘对象

你创建了一个蓝图类。你还没有生成任何实例。但惊喜——一个实例已经存在

它叫做类默认对象 (Class Default Object, CDO),自从你的类加载以来,它一直静静地存在于内存中。

CDO 是什么?

把 CDO 想象成你的类的主模具:

1
2
3
4
5
// 当你创建 BP_MyActor...
UClass* MyClass = BP_MyActor::StaticClass();

// 这已经存在了!
AActor* CDO = MyClass->GetDefaultObject();  // 秘密实例

CDO 是:

  • 你的类的真实实例(它是内存中的实际对象!)
  • 类加载时自动创建
  • 从不在世界中生成(它存在于虚无中)
  • 所有未来实例的模板

为什么每个类都需要一个?

问题: 当你生成一个 actor 时,它的默认值从哪里来?

糟糕的解决方案: 将默认值存储为元数据

1
2
3
4
5
6
// 想象中的糟糕设计
class ClassMetadata {
    float DefaultHealth = 100;
    FString DefaultName = "Player";
    // 数百个属性...
};

虚幻的解决方案: 只需创建一个”完美”实例并从中复制!

1
2
3
4
5
6
7
// CDO 就是默认值
AActor* CDO = GetDefault<AActor>();
CDO->Health = 100;  // 设置一次
CDO->Name = "Player";

// 生成从 CDO 复制
AActor* NewActor = SpawnActor();  // 从 CDO 复制所有属性

魔法时刻

当你在蓝图编辑器中编辑”默认”值时:

Blueprint Editor showing default values

你不是在编辑元数据。你在直接编辑 CDO!

1
2
3
4
5
// 在蓝图编辑器中,当你设置 Health = 100 时
CDO->Health = 100;  // 你确实在 CDO 上设置属性

// 稍后,生成时
NewInstance->Health = CDO->Health;  // 从 CDO 复制

CDO 在行动

这是生命周期:

1. 类创建

1
2
3
4
5
// 蓝图被编译
UBlueprintGeneratedClass* NewClass = CompileBlueprint();

// CDO 立即创建
UObject* CDO = NewClass->GetDefaultObject();

2. 设置默认值

1
2
3
4
// 你在蓝图编辑器中编辑
CDO->MaxHealth = 150;
CDO->TeamColor = FColor::Red;
CDO->WeaponClass = AK47::StaticClass();

3. 实例创建

1
2
3
4
5
6
7
// 玩家生成你的 actor
AActor* Instance = World->SpawnActor<AActor>(BP_MyActor);

// 底层:
// 1. 分配内存
// 2. 从 CDO 复制所有属性
// 3. 运行构造函数

恢复按钮之谜

曾经想知道”恢复到默认值”按钮是如何工作的吗?

它只是与 CDO 比较:

1
2
3
4
5
6
bool IsModified = (Instance->Health != CDO->Health);
// 如果为真,显示黄色恢复按钮

void RevertToDefault() {
    Instance->Health = CDO->Health;  // 只是从 CDO 复制!
}

CDO vs 构造函数默认值

C++ 构造函数:

1
2
3
AMyActor::AMyActor() {
    Health = 100;  // 每次生成都运行
}

CDO 系统:

1
2
3
4
5
// 在 CDO 上设置一次
CDO->Health = 100;

// 生成只是复制内存(更快!)
memcpy(NewInstance, CDO, sizeof(AActor));

CDO 方法对于生成许多实例快得多!

隐藏的 CDO 生命周期

在编译期间:

1
2
3
4
5
6
7
8
9
10
11
12
13
void CompileBlueprint() {
    // 旧 CDO 仍然有玩家配置的默认值
    UObject* OldCDO = OldClass->GetDefaultObject();

    // 清理类
    CleanAndSanitizeClass(OldClass);

    // 重新编译一切
    CompileClass(NewClass);

    // 从旧 CDO 复制默认值到新 CDO!
    CopyPropertiesFrom(OldCDO, NewCDO);
}

这就是为什么你的默认值在重新编译后仍然存在!

CDO 陷阱

1. CDO 在编辑器和运行时都存在

1
2
3
4
5
// 在编辑器中
CDO->SomeProperty = 10;  // 编辑默认值

// 在打包的游戏中
CDO->SomeProperty;  // 仍然是 10!(现在只读)

2. 永远不要在运行时修改 CDO

1
2
// 不要在游戏代码中这样做!
CDO->Health = 200;  // 你刚刚改变了所有未来生成的默认值!

3. CDO 和热重载

1
2
3
4
// 在热重载期间
OldCDO->SaveDefaults();
RecompileClass();
NewCDO->RestoreDefaults();  // 你的设置保留了!

快速要点

  • 每个类都有一个 CDO (类默认对象) - 一个隐藏的模板实例
  • 当你在蓝图中编辑默认值时,你在编辑 CDO
  • 生成 actor 从 CDO 复制属性(快!)
  • CDO 在重新编译中存活(这就是为什么默认值持久)
  • 永远不要在运行时修改 CDO(它影响所有未来的生成)

CDO 无处不在

下次你:

  • 在蓝图中设置默认值
  • 点击恢复按钮
  • 生成一个 actor
  • 重新编译蓝图

记住:你在与 CDO 交互,这个秘密模板对象使虚幻的类系统工作!

想要更多细节?

有关代码的完整解释:

下一个:节点处理器如何将你的图表变成代码!


🍿 BPVM 小食包系列

本文由作者按照 CC BY 4.0 进行授权