BPVM 小食包 #4 - 骨架类:隐藏的英雄 | ebp BPVM 小食包 #4 - 骨架类:隐藏的英雄 | ebp BPVM 小食包 #4 - 骨架类:隐藏的英雄 | ebp
文章

BPVM 小食包 #4 - 骨架类:隐藏的英雄

当蓝图 B 还没编译时,蓝图 A 如何引用蓝图 B?骨架类——蓝图版本的前向声明。

BPVM 小食包 #4 - 骨架类:隐藏的英雄

本文内容基于Unreal Engine 5.6.0

BPVM 小食包 - 小口大小的蓝图知识蓝图到字节码系列的一部分。

循环依赖问题

这是多人游戏中的常见场景:

1
2
3
Blueprint_PlayerController 引用 Blueprint_GameMode
Blueprint_GameMode 引用 Blueprint_PlayerState
Blueprint_PlayerState 引用 Blueprint_PlayerController

经典的循环依赖。如何在不死锁的情况下编译这些?

C++ 方式(在这里不起作用)

在 C++ 中,你会使用前向声明:

1
2
3
4
5
6
class AMyGameMode;  // 前向声明

class AMyPlayerController : public APlayerController
{
    AMyGameMode* GameMode;  // 使用前向声明
};

但蓝图在运行时(或在编辑器中按需)编译。你不能只是”前向声明”一个蓝图!

解决方案:骨架类

虚幻通过两遍方法解决这个问题。在阶段 VIII (重新编译骨架)期间,它为每个蓝图类创建一个”骨架”版本:

1
2
3
4
5
6
7
8
9
10
11
12
// 骨架类:只有结构,没有实现
class BP_PlayerController_SKEL : public APlayerController
{
    // 有所有属性
    UPROPERTY()
    ABP_GameMode* GameMode;

    // 有所有函数签名
    void DoSomething();

    // 但还没有字节码!
};

把它想象成 C++ 中的头文件 (.h),但在编译时为蓝图生成。

它如何解决循环依赖

阶段 1 - 创建骨架:

1
2
3
4
// 对于每个蓝图,首先创建骨架
BP_PlayerController_SKEL  // 只是形状
BP_GameMode_SKEL          // 只是形状
BP_PlayerState_SKEL       // 只是形状

阶段 2 - 完整编译:

1
2
3
4
// 现在每个人都可以引用骨架!
BP_PlayerController 引用 BP_GameMode_SKEL 
BP_GameMode 引用 BP_PlayerState_SKEL 
BP_PlayerState 引用 BP_PlayerController_SKEL 

没有循环依赖!每个人都有东西可以引用。

骨架里有什么?

骨架类包含:

变量声明(带类型)

1
2
3
4
5
UPROPERTY()
float Health;  // 类型已知

UPROPERTY()
ABP_Enemy* Enemy;  // 类型已知

函数签名(参数和返回类型)

1
2
3
4
5
UFUNCTION()
void TakeDamage(float Amount);  // 签名已知

UFUNCTION()
float GetHealth();  // 返回类型已知

没有字节码(实际的函数实现)

1
2
3
4
5
// 函数存在但函数体是空的:
void TakeDamage(float Amount)
{
    // 这里还什么都没有!
}

两遍编译

这就是为什么蓝图编译分两个主要阶段进行:

第一遍 - 仅骨架(快速):

  • 创建类结构
  • 添加所有属性
  • 添加所有函数签名
  • 不生成字节码

第二遍 - 完整编译(较慢):

  • 为所有函数生成字节码
  • 填充实现细节
  • 更新所有实例

什么时候你会看到骨架?

你很少直接看到骨架类,但它们在幕后工作:

场景 1 - 打开蓝图:

1
2
3
4
OpenBlueprint(BP_MyActor);
// 快速骨架编译发生
// → 现在可以在编辑器中看到变量/函数
// 当你点击"编译"时进行完整编译

场景 2 - 循环引用:

1
2
3
4
5
BP_A 引用 BP_B
BP_B 引用 BP_A
// 两者首先都获得骨架类
// 然后两者都完全编译
// → 没有死锁!

场景 3 - 加载游戏:

1
2
3
LoadLevel(MyLevel);
// 所有蓝图的骨架首先加载
// 然后按依赖顺序进行完整编译

SKEL 命名约定

如果你在日志或崩溃中看到这个:

1
BP_MyActor_C_SKEL

那个 _SKEL 后缀意味着你正在查看一个骨架类。_C 是生成类的后缀。

快速要点

  • 骨架类 = 类头文件(属性 + 函数签名,没有实现)
  • 在编译的阶段 VIII创建
  • 通过提供”可以引用的东西”解决循环依赖
  • 把它想象成智能前向声明
  • 在字节码生成后被完整类替换

为什么这很重要

理解骨架帮助你:

  • 调试”缺少函数”错误(骨架编译了,完整编译失败了)
  • 理解为什么编译分阶段进行
  • 知道为什么循环依赖通常有效(但如果不小心仍然会导致问题)

想要更多细节?

有关代码示例的完整解释:

下一份小食:我们将窥视”清理和净化”过程!


🍿 BPVM 小食包系列

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