UE4学习笔记:记录一下在学习UE4的过程中感受到的一些有用的杂事
本随笔用于记录我在学习过程中觉得对开发很有用的杂事,包括一些开发技巧、编程思想、计算机知识、项目组织技巧等等杂七杂八的东西,本随笔会不定期更新。
- 配置文件在项目中的作用也很重要,使用配置文件的话可以避免在项目内硬编码,而且可以让使用者通过更改配置文件来改动项目里的功能模块,实现不改动代码的情况下完成多种应用场景,比如说数据库的连接数据保存在配置文件里,项目通过读取配置文件来设置连接的数据库,这样通过更改配置文件就可以实现连接不同的数据库而不需要去改动项目的代码。
- UNIX时间戳是以格林尼治时间(UTC±0)为标准定义的从1970年1月1日00时00分00秒到当前时间的秒数,北京时间和UTC事件相差八个小时,在进行和时间相关的计算时需要注意将北京时间转换为格林尼治时间。
- 降低功能模块耦合度是非常重要的,通过降低耦合度,当某个功能模块出现问题需要修改的时候,我们可以直接修改出问题的模块而不必在意其他被引用的模块,低耦合度组织方法非常适用于维护项目。
- 应该用一种简明且高效的方式来记录目录结构取代杂乱的目录结构,同时文件(UE4里应该称为资产(Asserts))也应该用一种简明高效的方式来为每个文件命名,目录结构和文件名称应该能够最直白的表明该文件(资产)的作用。
- 在程序运行过程中遇到判断的情况(c++的if语句,UE4蓝图的Branch节点等),不要把判断失败的情况留空,也就是说如果当条件判断失败之后应该做一些善后的工作,就算是一段简单的提示也行,而不是要完全留空。
- 项目运行时的层次必须分明。例如在项目里有一个读取数据库数据的模块和一个显示登陆界面的模块,那么数据读取的模块就一定要在登陆界面模块正确执行之后才开始运行,比如登陆成功且选择了正确的数据源之后才开始读取,而不是刚启动项目之后就开始运行数据读取模块;
- 作为服务器使用的模块应该尽可能地布置在可以长期运行的计算机上(例如商业性服务器、不断电的计算机)而不是个人电脑上,要让该服务器尽最大可能地可以被随时访问。
- 对于一些并行启动会有冲突的程序(例如安装程序),应该添加一个检测模块,该模块用于检测是否有另外一个相同的程序在运行,这样可以避免运行同一个程序造成的不良后果(例如一个在执行安装另一个在执行卸载。
- 在一个项目的进行过程中,最好准备一台电脑用于非个人目的,例如上文提到过的服务器安装,以及类似UE4这种可以设置共享DDC功能的软件,这样方便所有参与项目开发的人员使用同一个共享文件或服务器。
- 在编写用户手册的时候,要详细说明每一个模块的功能、每一个API实现的功能以及每个参数的意义,还有所有依赖库的版本号(如果有依赖库的话)。
- 涉及到网络通信的模块应该设置“连接超时警告”功能和“自动尝试重新连接”功能。
- 目录结构里应该避免相似功能的目录,比如说“MOD”文件夹和“ASSERT”文件夹。
- 对于一些提出的问题。回答要尽可能做到详细,也就是尽可能回答问题提出者最想要的答案。
- 在开发过程中要考虑到各个界面显示之间的关联性,比如说刚开始的登录界面在登陆成功了之后需不需要保留、主界面和其他界面(历史回看界面、航班回看界面等)之间的关系:是否需要保存、是否可以从其他界面回到主界面上等。
- 按钮等可以和用户交互的控件需要编写功能来防止用户多次点击之后造成的不良后果。
- 项目打包的时候在“Output Log”窗口里有一段名为“Warning/Error Summary (Unique only)”的信息段,在这段信息段里会显示打包过程中哪里出了问题(Warning以黄色字体显示、Error以红色字体显示),可以很快速地定位错误。
- 在一些后台运行的程序中(例如打包程序、守护程序之类),健全的输出日志功能是很有必要的,尽可能地为每一步操作都设置一个日志输出功能,无论是正确运行或是错误运行,错误运行还要写明是什么地方出错了,这样可以让用户清楚地了解到后台发生了什么。
- UE4 C++函数参数的修饰符不同,在蓝图系统里映射出来的功能也会有相应的变化,由此产生了一个重要的前提:不能使用指针作为参数。
修饰符对应于蓝图中的功能为:
c++函数普通参数映射为蓝图函数的输入变量;
c++函数常量参数映射为蓝图函数的输入变量;
c++函数引用参数映射为蓝图函数的输出变量;
c++函数常量引用参数映射为蓝图函数的输入变量;
本质上就是提供了一种方法可以让函数有多个返回值,普通引用变量由于可以其特性,传递给函数且在函数内修改之后会反映到原来的变量上,在代码层面实现了多个返回值的功能,因此可以看出来UE4将普通引用参数映射成了蓝图函数的输出变量,其他修饰符都是输入变量; - 配置文件的JSON格式需要提前讨论好,这样有助于在后续的开发过程中减少改代码的问题产生。
- 有模块A和文档B,其中模块A提供了某个文件格式的处理方法,其在文档B中有详细的描述,但是在项目新版本中模块A的该方法被弃用了,我们需要及时更新文档B指明该方法在当前及之后的版本中已经被启用,需要想一个高效的方法可以当模块A被改变之后提示到用户需要更改文档B。
- 项目名后面应该跟上对应引擎版本号,这样后期好区分当前项目对应的引擎版本
- 若可使用前置声明,而非头文件,请使用前置声明。
- 包含时尽量细粒化。例如,勿包含Core.h,而在核心中包含需要定义的特定头文件。
- 在文件末尾留下空白行。所有.cpp和.h文件应包含空白行,以便和gcc兼容。
- 避免循环相同的多余运算。将常用子表达式从循环中移出,以避免冗余计算。
- 在某些情况下,使用静态变量来避免函数调用间的整体多余运算(如反复生成局部变量)。
- 使用中间变量来简化复杂表达式。若含有复杂表达式,将其拆分为指定至中间变量的子表达式将更易理解(该子表达式的的命名描述了其在父表达式中的意义)。
- 指针与引用应仅含一个空格,该空格位于指针/引用右侧。使用在文件中查找可方便快速地找到特定类型的所有指针和引用(如“int* Name”或“int& Name”)。
- 避免在函数调用中使用匿名文字。建议使用描述其含义的命名常量:
// 旧样式
Trigger(TEXT("Soldier"), 5, true);.
// 新样式
Trigger(ObjectName, CooldownInSeconds, bVulnerableDuringCooldown);
由于无需查找函数声明即可理解目的,因此此操作可协助普通读者快速理解。 - 创建新插件的时候,在插件的.uplugin文件里该插件的类型会是“Developer”,该类型指定编辑器只会在编辑器模式和开发模式里被启用,项目被打包发布时并不会打包该插件,因此在项目里用到插件的地方会全部失效,把插件的类型改成“Runtime”即可在打包过程中打包该插件,顺便一说插件类型还有一种是“Editor”,即只能在编辑器模式里使用的插件。
- 除了内容浏览器内的各种资产需要分类给文件夹以外,“世界大纲视图(World Outliner)”内存在的资产也需要在世界大纲视图里以文件夹的形式分类,分类规则还需要后续制定。
- 尽量将一个功能给细分化,例如程序需要保存一个数据,保存路径里包含了未创建的文件夹,这时候应该给予用户提示该文件夹不存在,并且可以让用户选择是否在找不到文件夹的时候自动创建文件夹,然后默认是不创建仅提示,目的就是为了防止输入文件夹名字时候意外输错而导致不小心新创建了文件夹让数据保存到了错误的地方。
- 类(无论是蓝图类还是C++类)实例的名字不要和类类型的名字一样,实例对象的名字应该和类类型名字有所区分。
- 蓝图接口和事件调度器都是用来实现蓝图之间通信的工具,但是使用蓝图接口需要知道被调用对象,使用事件调度器需要知道主动调用对象(以将事件调度器和被调用事件绑定)。
- 当一个程序正在安装文件的时候,会因为一些原因在安装到一半时被迫退出,这个时候目录里会残留着程序执行了一半之后的无用文件,本来可以被下载或安装程序清理掉,但是现在因为中途被迫中止而不能被清理,因此我们可以额外设计一个程序,该额外程序可以管理下载程序或者安装程序的所有过程,就算安装程序中途崩溃退出,该额外程序也可以清理残留的文件,同样,该额外程序还可以监控安装程序和卸载程序的运行情况,防止一边安装一边卸载的情况发生。又因为该额外程序是在安装程序之前运行,因此可以保证如果安装程序是中途退出,则一定会清理余下的文件。
- 在程序设计之前和设计的过程中,要尽可能的考虑到程序运行过程中会遇到的问题,越全面越好,并且要给这些问题提供解决的方法,防止一些未解决的问题对后续程序的开发产生不良的影响。
- 项目使用到网络的话需要多方面去检测网络连通状态,最好是从一台拥有独立IP且从没有运行过该程序的电脑上测试项目。
- 在Windows系统里已经被设置为可共享的文件夹,可以在Linux系统里通过“mount”命令来进行挂载从而可以访问共享文件夹:mount
-o username= ,password= 。
在Linux系统里安装samba服务并设置好对应的配置(配置内容待去理解),就可以在Windows的资源管理器的通过地址栏输入“\”来进行访问。 - 在UE4的项目里面,Actor的点击事件需要由启动了“点击事件(Click Events)”的Player Controller才能启用,而默认情况下Player Controller是禁用该设定的(猜想是为了提升性能),为了让Actor能够响应点击事件,需要手动启动“点击事件”。
- 关于两个Widget控件之间如何定义渲染顺序的问题(即两个完全不同的UMG之间的Z Order),在节点“Add to Viewport”上点击下拉框,就可以显示出ZOrder的选项,ZOrder的值越大,则渲染层级越靠后,也就是说显示的时候会更靠近玩家的界面。
- 节点“FInterp To”表示的含义是从“Current”开始,将“Target”分为“Delta Time”份,每一份的增长速度为“Interp Speed”,加上当前的值“Current”,即为当前的返回值。可以如下数学表达式来说明:
x = Current + (Target - Current) * DeltaTime * InterpSpeed
其中X即为返回的值,且X的值一定处于Target和Current之间。 - UE4引擎默认的DDC存储路径为:C:\Users<UserName>\AppData\Local\UnrealEngine\Common,该DDC不会自动删除,而且会占用很大的硬盘空间,所以除非是必要的,经常清除DDC有利于释放C盘的空间。
- 关于更改UE4项目分辨率的问题,当打包的构建配置选择的是“Shipping”时,在蓝图内使用的“Set Screen Resolution”函数可以发挥作用,但是在“Debug”和“DebugGame”选项时却不能够发挥作用,因为类似分辨率这样的选项是由配置文件“
/Config/GameUserSettings.ini”来管理的,在这里可以更改对应的配置。 - UE4里坐标系分为“世界坐标系(World Location)”和“局部坐标系(Local Location)”,世界坐标系的原点是以世界原点为坐标原点的,在该坐标系下进行的变形都是以世界原点为基础;局部坐标系是以某个父类组件或控件的轴心点为坐标原点,在该坐标系下进行的变形都是以该父类为原点进行变形。
- 如果某个UMG是被嵌套在了另一个UMG里面,然后在前者里面涉及到的全部的控件都是基于本身这个UMG空间范围来设计的,那么在获取位置信息的时候可以使用“Slot as Canvas panel”这个节点获取被绘制的尺寸,这样就在因为父UMG的布局改变了导致该子UMG的尺寸改变的情况下,也不会导致子UMG里面的布局被打乱。
- 编程过程中对于占用到内存的模块,及时释放被占用的内存是非常重要的,在一些小程序上不明显,但是在大型程序中如果没能够及时释放被占用的内存,系统内存就会面临被用完的情况。
- 在使用VS构建项目之后,如果要运行带有动态链接库的程序,则需要将相应的动态库(DLL文件)复制张粘贴到对应的可执行程序(exe文件)下,但是这个过程只能够手动执行,目前为了能够实现自动化复制和粘贴操作,合适的方法是右键项目->属性(Properties)->设置属性(Configuration Properties)->构建事件(Build Events)->命令行(Command Line)里设置对应的命令,该命令使用的是DOS命令。还有另外一种方法:选择当前工程,右击"属性" -> "配置属性" -> "调试",在"工作目录"设置dll的路径,这种方法适合于需要在编辑器里面测试程序构建好后运行的结果。
- VS提供了主动复制CPP运行库的功能(但是不包括先前所说的第三方库),只需要在“项目(Project)”->“属性(Properties)”->“配置属性(Configuration Properties)”->“高级(Advanced)”中的“高级属性(Advanced Properties)”目录下,将“复制CPP运行库到输出目录(Copy cpp runtime to outdir)”设置为“是(Yes)”即可,这样在每次构建完成之后VS会自动将需要用到的运行库(例如msvcp140d.dll)复制到输出目录下,但是需要特别注意的是,如果启用了“构建事件(Build Events)”中的“构建后事件(Post-Build Event)”,则该功能将会无效。
- UE4提供了一套涉及范围特别广泛的工具来使用户开发游戏,这些工具包括游戏逻辑、材质效果、音响音效、动画、画面效果、粒子效果等等,这些高效且强大的工具也带来了诸如入门难的问题,想要一次性全部学完然后开始项目的话会很困难,因此我个人建议我自己首先确定一个自己想要做的小应用,然后在实现这个应用的过程中去学习我所需要的功能,在应用中学习,在学习中实践,利用这种方法的话应该会比直接理论学习效果更好一些。
- 合理安排任务是非常重要的,将一个项目分工成合理的小项目,一方面适合不同专业的人完成各自的工作,另一方面也适合每个部分集合成一个完整的项目。
- 关卡蓝图里面可以直接绑定被定义在Actor蓝图类内的事件调度器。即在Actor蓝图里面定义的事件调度器,如果该Actor已经被放置在关卡里面,则在该关卡的关卡蓝图里可以直接通过输入事件调度器的名字来查找到对应的事件调度器并直接定义绑定的内容。
- 在UE4引擎里,部分设定并不能够在编辑器视口里被直接观察到,例如在世界场景构成(World Composition)里被分配给指定图层的关卡,其关卡距离只能在游戏启动时候才会起作用,在编辑器状态下所有关卡都是可见状态,而不受关卡距离的影响。
- 在UE4里使用任何虚幻资产,需要首先将其加载到内存里才能够使用(例如编辑其属性等等),在代码里的表现为如果直接使用find_asset方法去寻找在内容浏览器里面的资产时,并不会找到该资产,因为该资产并没有被加载到内存里面,需要首先调用load_asset方法加载资产之后才能够调用find_asset方法找到该资产。
- 今天学习了如何在VS里面创建和使用动态库(DLL)的方法。创建动态库的方法有两种,声明“__declspec(dllexport)”标识符和定义模块定义文件(.def文件),前者的方法需要在函数声明或者类声明之前标示,后者则是在一个单独的文件里面列出所有需要导出的函数或类,这两种方法各有各的优缺点,可以择一使用。生成的二进制文件除了以dll为后缀名的动态库文件外,还有一个以lib为后缀名的导出函数声明文件(和静态库文件是同一个后缀名,但是性质不一样),供其他vs程序使用的时候首先需要提供lib文件所在的路径,然后是lib文件的全名,这样可以让程序通过编译和链接,然后是将DLL文件放到生成的exe项目文件同级目录下即可使用。前一种生成动态库的方法在现在的编程中更加常见,但是需要注意的是,在头文件里用“__declspec(dllexport)”定义导出的类和函数可以在使用动态库的过程中可以不添加“__declspec(dllimport)”标识符,但是会造成一些额外的寻址过程,造成程序体积增加,因此最好是通过宏定义“#ifdef”和“#ifndef”的方法来定义这两个标识符。
- 在UE4项目中,对于“平行生成”的情况需要谨慎处理,所谓“平行生成”情况,就是指如果有复数个类需要互相引用到对方,那么对于这些类的生成步骤以及获取所需类对象的步骤需要有额外的操作,例如说有一个类A,一个类B,类A有十个实例对象,类B有一个实例对象,类A中的每一个实例对象都需要获取到类B的对象,类B的实例对象也需要获取到全部十个的类A实例对象,那么在互相获取到地方之前就不要进行后面的步骤,以防出现空指针的情况出现,因此使用“Valid”节点来判断是否获取到所需的对象就变得非常重要。
- 在打开UE4项目之后发现蓝图A里面的引用全部掉了,然后在项目外通过复制粘贴所需的资产之后,回到蓝图A里发现引用还是没有引用上,这个时候只需要右键蓝图A的资产,然后在Asset Actions分类里选择Reload即可重新搜索引用。
- 在对某个问题下结论了之后,最好再根据这个问题多分析分析,因为当前结论可能会是错误的结论,例如本来是代码的问题,我个人却把车辆重叠和突然间运行速度变慢的问题归结为CPU多线程处理太慢的问题,实际该问题是线性插值导致的问题。
- 在读取了DDP之后如果生成新的DDC,新DDC会被放到Local节点指定的位置上。
- 在配置文件里设置的节点除了几个默认的以外(Local、Shared等),可以自定义设置节点,当自定义设置节点之后可以实现分别读取DDP,比如说地形关卡单独生成一个DDP,建筑关卡单独生成一个DDP,然后可以让用到的部门去读取单个DDP,而不需要把这两个关卡的DDC打包成唯一的一个DDP。
- DDC的Local(还有其他)节点设置里面有个“ReadOnly”选项,如果将该选项设置为true,则在项目运行过程中生成的所有DDC都不会被保存到本地上,因此造成的情况就是每一次启动都要重新生成DDC,将该选项设置为false即可让运行过程中生成的DDC保存到本地上。
- 打包后的项目不会运行构造脚本(Construction Script)里面的逻辑,猜测是因为该构造脚本仅仅只会在Editor模式下使用到。(该问题还没有得到实际的证实,因此在这里仅作一次记录)
- 打发行包的项目也可以像打开发包的项目一样通过设置配置文件来更改项目,只是其存放配置文件的路径在:
C:\Users\<用户名>\AppData\Local\<项目名>\Saved\Config\<打包明台名称>。 - 当我们的项目需要使用一个Actor的组播函数时,且网络情况不良好的情况下,我们在生成这个Actor之后立即使用组播函数的话会让这个函数的“组播”功能失败,也就是会变成只有服务端调用了函数而客户端没有调用的情况,解决方法可以通过“延迟调用”来解决,例如使用定时器、使用Tick隔数帧后再调用等。总而言之就是要加长“生成Actor”到“调用组播函数”之间的间隔。
- 在使用C++动态生成MediaPlayer相关的资产并在项目里播放视频的时候,需要在MediaTexture使用SetMediaPlayer函数设置到播放器之后调用UpdateResource函数才不会报“check(bOk)”的错误。
- UE提供的TreeView控件(包括ListView控件)有时候会出现“需要按两次下才能导航到正确按钮”的问题发生,发生这种问题的原因其实是没有使用View控件的“SetSelectedItem”这类函数为控件设置已选择的项,从而造成内部的导航不正确。所以在动态设置导航的时候我们可以在SetFocus之后再SetSelectedItem,从而让View控件的导航能够正确运行。
- UE提供的TreeView控件(包括ListView控件)在源码里面的Slate控件里面将引擎的导航接管了(即重写了“OnNavigation”函数),因此Widget原本的导航函数(即能够设置输入Up、Down时进行的行为)不会被调用的,再加上View控件自己的导航仅仅只是会根据View里生成的数据数组,然后根据下标去寻找下一个\上一个下标,然后检测合法性,合法性通过就对对应下标的数据进行导航操作,并不会去检测对应下标Displayed Widget的Enable或者Focusable进行检测,所以在部分功能上View控件自带的导航会不满足需要(例如项目里面的任务UI的任务分类,如果使用手柄进行导航的话就会直接导航进还没有解锁当时显示出来的每周任务上)。如果说不想在引擎源码上进行重构,则可以在每一次导航到一个按钮的时候(例如在函数OnFocusReceived里面)去对导航方向上所有的按钮进行一次Enabled可用性检测,然后找到可以用的按钮然后再进行Focus和SetSelectedItem。
- 现在项目里面通过客户端与服务器互相发送心跳信息来检测网络连接是否正常,如果客户端在五秒内没有接收到服务器发送过来的心跳信息的话则报网络连接错误。但是这个功能存在一个问题,就是如果程序员在调试过程中在代码里打断点的话,中间暂停的时候还是会被计算到心跳过程中,这个时候当程序员取消断点并运行项目的时候,检测逻辑会把打断点那段时间也计算在心跳检测中,而且很有可能会超时然后报网络连接错误。解决方法就是可以使用“WITH_EDITOR”宏来暂时关闭该功能,或者在本地代码上把报错调用的委托暂时注释掉。
- 运行时按下~键,输入p.visualizeMovement =1可以将玩家的运动信息可视化。
- UMG中的Construct事件并不是在“Create Widget”之后立马执行的,而是在添加到玩家视图(AddToViewport、添加到其他已经显示的UMG上面等)的时候才会被调用。事件“OnInitialized”才真正实现了在“Create Widget”时候被调用的“初始化函数”的功能。需要注意的是,如果使用“Construct Object”去生成一个UMG的话,是不会调用“OnInitialized”事件的,而只会在添加到玩家视图时调用“Construct”事件。可以得出如下规律:
- 如果使用“CreateWidget”节点生成UMG的话,会调用UMG的“OnInitialized”事件,然后在“AddToViewport”的时候依次调用“PreConstruct”和“Construct”事件。
- 如果使用“ConstructObject”节点生成UMG的话,不会调用“OnInitialized”事件,但同样会在“AddToViewport”的时候依次调用“PreConstruct”和“Construct”事件。
- UMG中在Designer窗口布局时需要注意,在左下角的“继承树(Hierarchy)”中越靠下的控件越在后生成,也就是说一个控件的行为会受到其上级控件的影响。例如有控件A和控件B,控件B为A的子控件,这个时候如果控件AB两者的可视性都为“Visible”且尺寸都一致的话,控件A很有可能会拦截到本来应该给控件B的响应。需要注意的是并不一定要为子控件才会有这样的影响,就算是同级的控件,排列在上方的控件也会拦截传给排列在下方的控件的事件。
- 之前项目遇到一个问题就是在部分电脑上不能播放开头动画,解决方法是把视频源文件(也就是mp4文件)的分辨率调低一些,当前分辨率为3840*2160,猜测是分辨率太大了导致部分电脑解码出现问题。
- 使用UE4(UE4.27)自带的媒体框架时需要多用API进行很多组合尝试可能才会实现自己需要的功能。例如如果需要实现Open媒体源之后让纹理播放第一帧而不是白屏,需要在“OnMediaOpened”回调中先“Pause”然后再“Rewind”,而不是通常的直接“Rewind”再“Pause”。还有因为媒体可能会打开失败或者打开需要时间,因此最好使用回调(如上面的“OnMediaOpened”)来监测媒体状态而不是用循环Delay。
- UE4(UE4.27)自带的媒体播放器在PS5平台上表现不是很好(甚至在PC平台上也并没有优秀的表现),因此可以使用引擎自带的插件“Electra Player”作为视频播放的解决方案,在插件页面勾选“Electra Player”之后(其他两个插件也会在重启后自动勾选上),在File Media Source的页面里面把编码器选为“ElectraPlayer”(或者设置成“Automatic”也可以,引擎会优先选择“ElectraPlayer”作为解码器)即可,后续播放的视频将会以“ElectraPlayer”的解码器进行播放,经过简短的测试——例如项目里的开场PV在用原解码器时如果播放之时处于低帧率状态,点击编辑器之后视频播放依然会进行卡顿,但是使用新解码就不会出现这样的问题——之后,可以有效的解决之前存在的问题,甚至是可以设置第一帧画面:让视频默认播放,然后绑定MediaPlayer的委托OnMediaResumed,最后在回调事件里执行暂停逻辑,即可实现让视频停止在第一帧的功能。
- 当处在编辑器时,如果发现鼠标移动到图标显示ToolTip、两个显示器中其中一个窗口移动到了另一个显示器发生帧数下降的问题时,可以通过把N卡控制面板里面的“最大帧速率”设置为“关”来解决。
- 使用UnLua时候最好不要用到Lua本身的全局变量,因为可能会出问题,当前项目就因为使用了全局变量之后导致图文教程高亮框结束之后一直在循环调用,造成卡死的假象。
- API“Set Focus to Game Viewport”可以让当前玩家的导航Focus到Game Viewport上,应该可以解决玩家没有Widget去Focus时候的问题。
- 在UE4项目中,如果同时存在SSR和Reflection Capture时,会优先使用SSR,因为SSR的效果更准确,但是缺点是如果屏幕不存在物体的话则SSR会失效。
- 蓝图节点“Switch Has Authority”通常用来判断当前运行进程是DS服务器还是客户端,如果进程是DS服务器则会输出“Authority”(假设Actor是在服务器上生成的),是客户端则输出“Remote”,但是需要注意输出“Remote”的进程不一定就是玩家启动的客户端,也可能是另外的玩家启用的客户端在玩家客户端上模拟的结果,也就是通常的“Simulated Proxy”,这种情况也会输出“Remote”,这就造成了如果使用玩家的“BeginPlay”函数去生成UI时,如果只是通过“Switch Has Authority”节点来进行判断的话,本地玩家上的Role为“Autonomous Proxy”,网络玩家的Role为“Simulated Proxy”,都同样可以生成UI,会导致在本地玩家的客户端上生成了两个UI,这个时候只需要使用“Get Local Role”来进行判断是否为“Autonomous Proxy”即可让玩家只在自己的客户端上生成UI。
如上所说,即使Actor蓝图里该节点返回“Authority”也并不说明当前进程就一定是服务器,因为如果一个Actor是在客户端上生成,那么在客户端的Actor蓝图里调用“Switch Has Authority”也依然会返回“Authority”,因此节点“Switch Has Authority”更多是表达一种“是否拥有此Actor权限”的说明。 - 如果需要让一个UI拦截鼠标事件不再往下传播的话,可以实现该UI的“OnMouseButtonDown”事件并返回“Handled”变量,这样的话可以让鼠标事件被该UI截获从而不在往下转播,也就不会造成突然鼠标消失了的情况。
- “TeleportTo”方法(蓝图节点名称为“Teleport”)可以让物体“瞬移”到指定位置,该瞬移并不在意起始点到目标点之间是否存在阻挡,但是如果目标点存在阻挡的话,则该方法会尝试着微调物体目标点位置让物体不会被阻挡(不过该微调很小),微调后依然存在阻挡的话则不会让物体进行移动,除非勾选上“NoCheck”选项。“SetActorLocation”方法默认则会让物体直接移动到目标点,即使存在阻挡,使用“Sweep”选项则会检测起始点到目标点之间的阻挡,该方法会让物体在第一次接触到阻挡时停下。
- 在开发网络项目的时候,如果有需要记录物体的初始信息(例如物体初始位置)的时候,最好不要在BeingPlay里面记录初始信息,最好在Construct脚本里记录,因为网络同步存在延迟,Server上的物体记录了正确的位置信息然后开始移动一段距离之后,客户端的物体可能才刚创建完并执行BeginPlay,这个时候记录的初始信息是错误的,所以最好在Construct脚本里保存初始信息。
- 有时候UI表现会有“圆角”、“切角”的要求,这个时候可以使用“Retainer Box”控件搭配材质来实现。除了圆角效果,还可以让子控件的所有元素实现类似渐变等材质都可以实现的效果。
- Dynamic Entry Box的功能和ListView、TreeView的效果一样,可以动态创建条目,使用方法上前者比后几者简便,也因此没有做过性能优化,适合做需要有列表功能但是又不需要ListView时的场景。
- 在声明函数的参数时,按照UE的编码标准,最好在每个输入参数前加“In”前缀,目的是防止当参数名字和成员变量名词重复时可以避免编译器错误。即使c++允许函数声明时的参数名和定义时的参数名可以不一致,但是函数声明和定义时都需要加“in”或“out”前缀。
- 不像其他类型的委托绑定函数,
BindRaw或AddRaw绑定的原始c++对象因为没有反射系统,所以不能够通过IsBound()函数来判断对象是否存在。这个时候可以使用UE的智能指针TSharedPtr来指向该原始c++对象,然后再使用BindSP或AddSP来添加委托。 BindStatic或AddStatic可以用来绑定函数静态成员或原始c++全局函数。- 复制函数(ReplicatedUsing指定的函数)一共有多种形式,其中就有无参及一个参数的形式,其中需要注意的是,一个参数形式的赋值函数传送进来的值并不是更新后的新值,而是改变之前的旧值,如果用户不需要复制变量的旧值的话,则可以使用无参的形式。
- 在设置ViewTarget时,由于Actor和PlayerController分别覆写了CalCamera函数,因此如果玩家生成一个带摄像机组件的Pawn时可以正确获取到摄像机位置,但是设置带摄像机组件的PlayerController时并不会获取到正确位置。
- 使用UE4的控件ComboBox时,可以通过其定义的委托“OnGenerateWidget”来自定义生成想要在下拉菜单里使用的Widget,从而自定义想要的下拉菜单项样式。
- 在使用Enhanced Input系统时,Input Action(IA)或Input Mapping Context(IMC)都可以设置Trigger和Modifier,如果IA和IMC都设置了Trigger,则只有IMC里的Trigger起效;如果IA和IMC都设置了Modifier,则这两个Modifier会“叠加”,也就是首先应用IMC的Modifier,然后把结果传给IA的Modifier进行二次处理。
- 使用Git Checkout源码时候需要注意是否在“release”分支而不是“master”分支,因为引擎源码默认使用的是“Release”分支,如果检出到“master”分支上可能会没有我们需要的东西。例如插件CommonUI。
- 在开发UMG时,Horizontal Box在添加子控件的时候默认是会向右变宽的,这个时候我们可以通过设置“对齐(Alignment)”属性的X为1,这样就能让该控件右对齐,然后添加子控件时则可以实现向左变宽。Vertical Box同理。
- 蓝图继承蓝图时需要注意事件图标里默认的几个事件是未启用状态(如Tick、Construct等),如果没有在子蓝图里把这几个事件激活的话,是不会执行父蓝图的逻辑的。
- 某些时候AssetRegistry的函数GetAssets会搜索不到原本有资产的文件夹(例如在Standalone模式或Launch启动项目时),这个时候可以使用AssetRegistry的函数ScanPathsSynchronous来执行重新搜索的功能,之后再执行GetAssets函数就能获取到对应的蓝图资源了。(该条建议还是ChatGPT4告诉我的……)
- Actor同步变量时客户端接收同步后的变量有可能在BeginPlay前也有可能在BeginPlay后,也就是说某个同步变量VA,会在BeginPlay里从本地读表设置,这个时候如果我们的网络环境比较好,则同步会发生在BeginPlay之前,这个时候同步了变量但是因为BeginPlay又会从本地读表并赋值,会把同步过来的值给覆盖掉。如果同步变量设置成“InitiaOnly”的话,该变量就再也不会被同步成正确的值了,这部分需要注意一下。
- UserWidget上面的函数“OnMouseButtonDown”可以用来实现“鼠标右键”类型的功能,同时可以通过重载该函数来防止鼠标滚轮鼠标按键事件继续往下传播的问题。
- 蓝图纯函数(Blueprint Pure function)虽然说可以不需要连接执行引脚,但是该类型函数的每个输出引脚被连接的节点被激活时,都会执行该纯函数,例如某个纯函数返回一个结构体和一个bool值用于判断是否合法,则判断bool值返回值时会执行一次该纯函数,返回结构体时会再次执行一次该纯函数,因此需要注意如果该纯函数查询量过大的时候最好将其返回值保存下来供后续逻辑使用,而不是每次都要执行一次该纯函数。
- 大地图是一个DS,副本是一个DS,然后为了在大地图显示副本boss血量的信息,每次boss血量变更时都会发送RPC给大地图DS,这样网络性能消耗会特别高。这个时候可以先看一下需求,就是为什么“要在大地图显示副本BOSS血量”,在这个例子里是因为“需要让大地图的玩家知道副本的玩家什么时候会打死boss然后出副本”,为了实现这个需求我们可以让副本boss的血量在四分之一的时候发一次RPC给大地图,大地图显示BOSS血量的方式不再是实时,而是显示四个格子,当前有几个格子说明boss血量还剩百分之几。为了更准确的显示什么时候打死boss,也可以把显示的格子再细分一下,比如说十个格子,然后boss血量每减少十分之一就发送一个RPC,这样的网络性能会比之前好很多,然后同样也能实现需求。
- 对于需要成对执行的操作(例如new一个对象的时候需要delete对象,还有Register一个事件时还需要Unregister事件)时需要注意执行的位置,例如生成时执行绑定操作然后销毁时执行解绑操作,如果没有注意执行的话可能后续造成读空指针的问题,例如对象绑定了事件但是没有解绑,但是对象被销毁了,很可能后续被调用的时候会变成读空指针。
- ue中FastArray对数组需要网络同步的情景下有优化加持,后续需要了解一下FastArray的用法。
- 有时候错误信息并不一定会准确,例如一个
TSharedRef<SViewport>是可以转换到TSharedRef<SWidget>的,因为SViewport是SWidget的子类,但是如果当前没有包含Widgets/SViewport.h头文件的话VS会报Convert转换错误的信息,但实际上真正的错误是类未定义。 - UE的物理系统中每个物体有“模拟物理”和“启用重力”两个参数,顾名思义这两个参数都启用的时候可以模拟物体在有重力场景中的状态,启用“模拟物理”关闭“启用重力”则可以模拟物体在无重力场景中(例如宇宙)的物理状态,那么当我们关闭“模拟物理”时“启用重力”还会不会生效,虽然我们一般会理解为既然不再模拟物理系统则重力效果也不会应用,实际上当我们给一个物体关闭“模拟物理”并开启“启用重力”后,如果把该物体当成另一个物体的子物体时,该物体的重量就会影响到父物体的重心。详情可以参考视频。
- 不同于UUserWidget有Destruct和NativeDestruct事件,普通的UWidget并没有这些函数可以在UMG移除时候调用,一般情况下UMG移除时使用RemoveFromParent函数,这说明UMG本身只是从窗口上移除了,并没有实际销毁该UMG,因此也不会调用UWidget上面的BeginDestroy函数,这个时候如果我们需要在UMG被移除的时候让UMG里面的子控件,特别是纯UWidget控件而不是UUserWidget控件知道我们已经被移除的话,可以通过在UWidget的子类里重载ReleaseSlateResources函数来确定,该函数会在UserWidget调用RemoveFromParent时调用。
- C++定义蓝图委托时,如果参数用的是
TArray<FMyBlueprintableStruct>,引擎在蓝图执行绑定的时候会报错,这个时候需要使用const TArray<FMyBlueprintableStruct>&的形式才能在蓝图里正常使用。 - 当一个蓝图继承了一个c++子系统时,引擎会为c++子系统和该蓝图子系统分别生成一个实例,用户在蓝图调用了蓝图子系统而不是c++子系统的话,其变量并不会保存在c++的子系统里面的,而是蓝图子系统实例里面。
- 使用命令
showdebug enhancedinput可以对增强输入系统进行调试。 - 当项目需要和服务器交互时,除了正式服务器以外,最好再准备三个服务器,第一个服务器就是开发服务器,大部分时候客户端用的都是这个服务器,该服务器实装的都是一些已经稳定下来的需求,第二个服务器是每个服务器端人员都有的个人服务器,这样当服务器或客户端需要更改需求时可以使用这个服务器和服务器端人员对接,当新需求已经实现完成,但是还没有被客户端多方面测试过时,这个时候就需要第三个服务器,服务器端人员把实现的功能实装到这个服务器上,既不影响服务端人员自己继续在第二个服务器上开发功能,也不会影响到使用第一个服务器的其他客户端,然后需要测试新功能的客户端就可以使用第三个服务器来多方位测试C/S功能。
- 在使用UCameraShakeBase时,其中Time分类有Duration、BlendIn和BlendOut设置,当Duration的值小于0时混入和混出的设置都没有任何效果。当大于0时,混入和混出的时间是计算在Duration内的,而不是先混入然后再Duration最后再混出。而且当Duration为0时,表示该抖动持续无限时间,这个时候我们在停止摄相机抖动时必须勾选上“Immediately”,否则该抖动不会停止。详细代码可以查看
UCameraShakeBase:Update(float DeltaTime)函数。 - 在使用Tick编写逻辑的时候需要注意该逻辑如果依赖于另一个Tick,则逻辑的执行会依照这两个Tick执行顺序的不同而不同。例如我们需要编写一个在Tick里通过摄像机位置进行射线检测获取可交互物的逻辑,一般情况下我们都会直接在Character或者Controller的Tick里写逻辑,但如果直接这样写的话会发生问题:实际射线检测的位置总是落后一帧于摄像机的位置更新,发生该问题的原因在
UWorld::Tick(ELevelTick TickType, float DeltaSeconds)里引擎在对关卡里面的所有Actor执行Tick时会有一个先后顺序,而摄像机(准确来说是PlayerCameraManager)的更新(在UWorld::Tick函数1578行)是在其他所有Actor更新完毕之后再进行更新,也就是说我们在Character或Controller的Tick函数里获取的摄像机位置是当前Tick还未更新到的位置,因此会发生位置不同步的问题。解决方法有多种:- 设置Character或Controller的Tick组。在
UWorld::Tick函数里可以看到Tick组的Tick顺序是:PrePhysics、StartPhysics、DuringPhysics、EndPhysics、PostPhysics、UpdateCameraManager、PostUpdateWork和LastDemotable。Actor默认的Tick组都是PrePhysics,其中UpdateCameraManager并不是Tick组,但是可以看出来CameraManager的更新就是在Actor默认Tick组PrePhysics后面,所以我们把Actor的默认Tick组设置成PostUpdateWork或LastDemotable即可,这两个Tick组有什么区别还需后面再研究一下。 - 重载UpdateCameraManager函数。既然我们知道我们的业务逻辑要在Camera更新之后再执行,那么我们可以在PlayerController上重写函数,抛出回调或直接在函数里编写逻辑都可以。
- 设置Character或Controller的Tick组。在
- 在编写自己的自定义Slate时,用
SNew或SAssignNew生成的Slate一定要在ReleaseSlateResources里面手动释放掉,否则容易出现内存泄漏(Memory Leak)的问题让引擎打到断点ensure(!PreviewSlateWidgetWeak.IsValid()); - 在使用Slate版本的HorizontalBox或VerticalBox时需要注意,蓝图版本的这两个控件的“Size”属性默认都是Auto,但是Slate里默认为“Fill”,有需要的话我们在SNew时需要调用
AutoSize来指定该属性。 - 在使用
XXXTraceMulti进行射线追踪时需要注意,如果射线检测到了多个物体,但是第一个物体已经是Block状态了,那么HitResult返回的结果中也只会有第一个物体,并不会包括后续的物体。 - 引擎的射线检测提供
XXXTraceSingleByChannel和XXXTraceSingleForObjects两种类型,当使用Channel类型的射线检测时,函数会把我们传递的参数TraceChannel直接于物体上的Collision->Collision Presets->Trace Response进行比较,通过在Trace Response设置的类型(Ignore、Overlap或Block)来确定最终的检测结果;而使用ForObjects类型的射线检测时,函数只会把我们传递的参数ObjectTypes和物体上的Collision->Collision Presets->Object Type进行比较,而不再比较Object Responses,Object Responses`是用来给物体的碰撞模拟进行检测而不是射线。 - 引擎的材质系统提供了“自定义模板(Custom Stencil)”的方法来实现一个高级的材质功能。开启自定义模板功能需要首先在项目里开启“Project Settings->Engine->Rendering->Postprocessing->Custom Depth-Stencil Pass->Enabled with Stencil”,然后在静态网格体组件里“Details->Rendering->Render CustomDepth Pass”,同时还需要给CustomDepth Stencil Value选定一个1-255的值,用于指定模板值,该值可以在后处理材质中通过材质节点SceneTexture:CustomStencil获取,该节点的Color输出引脚输出的就是模板值,通过对比区分该值可以实现如遮挡显示等高级功能。个人对自定义模板的理解,就是材质系统会把给定模板值的材质保存到模板缓冲区中,后期可以从这个模板缓冲区获取到对应值的材质,从而实现更多的功能,也因为需要用到缓冲区,因此使用该特性会有一些性能损耗,具体性能损耗取决于实际的实现方式。下图为一个简单的后期处理材质,使用该材质可以实现让所有的模板值为2的物体被遮挡后依然显示出来:

- 对于后期处理材质,该材质类型没有办法直接使用SceneColor节点,因为该节点是为Domain为表面(Surface)的材质设计的,但是可以通过SceneTexture:PostprocessInput0来获取,该节点获取的就是应用后期处理材质后的SceneColor。
- 如果使用引擎自带的资产包“StartContent”里的资产去进行拼接,例如用多个Floor模型拼接成一个大的Floor时,在渲染光照之后会出现“接缝”的问题:

观看地面可以很明显的看到一些接缝。
会出现这个问题的原因是模型的光照UV不正确造成的,一般情况下光照UV会是这个样子:

每个UV岛都是尽量展开在UV框架里面的,为的是更好的接收光照烘焙信息,但是如果模型本身是拼接在一起的,就会造成模型拼接了但是光照UV没有拼接,因此实际烘焙出来的模型光照贴图看起来会不连续,解决办法就是让接收到光照的一面的UV尽可能的展开,如下图:

引擎限制了光照UV不能重叠,因此我是把接收光照面的UV展开到最大,其他面的UV缩到最小来实现的。
当然,你也可以只使用一个面片来拼接,这样接收光照的面的UV就已经展开到最大了。
世界设置(World Settings)里的Static Lighting Level Scale值也会对“接缝”产生影响,该值越小则接缝越不明显。 - 要想动态更改间接光照,可以在后期处理体积的渲染功能(Rendering Features)->全局光照(Global Illumination)选项里进行更改,可以动态更改间接光照颜色甚至是显示或隐藏间接光照。
- 在编写材质时候需要注意“高光(Specular)”值的设置,大部分时候高光值都可以不用设置或者设置为0.5(默认值就是0.5),UE引擎使用的PBR流程是金属粗糙度工作流(Metal Roughness Workflow),因此大部分时候我们不需要更改高光值。
- Lightmass的“Num Indirect Light Bounce(间接光照反弹次数)”会静止物体和动态物体(包括固定物体)产生不同的光照影响,原因是反弹次数越高,光照反射到静止物体上的可能性和次数也就越多,因为静态物体的光照是被烘焙出来的,受反弹次数的影响,而动态物体并不参与到光照烘焙当中来,动态物体的光照仅仅只受光源的直接影响,所以引擎里面使用了“体积光照贴图”,让光照烘焙时生成一些采样点,采样点记录了附近的光照烘焙信息,然后这样采样点会把光照信息附加到动态物体上,这样可以间接地实现让光照烘焙信息影响到动态物体,因此如果在设计关卡时发现某些静态物体的光照是正常的,但是固定和动态物体对比静态物体光照信息会出现问题(比如说比静态物体的光照“暗”),这个时候我们可以调节体积光照贴图来改善这个问题,例如降低“体积光照贴图细节单元格大小 (Volumetric Lightmap Detail Cell Size)”可以让场景生成更多采样点,从而在动态物体上生成更为精细的光照效果,此外还可以适当增加 "体积光照贴图最大砖块内存量 (Volumetric Lightmap Maximum Brick Memory Mb)"的值,防止因为内存量限制导致被剔除造成光照不正确的问题。
- UMG的“Play Animation With Finish Event”接口在4.27版本里会因为游戏暂停后不能触发Finish Event,因为
UWidgetAnimationPlayCallbackProxy::OnFinished里会在UI结束时设置一个定时器来调用Finish Event,但是计时器在游戏暂停时是不会执行的,因此UMG里也不会执行Finish Event,可以通过绑定Animation的Bind to Animation Finished接口或重写UMG的OnAnimationFinished事件来实现同样的功能。 - 使用Instance Static Mesh组件拼接出来一个地面时,很有可能因为每个实例之间距离太近甚至“重合”导致拼接出来的模型在离开视野范围后阴影投射有问题:

在视野范围内的正确阴影投射:

解决这个问题的方法就是在拼接时候让每个实例之间增加一点点间距,防止实例意外重叠即可:


浙公网安备 33010602011771号