之前用Unity在定义变量时经常会在变量前面标记[SerializeField],用于将private和protected属性的变量显示在inspector上(public变量会直接显示)。学习时没有细想这些都是什么。关于序列化也是只知道大概的作用。学习UE时碰到了反射系统,而序列化则是反射系统的一大应用场景。此篇笔记将学习记录序列化的概念,反射的概念以及UE的C++反射实现,以及在UE中,序列化又是如何通过反射实现的。
1. 什么是序列化
计算机程序的基本运行周期可分为三个阶段:启动、运行和终止。当程序运行时,编译后的代码会被加载到内存中,程序运行过程中产生的变量和对象则存储在内存的栈区和堆区。然而,一旦程序终止运行,这些存储在内存中的临时数据都将丢失。为了持久化这些数据,我们需要在程序运行期间或终止前将其保存下来。这种需求常见于以下场景:
- 网络传输(如多人游戏中不同客户端间的状态同步)
- 游戏存档(保存玩家进度)
- 编辑器中对游戏对象的修改保存
简单来说,序列化 是将程序中的 对象(Object) 或 数据结构 转换为一种 可存储或可传输的格式(如二进制、JSON、XML等),以便在需要时能够重新恢复(反序列化)为原始对象的过程。
2. 什么是反射(Reflection)
在UE中,每次修改完C++代码后都需要重新编译才能继续在编辑器中进行操作。这是因为编辑器需要获取代码中的变量和对象信息,以便在面板中提供可视化编辑功能。然而,C++编译过程会丢失许多对编辑器至关重要的元数据信息。C++的源码中的各种类型名变量名函数名文本人类可读,而编译器会直接将人类可读的代码源码编译为机器指令。编译器会丢弃什么呢?对于一个对象MyClass,运行时内存只需要知道某一块内存是哪种类型,并不需要知道名字字符串。对象成员health,只需要知道其在某一块内存的固定偏移量是多少。各种继承关系也会被处理为虚函数表(vtable),运行时通过vtable指针调用虚函数,而vtable本身不包含任何类名或父类名信息。简单来说,一切都被编译器转化为了各种地址信息,程序运行时只需要知道从哪块地址取出什么数据及指令即可。
程序在运行的时候如果想知道:
- 我自己有哪些类?
- 这个类叫什么名字?继承自谁?
- 类里面有哪些成员变量,叫什么?是什么类型?
- 类里面有哪些成员函数?参数和返回值是什么?
那么反射机制就是程序在运行时获取自身结构信息(类,属性,函数等)并能够操作他们的能力。
Unreal引擎的许多强大功能和高效工作流都严重依赖反射。没有它,以下事情要么极其困难,要么根本不可能:
- 序列化: 将对象的状态(属性值)保存到磁盘(如存盘文件.uasset)或通过网络发送。
- 引擎需要知道对象有哪些属性、它们的类型以及如何读取/写入它们的值。反射提供了这份“属性清单”和操作方法。
- 垃圾回收: Unreal使用自动内存管理(GC)来回收不再使用的UObject。
- GC需要知道一个对象引用了哪些其他UObject(避免误删仍在使用的对象)。反射提供了属性之间的引用关系图(通过
UPROPERTY()
标记)。- 蓝图与C++的互操作性: 这是Unreal最强大的特性之一。让设计师在蓝图中使用C++类、访问C++变量、调用C++函数。
- 蓝图系统需要在运行时知道C++类有哪些属性(
UPROPERTY
)暴露给蓝图编辑、有哪些函数(UFUNCTION
)可以被蓝图调用、它们的参数是什么。反射提供了这份蓝图与C++交互的“接口说明书”。- 编辑器细节面板: 在Unreal编辑器中选中一个Actor或Component时,右侧的“Details”面板会显示其可编辑的属性。
- 编辑器需要知道这个对象有哪些属性可以显示和编辑、它们的分类(Category)、显示名称、工具提示、取值范围等(通过
UPROPERTY
的元说明符如EditAnywhere, BlueprintReadWrite, Category="Movement"
等实现)。反射提供了构建这个UI所需的所有信息。- 网络复制: 在多人游戏中,服务器需要将状态同步到客户端。
- 引擎需要知道哪些属性(
UPROPERTY(Replicated)
)需要被复制、在什么条件下复制。反射提供了需要复制的属性列表和复制规则。- 命令行动态调用: 通过控制台命令或蓝图调用特定对象的特定函数。
- 系统需要根据字符串形式的函数名和参数,在运行时找到匹配的函数并执行它。反射提供了函数名到实际函数指针的映射。
- 动态创建对象: 根据类名(字符串)在运行时创建对象实例(例如
NewObject()
或SpawnActor()
内部查找)。
- 引擎需要根据提供的类名字符串,找到对应的
UClass*
,然后才能创建实例。反射维护了所有反射类的注册表。
C++ 语言本身不支持反射特性,UE 在 C++ 的语法基础上通过 UHT 实现了反射信息的生成,从而实现了运行时的反射的目的。
3. UE C++中的反射机制
随便打开一个在UE中创建的C++文件,比如MoveActor.h:
1 | // Fill out your copyright notice in the Description page of Project Settings. |
注意这三个地方:UCLASS(),GENERATED_BODY(),UPROPERTY(EditAnywhere,BlueprintReadWrite)