0 拓展
在开始介绍反射之前,我想先了解下不同的语言,不同的类型检查机制和反射的联系。以及偏移量在反射中的作用是什么。(这部分知识有利于反射机制的深入理解)
0.1 静态类型检查
静态类型检查 是在 编译时 对程序中的所有变量和表达式进行类型检查,以确保每个操作数、变量、函数调用等的类型都是一致和兼容的。其中C、C++、Java、C#都是静态类型检查的语言。其主要原理可以通过以下几个方面来解释:
0.1.1 静态类型检查的主要原理
-
类型声明和定义
在静态类型语言中,每个变量、函数参数和返回值等的类型在代码编写时就已经明确指定。这些类型信息是编译器进行类型检查的基础。int a = 10; // 变量a的类型是int double b = 20.5; // 变量b的类型是double
-
符号表(Symbol Table)
编译器在编译过程中维护一个符号表,用于存储所有变量、函数及其类型信息。符号表在语法分析阶段创建,并在语义分析阶段进行扩展和检查。 -
类型推导
有些静态类型语言支持类型推导,编译器可以根据上下文自动推导出变量的类型(如C++中的auto
关键字)。auto x = 10; // 编译器推导x的类型为int auto y = 20.5; // 编译器推导y的类型为double
-
类型检查
编译器在编译过程中对所有表达式和语句进行类型检查,以确保类型一致性。例如,变量赋值时检查类型兼容性,函数调用时检查参数类型匹配,运算符操作时检查操作数类型。int a = 10; double b = 20.5; a = b; // 编译器会警告或错误,类型不兼容 void foo(int x) { } foo(b); // 编译器会警告或错误,类型不兼容
-
类型转换
静态类型语言通常支持显式类型转换和隐式类型转换。编译器在类型转换时进行检查,确保转换是合法的。int a = 10; double b = static_cast<double>(a); // 显式类型转换 double c = a; // 隐式类型转换
0.1.2 编译器的工作流程
编译器的工作流程通常分为以下几个阶段,每个阶段都会涉及类型检查:
-
词法分析(Lexical Analysis)
将源代码转换为一系列标记(tokens),这些标记代表代码中的基本元素,如关键字、标识符、操作符等。 -
语法分析(Syntax Analysis)
根据语法规则将标记组织成语法树(parse tree 或 AST, Abstract Syntax Tree),表示程序的结构。 -
语义分析(Semantic Analysis)
进行类型检查,确保每个操作符和操作数的类型一致。建立和维护符号表,跟踪变量和函数的类型信息。 -
中间代码生成(Intermediate Code Generation)
将语法树转换为中间表示(IR, Intermediate Representation),这种表示比源代码更接近机器代码,但仍然与具体机器无关。 -
优化(Optimization)
对中间代码进行各种优化,提高程序执行效率。 -
目标代码生成(Code Generation)
将优化后的中间代码转换为目标机器代码。
0.1.3 静态类型检查的优点和缺点
优点
- 提高性能: 静态类型检查消除了运行时类型检查的开销,生成的代码更高效。
- 早期错误检测: 类型错误在编译时被捕捉,减少了运行时错误的发生。
- 代码可维护性: 明确的类型定义提高了代码的可读性和可维护性。
缺点
- 灵活性较低: 类型必须在编译时确定,减少了代码的灵活性和动态性。
- 开发速度较慢: 需要显式地声明类型,增加了编码工作量。
0.1.4 示例
以下是一个C++代码示例,演示了静态类型检查的基本原理:
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3); // 编译时确定类型
std::cout << "Result: " << result << std::endl;
return 0;
}
在这个例子中,变量result
的类型在编译时确定为int
,函数add
的参数类型也在编译时确定为int
,编译器会在编译时检查这些类型信息,确保类型一致性和正确性。
通过静态类型检查,编译器可以在编译阶段发现并修正许多潜在的错误,提高程序的可靠性和性能。这种设计使得静态类型语言在大型、复杂的系统中具有显著的优势。
0.1.5 C++也可以在运行时类型检查
虽然C++主要依赖静态类型检查来确保类型安全性和性能,但它也提供了多种机制支持运行时类型检查,如RTTI
、自定义类型检查机制和第三方库。通过这些机制,开发者可以在运行时识别和处理类型信息,实现更灵活和动态的程序行为。这种组合使得C++在需要高性能和类型安全性的同时,也能在某些场景下提供必要的动态类型处理能力。
接下来就详细介绍一下RTTI
RTTI(Runtime Type Information,运行时类型信息)是C++的一种机制,它允许程序在运行时获得类型相关的信息。RTTI的主要原理基于虚函数表(vtable)和类的继承关系。下面是RTTI的基本原理和实现机制:
RTTI基本原理
-
虚函数表(vtable)
当一个类包含虚函数时,编译器会为这个类生成一个虚函数表(vtable),其中包含了所有虚函数的指针。每个包含虚函数的类实例都有一个指向这个虚函数表的指针(称为vptr)。 -
RTTI数据
RTTI信息也会存储在虚函数表中。具体来说,编译器会在虚函数表中加入指向类型信息(如std::type_info
对象)的指针。这个指针通常在虚函数表的某个固定位置。
RTTI的实现
RTTI的实现依赖于两个关键操作符:typeid
和dynamic_cast
。
typeid
操作符用于获取对象的类型信息。它返回一个std::type_info
对象,该对象包含类型的信息。type_info
对象的实际内容由编译器生成和维护。
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {};
int main() {
Base* base = new Derived();
std::cout << "Type of base: " << typeid(*base).name() << std::endl;
delete base;
return 0;
}
在这个例子中,typeid(*base)
会返回一个std::type_info
对象,表示base
指向的实际对象的类型。
dynamic_cast
操作符用于进行安全的多态类型转换。它可以将基类指针或引用转换为派生类指针或引用。如果转换成功,返回目标类型的指针,否则返回nullptr
(对于指针类型)或抛出std::bad_cast
异常(对于引用类型)。
#include <iostream>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() {
std::cout << "Derived class method called" << std::endl;
}
};
int main() {
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived) {
derived->show();
} else {
std::cout << "dynamic_cast failed" << std::endl;
}
delete base;
return 0;
}
在这个例子中,dynamic_cast<Derived*>(base)
会检查base
是否指向一个Derived
对象,如果是,则返回Derived
类型的指针,否则返回nullptr
。
RTTI的工作流程
-
编译时:
- 编译器为每个包含虚函数的类生成虚函数表,并在虚函数表中添加指向类型信息的指针。
- 编译器生成
type_info
对象,用于表示每个类型的信息。
-
运行时:
typeid
操作符通过访问虚函数表中的类型信息指针,获取对象的类型信息。dynamic_cast
操作符通过检查虚函数表中的类型信息,确定是否可以进行安全的类型转换。
RTTI的限制
-
性能开销:
- RTTI会增加一些运行时开销,因为它需要在虚函数表中存储类型信息,并在运行时进行检查。
-
仅适用于多态类:
- RTTI依赖于虚函数表,因此只适用于包含虚函数的类(多态类)。对于非多态类,RTTI无法使用。
-
可移植性:
- 不同编译器和平台对RTTI的实现可能有所不同,因此在某些情况下,RTTI可能会有可移植性问题。
0.2 运行时动态类型检查
运行时动态类型检查是在程序运行时进行类型检查,以确保操作数和表达式的类型是有效和兼容的。与静态类型检查不同,动态类型检查是在程序执行过程中进行的,这意味着类型信息在运行时确定。动态类型检查通常用于动态类型语言,如Python、JavaScript和Ruby。以下是运行时动态类型检查的详细解释和其在几种常见语言中的实现方式。
0.2.1 主要特点
- 运行时确定类型: 变量和表达式的类型在运行时确定,允许更灵活的编程方式。
- 高灵活性: 由于类型信息在运行时可用,可以方便地进行类型转换、动态类型检查和
反射
。 - 潜在的性能开销: 运行时类型检查增加了程序的运行时开销,可能会影响性能。
- 延迟错误检测: 类型错误在运行时被捕获,这可能导致运行时错误,增加调试和维护的复杂性。
0.2.2 动态类型检查的实现
Python
是一种动态类型语言,所有的类型检查都是在运行时进行的。
def add(a, b):
return a + b
result = add(5, 3) # 有效操作
print(result)
result = add("hello", "world") # 有效操作,字符串拼接
print(result)
result = add(5, "world") # 无效操作,运行时报错
print(result)
在Python中,add
函数可以接受任何类型的参数,并在运行时检查它们的类型。如果参数类型不兼容,例如试图将整数与字符串相加,程序将在运行时抛出TypeError
异常。
JavaScript
也是一种动态类型语言,类型检查在运行时进行。
function add(a, b) {
return a + b;
}
console.log(add(5, 3)); // 有效操作,结果是8
console.log(add("hello", "world")); // 有效操作,结果是helloworld
console.log(add(5, "world")); // 有效操作,结果是5world
在JavaScript中,add
函数同样可以接受任何类型的参数,并在运行时决定如何处理这些参数。如果参数类型不兼容,例如将整数与字符串相加,JavaScript会进行隐式类型转换。
Ruby
也是动态类型语言,所有类型检查都在运行时进行。
def add(a, b)
a + b
end
puts add(5, 3) # 有效操作,结果是8
puts add("hello", "world") # 有效操作,结果是helloworld
puts add(5, "world") # 无效操作,运行时报错
在Ruby中,add
函数接受任何类型的参数,并在运行时检查它们的类型。如果参数类型不兼容,例如试图将整数与字符串相加,程序将在运行时抛出TypeError
异常。
0.2.3 优缺点
优点
- 灵活性: 允许变量在运行时改变类型,支持更多的编程模式,如鸭子类型(duck typing)和多态。
- 快速开发: 无需显式地声明类型,减少了编码工作量,加快了开发速度。
- 动态特性: 支持
反射
、动态类型转换和动态方法调用等特性,使得代码更具适应性。
缺点
- 性能开销: 运行时类型检查增加了额外的开销,影响了程序的执行效率。
- 延迟错误检测: 类型错误在运行时捕捉,可能导致运行时错误,增加了调试和维护的难度。
- 类型安全性: 动态类型检查可能会导致类型不安全的问题,增加了潜在的运行时错误。
0.3 两种检查方式和反射的关系
静态类型检查和动态类型检查都是类型系统的一部分,而反射机制则是与类型系统密切相关的高级特性。理解它们之间的联系有助于更好地理解编程语言的设计和应用。
0.3.1 反射机制
定义: 反射是指程序在运行时
能够检查和修改自身结构的能力,包括检查类、方法、属性等信息。
特点:
- 运行时类型信息: 允许在
运行时
获取和操作类型信息。 - 动态性: 支持动态调用方法、访问属性、创建对象等。
- 依赖语言支持: 需要语言提供相应的反射API。
优点:
- 动态性和灵活性:允许在运行时进行动态操作。
- 适用于框架和库:如
依赖注入
、ORM等需要动态类型信息的场景。
缺点:
- 性能开销:反射操作通常比直接调用更慢。
- 安全性问题:反射可能绕过类型检查,导致潜在的安全问题。
- 代码复杂性:反射增加了代码的复杂性和维护难度。
示例语言: Java、C#、Python
0.3.2 静态类型检查、动态类型检查和反射的联系
-
类型系统的基础:
- 静态类型检查和动态类型检查是类型系统的两种不同实现方式。
- 静态类型检查在编译时确定类型,动态类型检查在运行时确定类型。
-
反射的实现依赖:
静态类型语言
通过编译时
生成的元数据
和运行时类型信息
(RTTI)实现反射。动态类型语言
天然支持反射,因为类型信息在运行时就可用。
-
灵活性和性能的权衡:
- 静态类型检查提供了更好的性能和类型安全性,但灵活性较低。
- 动态类型检查提供了更高的灵活性,但带来了性能和类型安全性的挑战。
- 反射增加了动态性,但也带来了额外的性能开销和潜在的安全问题。
0.3.3 示例对比
静态类型语言中的反射(Java):
public class Main {
public static void main(String[] args) throws Exception {
// 通过类名获取Class对象,Class.forName会返回一个与给定字符串名对应的Class对象
Class<?> clazz = Class.forName("ExampleClass");
// 通过Class对象获取无参数的构造器,并创建该类的实例
Object obj = clazz.getDeclaredConstructor().newInstance();
// 通过Class对象获取名为"exampleMethod"的方法对象
Method method = clazz.getMethod("exampleMethod");
// 调用获取到的方法,传入实例对象
method.invoke(obj);
}
}
// 示例类,包含一个可以通过反射调用的方法
class ExampleClass {
public void exampleMethod() {
System.out.println("Method called through reflection!");
}
}
动态类型语言中的反射(Python):
# 定义一个示例类,包含一个可以通过反射调用的方法
class ExampleClass:
def example_method(self):
print("Method called through reflection!")
# 创建ExampleClass的实例
instance = ExampleClass()
# 使用getattr函数获取实例中的example_method方法
method = getattr(instance, "example_method")
# 调用获取到的方法
method()
0.4 偏移量在反射机制中的作用
在 Unreal Engine(UE)的反射机制中,偏移量(offset)扮演着至关重要的角色。反射机制允许在运行时动态地访问和操作对象的属性和方法,而偏移量是实现这一功能的基础之一。
偏移量在反射机制中的作用:
-
属性访问:
- 反射机制通过属性的偏移量来访问对象中的成员变量。当我们在运行时想要读取或修改对象的某个属性值时,UE通过反射系统找到该属性的偏移量,然后使用该偏移量直接访问内存中的相应位置。
-
内存布局解析:
- 偏移量帮助引擎解析和理解对象的内存布局。反射系统需要知道每个属性在对象内存中的具体位置,以便能够正确地进行属性的读写操作。
-
序列化和反序列化:
- 偏移量在对象的序列化和反序列化过程中起重要作用。通过偏移量,反射系统可以精确地读取对象中的每个属性并将其转换为适当的格式进行存储,或者从存储格式中恢复对象的状态。
实际应用
示例:使用反射机制访问属性
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyObject.generated.h"
UCLASS()
class UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyCategory")
int32 MyIntProperty;
};
// 示例:使用反射访问属性
void AccessPropertyWithReflection()
{
UMyObject* MyObject = NewObject<UMyObject>();
MyObject->MyIntProperty = 42;
// 获取属性的偏移量
UProperty* Property = FindField<UProperty>(UMyObject::StaticClass(), "MyIntProperty");
int32 Offset = Property->GetOffset_ForInternal();
// 通过偏移量访问属性
int32* PropertyAddress = (int32*)((uint8*)MyObject + Offset);
UE_LOG(LogTemp, Log, TEXT("MyIntProperty value: %d"), *PropertyAddress);
}
在这个示例中,我们首先定义了一个包含 MyIntProperty
属性的 UMyObject
类。然后,在 AccessPropertyWithReflection
函数中,通过反射机制获取 MyIntProperty
的偏移量,并使用该偏移量访问属性的值。
通过理解和正确使用偏移量,可以更好地利用 UE 的反射机制,实现动态属性访问和操作,提高代码的灵活性和扩展性。
1 UE反射的原理
1.1 为什么UE要实现反射机制,而C++没有实现
1.1.1 UE为什么要实现反射
Unreal Engine(UE)实现反射机制是为了提供一种灵活且强大的方式来处理对象的属性和方法,尤其是在编辑器、脚本和序列化等场景中。以下是Unreal Engine中实现反射机制的主要原因和应用场景:
1. 编辑器支持
反射机制使得UE4编辑器能够动态地显示和修改对象的属性。这对于开发人员和设计师来说极其重要,因为它们可以在编辑器中直接调整游戏对象的属性,而无需重新编译代码。
- 属性面板: 通过反射,编辑器可以自动生成用户界面,让开发者和设计师在属性面板中查看和修改对象的属性。
- 蓝图编辑器: 蓝图是UE4中的可视化
脚本语言
,利用反射机制,蓝图编辑器可以动态地识别和调用C++类中的方法和属性。
2. 序列化
反射机制使得对象的自动序列化和反序列化变得更加容易。UE4中使用反射来读取和写入对象的属性,以便保存和加载游戏状态、资源等。
- 保存和加载: 使用反射,可以自动将对象的属性保存到文件中,并在需要时重新加载。
- 网络同步: 在多人游戏中,反射机制可以用来同步游戏状态,使得对象的属性在网络上传输时能够自动序列化和反序列化。
3. 动态对象创建
反射机制允许在运行时动态创建和管理对象。这在需要根据运行时条件动态加载和创建对象的场景中特别有用。
- 工厂模式: 反射可以用于实现工厂模式,动态创建对象实例,而无需在编译时确定具体的类。
- 模块和插件: 通过反射,可以动态加载和使用模块或插件中的类和对象,增强引擎的扩展性和灵活性。
4. 脚本语言支持
UE4支持使用脚本语言(如Blueprint)来编写游戏逻辑。反射机制使得脚本语言能够访问和调用C++类中的属性和方法,从而实现脚本与引擎核心代码的无缝集成。
- 脚本调用C++方法: 通过反射,脚本语言可以调用C++类中的方法,访问对象的属性,实现高度的灵活性和动态性。
- 跨语言互操作: 反射机制使得不同语言之间能够方便地进行互操作,提高了引擎的扩展能力。
5. 自动化测试和调试
反射机制在自动化测试和调试中也起到了重要作用。通过反射,可以动态地检查对象的状态,调用对象的方法,从而实现更加灵活和全面的测试。
- 自动化测试工具: 测试工具可以通过反射机制自动化地调用对象的方法,设置对象的状态,从而进行全面的测试覆盖。
- 调试工具: 调试工具可以利用反射机制动态检查对象的状态,帮助开发者快速定位和修复问题。
6. 总结
UE4实现反射机制的主要目的是为了提高引擎的灵活性和动态性,支持编辑器中的动态属性调整、自动序列化、动态对象创建、脚本语言支持以及自动化测试和调试。这些特性使得开发者能够更高效地开发和维护游戏项目,同时提高了引擎的扩展性和灵活性。
1.1.2 反射对蓝图脚本至关重要
假如不实现反射机制,蓝图脚本将难以实现其大部分功能,尤其是动态访问和操作C++类的属性和方法。反射机制在UE4中起到了关键作用,使蓝图能够与底层C++代码无缝集成,并提供强大的编辑器支持和运行时动态行为。
为什么反射机制对蓝图脚本至关重要
-
动态访问属性和方法:
- 反射机制允许蓝图在运行时动态访问和操作C++类的属性和方法。没有反射,蓝图将无法知道或调用这些属性和方法,因为它们的类型信息和内存布局在编译时已经确定,而在运行时则不可见。
-
编辑器支持:
- 反射使得UE4编辑器可以自动识别C++类的属性和方法,并生成相应的用户界面。例如,属性面板中显示的变量,蓝图编辑器中的节点,这些都是通过反射机制动态生成的。
-
序列化和网络同步:
- 反射允许UE4自动序列化和反序列化对象的属性,以便保存和加载游戏状态,以及在网络同步中传输对象状态。没有反射,这些功能将需要大量的手工编码和维护。
如果不使用反射机制的替代方案
假如UE4不使用反射机制,以下是一些可能的替代方案,但这些方案都存在显著的缺陷和局限性:
-
手动编写桥接代码:
- 开发者需要手动编写大量桥接代码,将C++类的属性和方法暴露给蓝图。这不仅繁琐且易出错,而且难以维护和扩展。
-
静态生成代码:
- 使用代码生成工具在编译时生成静态的接口代码,将C++类暴露给蓝图。这种方法增加了编译时间和复杂性,而且难以处理动态行为。
-
限制蓝图功能:
- 大幅度简化蓝图功能,仅提供有限的、预定义的操作和接口。这样会严重限制蓝图的灵活性和实用性,使其难以满足复杂游戏开发的需求。
总结
没有反射机制,蓝图脚本将难以实现其动态访问和操作C++类属性和方法的能力,这将大大限制蓝图的功能和灵活性。反射机制是蓝图脚本能够高效、动态地集成和操作C++代码的关键技术,支持UE4编辑器提供强大的功能,简化了游戏开发过程。如果不使用反射机制,开发者将需要手动编写大量桥接代码,增加了开发难度和维护成本,同时大幅度降低了开发效率和代码的可维护性。
1.1.3 为什么C++没有实现反射
C++没有原生实现反射机制的主要原因涉及到语言设计的目标、性能考虑和历史背景。以下是一些关键因素:
1. 语言设计目标
高性能:
- C++是一种系统级编程语言,设计目标之一是提供高性能和高效的资源管理。反射机制通常需要在运行时维护大量的类型信息,这会引入额外的开销,影响性能。
- 在C++中,编译时确定的类型信息有助于编译器进行优化,如内联函数、去除死代码等,这些优化提高了程序的执行效率。引入反射机制可能会干扰这些优化。
静态类型安全:
- C++强调静态类型安全,编译时进行类型检查能够捕捉大多数类型错误,确保类型一致性。反射机制则允许在运行时动态处理类型,这可能会引入类型安全性问题。
2. 复杂性和实现难度
复杂性增加:
- 实现一个高效且通用的反射机制需要在编译器和运行时系统中引入大量复杂的元数据管理和类型检查逻辑。这会增加编译器和运行时系统的复杂性。
- C++语言本身已经非常复杂,增加反射机制会使语言和编译器的实现变得更加复杂和难以维护。
语言历史和演变:
- C++起源于C语言,并一直保持向后兼容。C语言没有反射机制,而C++的设计也继承了这一传统,注重性能和低级别的系统编程能力。
- 反射机制在较新设计的高级语言(如Java、C#)中更常见,因为这些语言在设计时就考虑了运行时类型检查和反射功能,而C++的设计初衷并没有包含这些特性。
3. 替代方案和社区解决方案
手动反射和宏:
- 虽然C++没有原生的反射机制,但开发者可以使用宏、模板和编译时元编程等技术手动实现某些反射功能。例如,可以使用模板和宏生成类型信息。
- Boost等库提供了部分反射功能,虽然不如原生支持,但可以在一定程度上满足需求。
第三方库:
- 有一些第三方库为C++提供了反射功能,如RTTR(Run Time Type Reflection)和Boost.TypeErasure。这些库通过元编程和其他技术实现反射,虽然性能不如原生支持,但提供了较为灵活的解决方案。
4. 总结
C++没有原生实现反射机制的原因包括语言设计目标(高性能和静态类型安全)、复杂性和实现难度以及历史背景。然而,通过手动实现和使用第三方库,开发者仍然可以在C++中实现某种程度的反射功能,以满足特定需求。
1.2 UE是如何实现反射机制
Unreal Engine(UE)通过一套宏、元数据和自定义的对象系统实现了反射机制。这些机制允许在运行时获取类型信息、访问和修改对象的属性和方法。以下是UE反射机制的核心部分及其实现方式:
1.2.1 核心组件
- UObject和UClass
- 宏系统
- 元数据系统
- 反射API
1. UObject和UClass
UObject:所有支持反射的类都必须继承自UObject
。UObject
是UE的所有反射类的基类,提供了反射所需的基础设施。
UClass:UClass
是UObject
的元类,包含了反射信息,如类的名称、属性和方法。
2. 宏系统
UE使用一套宏来标记和生成反射信息,这些宏包括UCLASS
、USTRUCT
、UPROPERTY
和UFUNCTION
等。
UCLASS:用于声明一个类支持反射。
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
}
USTRUCT:用于声明一个结构体支持反射。
USTRUCT()
struct FMyStruct {
GENERATED_BODY()
UPROPERTY()
int32 MyProperty;
};
UPROPERTY:用于声明类或结构体中的属性支持反射。
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Custom")
int32 MyProperty;
}
UFUNCTION:用于声明类中的方法支持反射。
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category="Custom")
void MyFunction();
}
3. 元数据系统
UE的反射系统通过生成元数据文件来记录类、属性和方法的信息。这些元数据在编译时生成,并在运行时使用。
编译时生成:使用宏系统,UE在编译时生成元数据文件,描述类、属性和方法的反射信息。
运行时使用:在运行时,UE使用这些元数据来实现反射功能,如动态属性访问、方法调用等。
4. 反射API
UE提供了一套反射API,用于在运行时访问和操作反射信息。这些API包括但不限于:
查找类和创建实例: FindObject
和StaticConstructObject
用于查找类并创建其实例。
UClass* MyActorClass = FindObject<UClass>(ANY_PACKAGE, TEXT("MyActor"));
if (MyActorClass) {
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
}
访问属性:UProperty
类及其派生类(如UIntProperty
、UFloatProperty
等)用于访问和修改对象的属性。
UClass* MyActorClass = AMyActor::StaticClass();
UProperty* MyProperty = FindField<UProperty>(MyActorClass, TEXT("MyProperty"));
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
if (MyProperty) {
MyProperty->SetPropertyValue_InContainer(MyActorInstance, 42);
}
调用方法:UFunction
类用于调用对象的方法。
UClass* MyActorClass = AMyActor::StaticClass();
UFunction* MyFunction = MyActorClass->FindFunctionByName(TEXT("MyFunction"));
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
if (MyFunction) {
MyActorInstance->ProcessEvent(MyFunction, nullptr);
}
1.2.2 反射的实现步骤
Unreal Engine(UE)通过一套宏、元数据和自定义的对象系统实现了反射机制,这些机制依赖于编译时的代码生成和运行时的反射API。以下是反射机制的实现过程,详细解释了.generated.h
文件、GENERATED_BODY()
宏、UHT(Unreal Header Tool)
和UBT(Unreal Build Tool)
的作用及其在不同阶段的功能:
1. Unreal Header Tool (UHT)
作用: UHT是一个在编译时运行的工具,只用于解析带有UE4宏的C++头文件,并生成反射信息和必要的代码。
工作流程:
- 解析C++头文件:
- UHT读取和解析C++头文件,识别其中的反射宏(如
UCLASS
、USTRUCT
、UPROPERTY
、UFUNCTION
等)。
- UHT读取和解析C++头文件,识别其中的反射宏(如
- 生成代码:
- 根据解析到的元数据,UHT生成对应的
.generated.h
文件,这些文件包含反射系统所需的额外代码。 - 生成的代码包括类型信息、属性和方法的元数据注册代码等。
- 根据解析到的元数据,UHT生成对应的
2. .generated.h 文件
作用:.generated.h
文件由UHT生成,包含了反射系统所需的自动生成的代码。它们通常会在每个类或结构体的头文件中被包含。
3. GENERATED_BODY() 宏
作用:GENERATED_BODY()
宏是一个复杂的宏,它扩展为一系列声明和定义,这些声明和定义由UHT生成,用于支持UE4的反射系统。
示例:
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY() // 这里插入由UHT生成的代码
public:
// 用户定义的属性和方法
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom")
int32 MyProperty;
UFUNCTION(BlueprintCallable, Category = "Custom")
void MyFunction();
};
展开后的效果: GENERATED_BODY()
宏会展开为在.generated.h
文件中生成的实际代码,包括反射信息和元数据注册函数的声明。
4. Unreal Build Tool (UBT)
作用: UBT是UE4的构建系统,用于管理项目的编译过程。它调用UHT并处理生成的代码文件。
工作流程:
- 调用UHT:
- 在编译过程中,UBT会调用UHT来解析头文件,并生成
.generated.h
文件
- 在编译过程中,UBT会调用UHT来解析头文件,并生成
- 编译生成的代码:
- UBT会将生成的
.generated.h
文件与原始代码一起编译,确保所有反射信息和元数据都被正确集成到最终的二进制文件中。
- UBT会将生成的
1.2.2.1 反射机制的工作流程
- 代码编写:
- 开发者在C++头文件中使用
UCLASS
、USTRUCT
、UPROPERTY
、UFUNCTION
等宏标记类、结构体、属性和方法。
- 开发者在C++头文件中使用
- UHT解析:
- UBT在编译时调用UHT,UHT解析C++头文件,识别反射宏,并生成
.generated.h
文件。
- UBT在编译时调用UHT,UHT解析C++头文件,识别反射宏,并生成
- 代码生成:
- UHT生成的
.generated.h
文件包含了类型信息、属性和方法的元数据注册代码。
- UHT生成的
- 编译:
- UBT将原始代码和生成的代码一起编译,生成最终的二进制文件。
- 运行时反射:
- 在运行时,UE4使用生成的元数据和反射API来动态访问和操作对象的属性和方法。
1.2.2 示例
以下是一个完整的示例,展示了如何使用UE的反射机制动态访问属性和调用方法:
// MyActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom")
int32 MyProperty;
UFUNCTION(BlueprintCallable, Category = "Custom")
void MyFunction();
};
// MyActor.cpp
#include "MyActor.h"
void AMyActor::MyFunction() {
UE_LOG(LogTemp, Warning, TEXT("MyFunction called!"));
}
// UsageExample.cpp
void UseReflection() {
// 查找MyActor类
UClass* MyActorClass = AMyActor::StaticClass();
// 创建MyActor实例
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
// 动态设置属性
UProperty* MyProperty = FindField<UProperty>(MyActorClass, TEXT("MyProperty"));
if (MyProperty) {
MyProperty->SetPropertyValue_InContainer(MyActorInstance, 42);
int32 PropertyValue;
MyProperty->GetValue_InContainer(MyActorInstance, &PropertyValue);
UE_LOG(LogTemp, Warning, TEXT("MyProperty value: %d"), PropertyValue);
}
// 动态调用方法
UFunction* MyFunction = MyActorClass->FindFunctionByName(TEXT("MyFunction"));
if (MyFunction) {
MyActorInstance->ProcessEvent(MyFunction, nullptr);
}
}
2 使用UE反射机制
2.1 使用反射机制的前提
要使用反射机制有四个必要条件:①继承UObject类;②包含.generated.h头文件;③添加了GENERATED_BODY()宏;④给需要反射的类、属性、方法通过宏进行标记。
前三个条件都没必要强调,在UE中创建一个集成UObject的类自动就帮我们设置好了。所以讨论下第四点就行。
使用反射系统的总开关是类的反射,UCLASS(),如果这个类没有用这个宏进行标记,则内部的属性或方法标记了也不能反射。这一点需要注意。
同时反射宏还有需要可以配置的
2.2 使用反射API
Unreal Engine 4(UE4)反射API提供了一套强大且灵活的工具,用于在运行时动态地访问和操作对象的属性和方法。以下是对UE4反射API的详细介绍,包括常用类和函数的使用方法。
1. 核心类和函数
UE4的反射API主要涉及以下几个核心类和函数:
- UClass: 描述一个UObject类的元数据。
- UObject: 所有反射对象的基类。
- UProperty: 描述对象属性的元数据。
- UFunction: 描述对象方法的元数据。
- FField: 用于访问对象的属性和方法。
2. 查找类 == 查找UClass对象
使用StaticClass
、FindObject
或GetClass
函数来查找类。这三种方法使用场景不一样。
StaticClass函数是静态函数,直接通过类调用静态函数即可。
FindObject函数直接在运行时通过类名来查找UClass对象。
GetClass函数是通过对象实例来查找UClass对象。
// 通过StaticClass查找类(静态函数查找)
UClass* MyActorClass = AMyActor::StaticClass();
// 通过FindObject查找类(运行时根据对象名称查找类)
UClass* MyActorClass = FindObject<UClass>(ANY_PACKAGE, TEXT("MyActor"));
// 获取对象的类(通过对象来查找)
UClass* ObjectClass = Object->GetClass();
3. 创建实例
使用NewObject
函数来创建类的实例。
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
4. 访问属性
使用FindField
函数查找属性,然后使用属性的API来获取或设置属性值。
查找属性
UProperty* MyProperty = FindField<UProperty>(MyActorClass, TEXT("MyProperty"));
获取和设置属性值
if (MyProperty) {
// 设置属性值
MyProperty->SetPropertyValue_InContainer(MyActorInstance, 42);
// 获取属性值
int32 PropertyValue;
MyProperty->GetValue_InContainer(MyActorInstance, &PropertyValue);
UE_LOG(LogTemp, Warning, TEXT("MyProperty value: %d"), PropertyValue);
}
5. 调用方法
使用FindFunctionByName
函数查找方法,然后使用ProcessEvent
函数调用方法。
查找方法
UFunction* MyFunction = MyActorClass->FindFunctionByName(TEXT("MyFunction"));
调用方法
if (MyFunction) {
MyActorInstance->ProcessEvent(MyFunction, nullptr);
}
6. 实际应用示例
下面是一个完整的示例,展示如何使用UE4反射API在运行时访问和操作对象的属性和方法。
MyActor.h:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class MYGAME_API AMyActor : public AActor {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Custom")
int32 MyProperty;
UFUNCTION(BlueprintCallable, Category = "Custom")
void MyFunction();
};
MyActor.cpp:
#include "MyActor.h"
void AMyActor::MyFunction() {
UE_LOG(LogTemp, Warning, TEXT("MyFunction called!"));
}
UsageExample.cpp:
#include "MyActor.h"
void UseReflection() {
// 查找MyActor类
UClass* MyActorClass = AMyActor::StaticClass();
// 创建MyActor实例
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
// 动态设置属性
UProperty* MyProperty = FindField<UProperty>(MyActorClass, TEXT("MyProperty"));
if (MyProperty) {
MyProperty->SetPropertyValue_InContainer(MyActorInstance, 42);
int32 PropertyValue;
MyProperty->GetValue_InContainer(MyActorInstance, &PropertyValue);
UE_LOG(LogTemp, Warning, TEXT("MyProperty value: %d"), PropertyValue);
}
// 动态调用方法
UFunction* MyFunction = MyActorClass->FindFunctionByName(TEXT("MyFunction"));
if (MyFunction) {
MyActorInstance->ProcessEvent(MyFunction, nullptr);
}
}
7. 高级用法
获取类的所有属性:可以使用TFieldIterator
来遍历类的所有属性。
for (TFieldIterator<UProperty> It(MyActorClass); It; ++It) {
UProperty* Property = *It;
UE_LOG(LogTemp, Warning, TEXT("Property: %s"), *Property->GetName());
}
获取类的所有方法:可以使用TFieldIterator
来遍历类的所有方法。
for (TFieldIterator<UFunction> It(MyActorClass); It; ++It) {
UFunction* Function = *It;
UE_LOG(LogTemp, Warning, TEXT("Function: %s"), *Function->GetName());
}
动态创建对象并设置属性
void DynamicCreateAndSetProperty() {
// 创建对象
UClass* MyActorClass = AMyActor::StaticClass();
AMyActor* MyActorInstance = NewObject<AMyActor>(MyActorClass);
// 设置属性
UProperty* MyProperty = FindField<UProperty>(MyActorClass, TEXT("MyProperty"));
if (MyProperty) {
MyProperty->SetPropertyValue_InContainer(MyActorInstance, 100);
int32 PropertyValue;
MyProperty->GetValue_InContainer(MyActorInstance, &PropertyValue);
UE_LOG(LogTemp, Warning, TEXT("Property value: %d"), PropertyValue);
}
// 调用方法
UFunction* MyFunction = MyActorClass->FindFunctionByName(TEXT("MyFunction"));
if (MyFunction) {
MyActorInstance->ProcessEvent(MyFunction, nullptr);
}
}