【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
这是侑虎科技第1445篇文章,感谢作者雪流星供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。()
一直以来很多人都在说,虚幻引擎的学习难度要比Unity大很多,其中一个原因是C++本身很难,另外一点就是由于虚幻写了自己的编译系统,并且为了实现反射对C++代码进行封装,因此就算对C++基础掌握的不错,也一样很难理解其中代码的意思。本文将介绍虚幻引擎对编译和反射做了哪些工作,帮助刚接触虚幻引擎的开发者理解并快速上手开发,做一篇避坑指南,本文涉及到项目开发过程中,如何避免头文件、宏等奇怪的报错,解决常见编译问题,理解虚幻引擎模块化管理代码方式,理解引擎编译和启动过程,创建插件,引用第三方库,并参考引擎代码设计,获取快速开发技巧等等。全文大概2万字,希望能耐心看完。
当我们创建一个普通的C++空项目时,一般的步骤是配置平台、版本后,添加代码,创建`main`函数入口,右键项目点击Build(生成)或者重新生成,或者直接点击运行,然后就可以测试自己的代码和功能了。
一般情况下,是不需要点重新生成(R ebuild)的,尤其是在使用UE4时,这项一定要慎用,因为要等很久很久才能编译完。但除非你遇到了奇怪的编译错误,已经确保我之后说的那些配置正确之后还有问题,再点重新生成试一下能否成功,这里附一张图说明:
UE4运行项目也是如此,但不同之处在于点击“Build”的过程。首先先分析下UE4项目文件结构,不然很难理解。
下面展示的是UE4项目带C++的样子(不带C++的项目这里不再赘述):文件夹数量可能有多有少,和编译目标、插件有关,都无关紧要。
另外,Config文件夹下是引擎和项目的配置文件,一定要小心,不要删除。Content文件夹是游戏内容资源的目录;Plugins是项目依赖插件目录;Saved保存项目的一些缓存数据,包括性能调试、命令行生成的文件、保存的游戏数据和打包Cook数据等,可以删除;Source是项目C++代码目录。
Engine文件夹下包含引擎源码;Game下包含项目代码,包括插件;Programs文件夹下有两个重要的项目:UnrealBuildTool(编译工具)和UnrealHeaderTool(头文件解析工具)即UBT和UHT。
虚幻引擎的代码量非常恐怖,因此需要更专业的方式管理,虚幻引擎采用模块化的方式管理代码,每个模块之前相互引用依赖,通过引用的方式递归加载对应的模块,而管理这些模块的工具就是UBT,UHT用于头文件生成解析。
使用UBT管理模块的另一个原因是为了方便跨平台,这样我们不需要为每个平台都做对应的配置,不方便且容易出问题,对开发者不友好。有了UBT,只需要在对应的CS文件里配置一次后就可以应用到多个平台了。跨平台的工作由UBT来替你完成。
到目前为止大概了解了UBT是虚幻引擎管理各个模块的工具,但它不会编译代码,只是负责收集模块之间的信息然后根据平台和编译目标并告诉编译器进行编译。UBT源码可以在解决方案的Programs\UnrealBuildTool\下找到。
UBT采样NMake Build System,我们可以在项目属性的NMake处看到相关设置,这里显示的就是当我们右键点击Build和Rebuild,Clear事件会执行的内容,如下图:
我们顺着它指示的路径找,然后就会找到Build.bat文件,打开后查看内容:
参数来自上面的选项中,包括项目名字、编译目标、32位还是64位等信息。然后我们从UnrealBuildTool项目代码中找到入口函数,如下:
可以看到会接受对应平台等信息并进行解析,之后的内容就是读取分析Target.cs和Build.cs文件,然后调用编译工具编译项目,至于细节也没必要深究,对UBT了解到这一步感觉就足够了。
提了好几次模块,但还没有具体介绍,因为模块内容量很大,我会放在后面详细介绍,不过这里想大概介绍下每个模块下的Build.cs和项目中的Target.cs文件。
模块核心类,继承自IModuleInterface,并实现两个核心方法:StartupModule和ShutdownModule,会在引擎启动加载模块的时候执行,用于初始化模块信息,一般不需要更改
Target.cs只在项目中存在,它类似于项目设置,配置目标和扩展依赖模块,后面再解释。
Generate Visual Studio Project选项作用非常大,当我们打开VS项目后,我们会发现本地磁盘文件中的Source和VS显示的内容是没有保持一致的,因为虚幻引擎编译会忽略SLN文件,SLN存在的意义只是方便打开编写代码,不会查看Source下的文件结构,如果在VS项目中删除某个类,它会依然存在磁盘本地文件夹下请确保同时删除本地文件,或者当你更改或移动某个类文件路径,本地文件实际没有发生变化,而如果你这时不通过VS移动,而是直接移动本地文件夹下的文件,在VS中就会提示找不到该文件引用,学习虚幻引擎的新人一定会受此问题困扰,我当初也是,后来知道重新Generate Project Files就好了~第一个避坑指南说完了。
一般只要你修改 [ModuleName].Build.cs 文件,或移动文件,就需要为IDE生成解决方案文件,有时候也不用,如果出问题了就点一下,下面这三种方式一样效果:
1. UBT读取每个模块的Build.cs文件获取各模块之前的依赖关系(如果代码没有更改,则会跳过该模块)
由于UBT是由C#代码编写的,在C++项目中没有智能提示,里面的代码无法转至定义,虽然需要编写的代码不多,但依然很不方便,这对新手来说更加不友好,有办法解决提示问题吗?有。放到后面说,这里不是卖关子,只是要介绍的插件不仅仅可以用来提示UBT开发。
文本解析工具UnrealHeaderTool存在的意义就是为虚幻反射系统服务,蓝图的实现原理。(反射系统简单来说就是运行时获取类的信息,包括类的属性、方法、属性类型、名字、访问权限、方法名字和返回值等等,获取类的信息可以做到很多事情,包括通过名字调用类的方法等。)
UBT的主要工作就是利用开发者手动标记的属性、方法、类等宏来识别类的信息,然后将类信息封装到gen.cpp的代码中,当然还包括对构造析构函数等内容的修改,使用虚幻本身的垃圾回收系统进行管理。
可以看到最基础的Actor类的属性、方法、类等内容被一些特殊宏标注,包括:UCLASS()、UPROPERTY()、UFUNCTION()等,当然不止这些,还有很多其他不常用的。同时还可以看到有点属性和方法没有被宏标记,他们主要区别如下:
1. 上面提到过这些宏的作用是为了服务反射系统,将类的信息暴露给反射系统,也是蓝图能访问的基础条件,如果不加宏就无法被反射系统识别,更不用说在蓝图中调用了。当然如果需要蓝图对属性能进行更改等操作,还需要在宏括号内加标签进一步修饰。
2. 当类属性有宏修饰时,虚幻的垃圾回收系统可以对该属性进行管理,没有被UPROPERTY宏修饰或在AddReferencedObjects函数被没添加引用的UObject*成员变量无法被虚幻引擎识别,垃圾回收系统不会管理,因此使用指针时要注意。
每一项都有可选参数,主要是告诉蓝图关于这个类/函数/结构体等内容信息,不同参数在蓝图中有不同表现。
下面有宏体内类型修饰符都有哪些的介绍,分别对应UCLASS()、UINTERFACE()、UFUNCTION()、UPROPERTY()和USTRUCT()。里面具体参数项非常多,但实际上大部分都用不到,所以这里我就挑常用的介绍下:
改为:Blueprintable后,就可以创建了,也是继承Actor的默认的选项:
Actor这个类标注了Blueprintable,因此子类也就继承了这一选项,所以当一个继承自Actor的子类即便不写也是Blueprintable的。换句话就是,有些属性可以继承,具体有哪些可以在ObjectMacros.h中查看。
Const:该类所有函数和属性应该是Const的,且Const标签可以继承
注意:这里应该保持蓝图和C++代码本身的一致性,即,如果你在UCLASS中加了Abstract,应该也要将这个类写为抽象类,不然会出各种问题,包括下面的UFUNCTION也一样,如果是BlueprintPure修饰,就应加Const修饰函数,下面会有例子。
BlueprintPure,这个宏的意思是纯函数,但不可以理解为和C++中的纯虚函数有关系,事实上,这两个没有任何关系,当初我就是陷入这个循环无法自拔。
首先,BlueprintPure标记的函数必须有返回值,因为没有输入输出节点,看下图。其次
BlueprintPure的作用有些类似于用Const修饰的函数:即不可以更改类的成员变量值,但是它的限制并不像Const那样,你改了值就报错,但是, 请不要在标记为BlueprintPure的函数中更改其类的变量值,一定不要,看一下下面的例子:
对应在蓝图中的样子:和普通函数有些区别,是绿色的,没有执行输入和输出节点,至于返回值为void的就有了,不然它就没办法在蓝图里调用。
这个就是函数中Pure的真面目了:被Const标记的函数,因此以后再用就不要在标记Pure
每一项的属性都很多,比如说属性(UPRPOPERTY)能被EditAnywhere和VisibleAnywhere等修饰,但是这两个只能同时存在一个,原因是这两个属性属于相同的枚举类型,我们可以转到定义处查看,确保不出现类似错误,另外就是多用就熟悉了。
首先,它们的作用都是封装了引擎给你写好的代码。包括重写构造函数等,他们的区别是什么呢,看一下定义就好了:
有的构造函数里带FObjectInitializer& ObjectInitializer,这都是虚幻早期版本,现在基本都不用了,当然为了兼容,构造函数这样写也没问题,看一下UObject的定义就知道答案了:
至于这些宏具体干了什么,大钊老师已经在他的反射文章里写的非常清楚了,感兴趣可以看下《 《InsideUE4》UObject(八)类型系统注册-CoreUObject模块加载》。
类前面模块名_API宏的作用:用于暴露这个类给其他模块访问,如果没有,其他模块是无法获取到这个类的信息的,即便在Build.cs中引入了该模块。
模块是引擎管理代码的基本单位,插件和游戏中最少要包含一个模块,当创建项目时会自动生成游戏主模块。
当在引擎中创建一个插件时也会有一个默认模块,像这样:目前手动创建模块还没什么高效的方式,只能创建对应文件夹和模板文件。
这是一个模块最基本的结构,创建一个新模块只能先创建文件夹,然后加上对应模块名称的文件,Build.cs和ModuleName.h和cpp。
这里引用大钊老师总结的内容:每个模块都有Public和Private文件夹,如果类写在最外面(不在Public文件夹内)就会被视为Private,如果包含模块还是无法引用某个类,要检查头类文件是否在Public文件夹内:
如果能用Private包含,尽量用Private,以减少模块依赖,减少编译时间。
二者都能使本模块可以包含其它模块Public目录下的头文件,从而调用函数。但是PublicIncludePathModuleNames只能调用定义全部在头文件里的函数,这里要区别什么是声明(不带实现)、什么是定义(带实现)。但是如果定义在头文件中,很容易会出现重定义的错误,而且会拖慢编译速度,除非用Static或者Inline修饰。
PublicIncludePaths仅仅是包含相应目录,不是引用里面的头文件,如果你的#include里的路径很长,就可以利用这种方式在include的时候不用写对应头文件路径:
DLL和LIB库导入下面单独介绍。模块属性()DLL和LIB库导入下面单独介绍。
这里基本就是固定写法StartupModule和ShutdownModule两个方法分别是该模块被加载和卸载的时候执行,这里一般不需要修改什么,也可以选择在模块启动时写加载DLL之类的逻辑。至于模块什么时候被加载,是在ulpugin或者uproject中配置的。
当需要在本模块中引用另一个模块的类时,配置正确Build.cs之后,还要正确include头文件才能使用
为了正确做到上面两步,我一般选择的方式是去官网API查询,像这样:FDesktopPlatformModule
官网会告诉你引入的模块名字和include的方式,但是,这里我使用官网的方式是无法找到头文件的,而写上路径Developer后面的全部才能正确include这个头文件,像这样,红色的是错误写法,官网的方式,下面的是正确写法:
另外,尽量不要复制代码,尤其是整体类,太容易出问题了,而且复制代码容易导致很多模块重新编译,巨慢。
LIB路径(可选:也可以在上面的glfw.lib前面加,下面就不用写了)
然后在某个地方动态加载DLL,在使用前,可以写到StartupModule函数中:
其实插件就是模块的集合,最少有一个模块,基本结构如下:Module1是自己加进去的:
上面大部分信息都是描述插件信息的,关键是M odules部分,里面定义了模块的名字,加载时机等关键信息.
Type,描述的是打包到某个平台时是否要加载这个模块,比如打包游戏就不需要Editor相关的模块代码。
LoadingPhase,描述什么时候加载该模块,这个比较重要,依赖其他模块的模块应该在依赖模块之后加载,不然就会报错找不到对应模块。
这个是按时间排序的,默认是Default,顺便Default一般是引擎启动到百分之75左右加载这些模块,就是这个时候:
EarliestPossible是还没看到这个黑的加载界面之前就加载,几乎用不到,想知道其他的什么时候加载打断点测一下就好。
在游戏项目中创建模块的方式和在插件中无异,模块的基本结构都是一样的,但唯一需要注意的是对应的主模块类里的宏会发生变化:
上面版本信息不是介绍重点,主要是里面的模块,这里我创建了一个GameModule的游戏模块,里面的属性配置和上面讲的一致,关键是如果这里面不写GameModule模块,游戏引擎中是不会识别到这个模块的(引擎C++文件夹处不会有GameModule模块代码,也因此无法引用该模块内容,即便主模块的Build.cs中引用了GameModule模块),因此,这里一定要加上你写的模块。这里思考一个问题:如果GameModule没有被主模块引用,打包的时候会发生什么。
在讲虚幻模块的文章中,很少有讲到Target.cs文件的作用的,项目一般会默认创建两个,一个是Target.cs,另一个是Editor.Target.cs。
刚刚一直说模块的引用方式依赖Build.cs,但是没有任何模块依赖游戏主模块,游戏主模块是怎么加载进内存的呢,这就依赖了Target.cs中的 ExtraModuleNames.AddRange( new string[] { } );。 默认情况下,里面加的是游戏主模块名字,这也就是为什么游戏主模块能加进引擎的原因了。
回到上面提的那个问题上,如果自己写的模块没有被主模块引用,打包的时候会发生什么。
通过以上几种情况对比可知,如果自定义的模块没有被引用,打包的时候是不会加载进去的。uproject只决定模块是否对引擎可见,于打包配置无关。
然后根据不同的编译Target,引擎会执行不同的Target.cs,如果没用实现,会执行Target.cs中的基类内容。
模块由Build.cs确定相互依赖关系,如果模块在插件中,加载时机和编译目标在uplugin中定义,如果是游戏项目模块,在uproject中定义。
Target.cs是在打包到对应平台会起作用的文件,如果你的模块没有被依赖,就需要在这里加进去,当然如果模块被其他模块依赖了就不需要再加了。
uproject里除了游戏模块,还会控制插件是否激活,在引擎里更改插件是否激活,uproject就会动态生成对应内容,一般不需要手动添加插件信息。
最常规C++代码提示工具,对虚幻项目提示能力不错,不过仅限于C++部分。
由于UBT用C#编写代码,导致在配置模块的时候没有提示,虽然可以在引擎模块中复制相关代码,但是还是很不友好,这里就要出动强大的Resharper插件了,这个插件的强大之处不仅仅在于能提示C++中的各种宏以及普通C++语法提示,还能做到给Build.cs这种配置文件做出提示,有了它,再也不需要犯愁模块配置问题了,但他的强大不仅止于此:
智能提示头文件是否被使用,如果没用会加灰色(IWWY,include what you use)提示能力简直恐怖
快速在VS中创建Actor,UObject模板类,以前都是从引擎里创建,或者复制一个改很多内容,改错了就一堆奇怪的编译错误。有了这个功能,极大提升开发速度
说到这就足够证明这个插件的强大了,不过它也有问题,就是严重拖慢Visual Studio启动速度,比较耗性能,所以根据大家的需求决定是否使用吧。下载就不说了,需要安装两个插件resharper C++和resharper C#两个。
新工具,提示同Resharper,而且轻量级,现在引擎已经支持,但是我还是习惯用Visual Studio,附上网址,有需要自行申请。
完成本文耗时还蛮久的,但感觉还是有必要写一下,对初学虚幻引擎的开发者们应该或多或少有所帮助,其实还有很多没有写,包括PCH,还有一些宏介绍,但是我更希望大家获取的是我学习这些东西的方法,关于学习方式主要还是参考官网、论坛,然后如果有能力翻墙就看看国外的教程,比国内的要好很多,当然B站和知乎上也有很多优秀的教程,最后如果哪里有错误或者不足还请大家指点。
文末,再次感谢 雪流星 的分享,作者主页:,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。()九游中国娱乐九游中国娱乐