最近新开了项目,大概总结了之前项目的一些问题,列举了一些UE开发项目的设计规范和代码标准(代码标准这个词看起来太严肃了,写代码的习惯是一个比较主观的概念,其实叫代码约定更好,但是在组内推广还是要有严格执行的要求)。本篇文章会持续更新和整理,欢迎指出问题和交流意见。
设计规范
- 所有设计的逻辑和类要成为可配置的,在不让玩家重新安装的基础上根据需求可以替换掉指定逻辑(尤其是替换掉C++实现的的逻辑)。
- 写真正的业务代码之前第一步要设计接口作为中间层,设计接口之后不要实现,必须先提交接口(可以没有任何逻辑,只是打印log都可以),可以供其他人使用,避免依赖工作间的等待。
- 保持接口的稳定和可扩展,接口不能随意变动,命名应该简洁直观,接收参数应该齐全(所有依赖外部的参数都要传递),保持接口的无状态。
- 所有写的业务依赖的工具函数要抽出作为通用的工具,比如之前写的下载Pak列表的功能,其中包含下载任意文件的功能,要抽出作为通用的代码,类似的一个大功能包含一堆小功能的,都可以抽象出来的都要单独写成可以随意调用的函数或者对象,不能够依赖调用顺序,最大限度地降低依赖。
- 所有涉及配置读取/初始化之类的操作必须使用统一的通用方法,不可以每个对象自己写一个流程,会变的很混乱。
- 待补充。
代码规范
基础要求是需要执行UE的代码标准:Coding Standard
额外的扩展要求:
- 每一个USTRUCT、UObject(可除去U)、函数库也必须单独位于一个同名的源文件,且函数库的命名为以
Flib
开头; - 所有的头文件中所包含的其他头文件必须区分引擎和项目,在包含头文件里使用
// Project Header
和// Engine Header
注释; .h
中应该只包含头文件中用到的符号声明的头文件,不要把所有的头文件都写到.h
里;- 项目里所有的类,不管具体是在蓝图里实现还是C++实现,必须要有C++的基类和C++的接口,注意基类和接口只写通用的东西,不要写具体的业务流程。
- 所有的非内部成员(不暴露给外部使用的成员)都写成UFUNCTION,并且加上BluepringNativeEvent标签,属性也同理,加了UFUNCTION和UPROPERTY可以被反射使用以及可以被蓝图继承;
- UCLASS必须加上Blueprintable/BlueprintType,USTRUCT必须加上USTRUCT,使其可以被蓝图继承和访问;
- C++写的类的数据成员的初始化顺序必须要与声明顺序一致;
- 函数内接收的任何参数都必须在自己的函数内做检测,如判空和clamp操作,不能够相信外部输入是合理的;
- 在有可能会执行失败的函数中必须要提供返回值,在Get函数中同样需要(错误码或者bool都可以),并且不可以直接在执行结尾
return true;
最终执行的结果要根据前面可能会执行失败的逻辑是否成功; - 禁止所有的static成员的在全局作用域初始化操作(尤其是获取引擎数据的初始化),只允许字面值类型的static初始化,如
static FString Name = TEXT("helloworld")
等; - 对于在局部作用域创建的资源(裸内存或者文件访问)要使用
ON_SCOPE_EXIT
来确保释放逻辑; - Lambda引用捕获的对象不可做为异步逻辑操作的对象,如在一个函数内创建了lambda对象引用捕获了局部变量,将其传递给异步逻辑,这种操作是禁止的。
- UE中默认中禁止
dynamic_cast
,在UEC++的范畴内不要使用标准C++的转换,可以使用Cast<>
; - 继承自UObject的对象统一使用
GENERATED_UCLASS_BODY
,并需要创建出XXXXX:XXXXX(const FObjectInitializer& InObjectInitializer)
的构造函数; - 函数内如果有可能具有某种竞争逻辑,则需要使用
FScopeLock ScopeLock(&CriticalSection);
为当前作用域加锁,防止资源竞争; - 尽量使用防御式编程的Impl机制
- 项目中完全禁止使用异常,
bForceEnableExceptions
不可以被设置为true; - 用C++创建的结构成员必须要提供
==
的操作符 - 用C++创建的结构成员如果要自定义构造函数,则需要提供默认构造函数、拷贝构造函数、移动构造函数、赋值操作符的实现,偷懒可以使用
A()=default;
,如果使用default就需要确保类内没有引用某种资源; - 用C++创建的供外部使用的结构成员必须使用
UPROPERTY
并且需要提供BlueprintReadWrite
属性; - 所有暴露给其他模块使用的类必须有导出符号
MODULE_NAME_API
- 所有的插件和单独的模块在
build.cs
中必须写OptimizeCode=CodeOptimization.InShippingBuildsOnly
,方便调试。 - 所有Editor模块的插件加载顺序必须先于
Default
,可以使用PreDefault
或者PostEngineInit
; - 在代码中使用的宏统一需要由
build.cs
中来创建,使用PublicDefinitions
来添加。 - 所有依赖的第三方代码必须要支持最少
Windows/MacOS/Android/IOS
四个平台。 - 代码文件的编码格式统一为UTF-8
- 注意不同平台之间对C++特性支持的差异,写代码时不能以单一平台的编译结果为准;
- 两个模块之间不可以互相包含比如,A包含了B,B又包含了A,这种循环包含在Mac上会编译失败;
- 如果整个项目中都要用到一个宏,必须在
target.cs
中使用ProjectDefinitions
来添加,不要把同一个宏在每个模块都定义一遍; - 所有与路径相关的操作都需要使用
FPaths
,不要自己写,注意路径使用FPath::Combine
拼接之后要对其使用FPath::MakeStandardFilename
。 - 包含外部模块的头文件路径必须是相对于该模块的
Public
全路径。 - 对于不同平台不同处理的跨平台代码要参考
FPlatformMisc
的实现方式(参考UE4:PlatformMisc的跨平台实现) - 当需要将数组(
TArray
)的原生数据通过指针访问时,要注意元素动态增长导致的Reserve
问题。 - 不要在
*_Implementation
的函数里调用Super
本函数的无Implementation
版本,会无限递归。要使用INTERFACE_NAME::Execute_*
这种方式调用,不然在Unlua里调不到lua里重写的函数。 - 类成员模板函数的特化版本不要写在类声明内,在Android上会有错误。
- Runtime模块的依赖模块不可以包含Developer和Editor的模块,如果需要使用Editor或者Developer的功能,则在项目或者插件下新加一个Editor或者Developer的模块。
- 提供给IOS使用的静态链接库必须要启用bitcode。
- 不要通过
TSubclassOf<>
的对象调用GetClass()
,它获取到的是所管理的UClass
的Class
,要获取TSubclassOf
所管理的UClass
,直接调Get()
,因为TSubclassOf
重载了operator->
操作符,所以才会有这样的歧义。 - 因为需要使用UnLua,而UnLua不支持导出非动态代理,所以项目中可以使用的Delegate类型为
Dynamic Delegate
和Dynamic Mulitcast Delegate
。 - 不要使用
std::stream
等C++原生读取文件的操作来读取打包到Pak里的文件,要使用FFileHelper::LoadFileToArray
等API。 - 如果有些反射属性只想在Editor下存在,不要使用
WITH_EDITOR
,要使用WITH_EDITORONLY_DATA
。
跨平台编译代码规范
因为UE在不同平台编译时使用的编译器、支持的C++标准版本、以及UE本身编译规则都有区别,相同的代码在不同平台编译时会有不同的规则,在编写代码时需要注意一些问题。
namespace要打全(如引用protobuf生成的代码)
避免隐式类型转换(如enum->int)
UE中已有名字的宏不要重新定义,如
PI
等基础数学宏头文件的引用使用模块相对路径的方式,不要指定全路径(XXXX/Public/xxxx.h)
Runtime模块不要包含Editor和Developer的模块(如果某些Runtime的模块需要在Editor和打包时有一些区别的操作,需要在build.cs中检测
bBuildEditor
来包含模块,在CPP代码中需要加WITH_EDITOR
检测)使用UE中的跨平台功能,不要直接引用到某个具体的平台头文件,如
PlatformMisc
,不能直接引用WindowsPlatformMisc
不要在Module的build.cs中使用
PublicAdditionalLibraries
添加另一个Module中的链接库文件代码中使用的头文件要自己手动包含
代码中使用其他模块,要显式添加到build.cs里,不依靠其他模块自动依赖
代码文件的编码使用UTF8,不要使用GBK,GBK编码在注释中包含中文时,经过UHT生成的反射信息会产生三字符序列的编译问题
避免在代码中使用三字符序列,不同编译器的支持不同,而且在C++17之后就废弃了
引用头文件指定路径时要使用
/
而不是\
,反斜杠路径包含在Android/Mac/IOS上会报错构造函数初始化列表的成员初始化顺序一定要按照声明的顺序
使用RTTI时要在build.cs中开启
bUseRTTI
,需要注意的是,不要对其他模块的符号使用RTTI,在不同的平台会有不同的未定义错误,尽量不要在UE中使用C++的RTTI。类声明后如果没有直接定义对象,不要使用static修饰
模块不能循环引用
插件依赖了外部的插件,需要在插件中添加插件依赖
插件需要添加平台的whitelist
值类型数据成员必须有默认初始值(数值类型、枚举等等)
1
2static class A{}AIns; // OK
static class A{}; // ERROR:'static' is not permitted on a declaration of a type [-Werror,-Wmissing-declarations]上面的代码在Win上是可以编译过的,在Mac上就是编译错误,因为在Mac编译时默认的编译参数具有
-Werror,-Wmissing-declarations
。
命名规则
UE架构内的命名同样需要遵循UE的代码标准:Coding Standard。
- 变量名要能够标识出类型,比如b开头是bool,i默认是int32,特殊位长的需要特殊命名,无符号类型要加u;
- 函数库统一使用Flib开头;
- Delegate命名时要能够标识出其类型,可以使用缩写,Dy代表动态代理,Multi代表多播代理,Dlg代表是代理;
- 游戏框架内的Subsystem类必须要以
Subsys
开头,并且能够知道这个类是做什么的,比如SubsysGameUpdater
和其子类SubsysHTTPGameUpdater
; - 函数的命名要能够根据名字知道它要做什么事情、所有获取的方法统一使用Get,进行检测的可以使用TryGet命名;
- 函数传入的参数必须以In开头,返回的参数必须以Out开头(引用);
- 函数有可能会执行失败的要返回bool或者错误码(返回值必须要有意义,不能逻辑中什么都不判断直接在函数末尾写
return true
) - 蓝图的类统一使用
BP_
开头 - UI的类统一使用
UMG_
开头; - 代码中的namespace要以
NS
开头; - 在C++里继承自UserWidget的类命名以
UW
开头,以继承关系为头缩写,后缀以UI
结尾; - 插件和工程中的蓝图不可以有名字重复;
- 暴露给蓝图的函数,如果形参具有默认参数,则声明与定义里的形参名必须一致,不可以出现下列情况(在原生C++可以,但是暴露给蓝图的不行)
1 | // .h |
这种情况会导致在蓝图中调用的函数的实参传递不到定义里的Ival
,相当于这个参数没有被传递。