• 【UE5 C++基础 04】UHT基础


    提到UHT,离不开它的另一个关键词就是“反射
    先从初学者接触的UHT说起

    前言

    第一次写RPC函数的时候,会发现下面这样的问题,导致IDE给出Error,但是编译可以通过。

    现象:C++绕过IDE实现的反射

    在目前主流的IDE 比如VS2019,VS2022。
    都会对UE的C++工程报错,因为
    例如:RPC相关函数XX_Implementation 在声明和调用的时候其实只有XX

    /** 用于生成投射物的服务器函数。*/
    	UFUNCTION(Server, Reliable)
    	void HandleFire();
    
    • 1
    • 2
    • 3

    这样就导致IDE 会认为这个函数不存在,而抛出错误,但是编译可以通过。

    非C++规范的代码格式

    引用马哥的UBT解释
    首先我们要知道,我们写的UE5代码不是标准的C++代码,是基于UE5源代码层层改装了很多层的,所以,UHT将UE5代码转化成标准的C++代码,而UBT负责调用UHT来实现这个转化工作的,转化完以后,UBT调用标准C++代码的编译器来将UHT转化后的标准C++代码完全编译成二进制文件,整体上看,UHT是UBT的编译流程的一部分,不知道讲清楚了没。

    UBT:UnrealBuildTool(C#)
    UHT:UnrealHeaderTool (C++)

    UBT是一个自定义工具,负责管理通过各种编译配置来编译虚幻引擎5(UE5)源代码的过程。

    完整编译流程

    UBT搜集目录中的.cs文件,然后UBT调用UHT分析需要分析的.h .cpp文件(典型根据文件是否含有#include"FileName.generated.h",是否有UCLASS()UPROPERTY等宏)生成generated.h和gen.cpp文件,生成的文件路径为 Intermediate->Build-Win64-UE4-Inc,最后UBT调用MSBuild,将.h.cpp和generated.h gen.cpp结合到一起然后编译。

    UBT通过扫描头文件,记录所有包含反射类型的modules(模块),当其中有头文件改变时,就会用UHT更新反射数据。UHT解析头文件,扫描标记,生成用于支持反射的C++代码。编译时,两个工具皆有可能出现错误,必须仔细检查。

    UHT

    UHT全称为UnrealHeaderTool

    自定义编译方法,负责处理引擎反射系统编译所必需的信息,是支持UObject系统的自定义解析和代码生成工具。代码编译分两个阶段进行:

    • 调用UHT。它将解析C++头文件中引擎相关类元数据,并生成自定义代码,以实现诸多UObject相关的功能。
    • 调用普通C++编译器,以便对结果进行编译。

    常用的UHT宏

    UENUM()
    UCLASS()
    USTRUCT()
    UFUNCTION()
    UPROPERTY()

    反射

    什么是反射

    引用yimi的反射解释
    ​ 在C#中,反射的定义是运行中的程序查看本身的元数据或其它程序的元数据的行为。所谓元数据就是有关程序及其类型(由名称、数据结构、行为及约束条件组成)的数据被称为元数据。

    ​ C++原生并不支持任意形式的反射,元数据都保存,在C++这里行不通,C++的语言哲学不为用不到的特性付出任何代价。但是绕开反射,就只能做阉割版缩水版的面向对象。很简单的事实就是,在缺乏反射的支持下,C++上现存的诸多Json库、xml、序列化库、数据库orm、gui等杂碎库,与java、C#等相比,就显得很弱智很啰嗦,因此虚幻引擎自己实现了一套反射系统,该系统用来利用、查询以及操作关于C++类、结构体、函数 、成员变量以及枚举的信息。通过反射,UE4可以实现序列化,GC(垃圾回收),网络复制,C++蓝图通信等重要功能。

    ​ 反射系统是可以选择加入的。你需要给暴露给反射系统的类型或属性添加注解,这样Unreal Header Tool (UHT)就会在编译工程的时候利用那些信息生成特定的代码。

    在代码中加入反射

    以下内容摘取翻译自官方博客Unreal Property System (Reflection)

    ​ 反射系统是可以选择加入的。你需要给暴露给反射系统的类型或属性添加注解,这样Unreal Header Tool (UHT)就会在编译工程的时候利用那些信息生成特定的代码。

    标记

    为了标记一个头文件包含反射类型,需要在文件顶部添加一个特殊的include文件。该文件必须放在#include的最后,这让UHT知道它需要考虑这个文件,并且在反射系统的实现里也是需要的。

    #include "FileName.generated.h"
    
    • 1

    你现在可以使用UENUM()、UCLASS()、USTRUCT()、UFUNCTION()、以及UPROPERTY()来在头文件中注解不同的类型以及成员变量。每一个宏都会出现在类型或者成员变量的前面,并且可以包含额外的修饰符关键字。

    #include "StrategyTypes.h"
    #include "StrategyChar.generated.h"
    
    UCLASS(Abstract)
    class AStrategyChar : public ACharacter, public IStrategyTeamInterface
    {
        GENERATED_BODY()
    
        UFUNCTION(BlueprintCallable, Category = Attachment)
        bool IsWeaponAttached();
    
    protected:
        [more code omitted]
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这个类继承自ACharacter,使用UCLASS()来声明它是需要反射的类型,在内部定义了一个GENERATED_BODY()的宏。GENERATED_BODY()在需要反射的类或结构体里面是必要的,且必须被放置在类体的最前方,它们会被替换成额外的函数和typedef。

    限制

    ​ UHT并不是一个真正的C++分析器,它只能理解这个语言的一个子集并且会尝试跳过那些它不需要的文本。只关注反射的类型、函数以及属性。

    使用反射数据

    大多数的游戏代码可以在运行时忽略属性系统,也可以享受它给你带来的好处,但是当你在写工具代码或者构建游戏性系统的时候,你就会觉得它很有用了。

    ​ 属性系统(property system)的类型层次大约如下所示:

    UField
        UStruct
            UClass (C++ class)
            UScriptStruct (C++ struct)
            UFunction (C++ function)
    
        UEnum (C++ enumeration)
    
        UProperty (C++ member variable or function parameter)
    
            (Many subclasses for different types)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • UStruct是聚合结构体的基础类型(包含其它成员的类型,比如一个C++类、结构体、或者函数),不应该跟C++中的结构体(struct)混为一谈(那是UScriptStruct)
    • UClass可以包含函数或属性作为其子集
    • 而UFunction和UScriptStruct只能包含属性

    您可以通过编写 UTypeName::StaticClass() 或 FTypeName::StaticStruct() 来获取反射 C++ 类型的 UClass 或 UScriptStruct,并且可以使用 Instance->GetClass() 获取 UObject 实例的类型(不可能获取结构实例的类型,因为没有公共基类或结构所需的存储)。

    要遍历 UStruct 的所有成员,请使用 TFieldIterator:

    for (TFieldIterator<UProperty> PropIt(GetClass()); PropIt; ++PropIt)
    {
        UProperty* Property = *PropIt;
        // Do something with the property
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    TFieldIterator 的模板参数用作过滤器(因此您可以使用 UField 来查看属性和函数,或者只查看其中一个)。迭代器构造函数的第二个参数指示您是只想要指定类/结构中引入的字段,还是父类/结构中的字段(默认);它对函数没有任何影响。

    每种类型都有一组独特的标志(EClassFlags + HasAnyClassFlags 等),以及从 UField 继承的通用元数据存储系统。关键字说明符通常存储为标志或元数据,这取决于它们是在运行时游戏中需要,还是仅用于编辑器功能。这允许删除仅编辑器的元数据以节省内存,而运行时标志始终可用。

    你可以利用反射数据来做很多不同的事情 (枚举属性,以数据驱动的方式来获取、设置值,调用反射函数,甚至是创建新的对象)与其深入这里说的某个事例,倒不如看看UnrealType.h和Class.h中的代码,并且研究其中的一个与你想要完成功能相似的代码示例。

    中间文件.generated.h的分析

  • 相关阅读:
    体验版小程序访问不到后端接口请求失败问题解决方案
    vue框架之插槽,组件的自定义,网络代理配置,网络公共路径配置
    第一百五十五回 如何获取位置信息
    uni-app + mui-player & vue + mui-player 播放flv文件
    如何使用AI提问提示词(Prompt):让你的提问回答更有效
    如何使用Apple Watch解锁iPhone和Mac?
    python程序打包——基础准备、源代码打包、二进制打包、setuptools基础
    【开题报告】基于SpringBoot的膳食营养健康网站的设计与实现
    Python高级_第3章_HTTP协议与静态Web服务器开发
    Linux开发工具之编辑器-vim
  • 原文地址:https://blog.csdn.net/AiTanXiing/article/details/125430110