UE项目的设计规范和代码标准

Design specifications and code standards for UE projects

最近新开了项目,大概总结了之前项目的一些问题,列举了一些UE开发项目的设计规范和代码标准(代码标准这个词看起来太严肃了,写代码的习惯是一个比较主观的概念,其实叫代码约定更好,但是在组内推广还是要有严格执行的要求)。本篇文章会持续更新和整理,欢迎指出问题和交流意见。

设计规范

  1. 所有设计的逻辑和类要成为可配置的,在不让玩家重新安装的基础上根据需求可以替换掉指定逻辑(尤其是替换掉C++实现的的逻辑)。
  2. 写真正的业务代码之前第一步要设计接口作为中间层,设计接口之后不要实现,必须先提交接口(可以没有任何逻辑,只是打印log都可以),可以供其他人使用,避免依赖工作间的等待。
  3. 保持接口的稳定和可扩展,接口不能随意变动,命名应该简洁直观,接收参数应该齐全(所有依赖外部的参数都要传递),保持接口的无状态。
  4. 所有写的业务依赖的工具函数要抽出作为通用的工具,比如之前写的下载Pak列表的功能,其中包含下载任意文件的功能,要抽出作为通用的代码,类似的一个大功能包含一堆小功能的,都可以抽象出来的都要单独写成可以随意调用的函数或者对象,不能够依赖调用顺序,最大限度地降低依赖。
  5. 所有涉及配置读取/初始化之类的操作必须使用统一的通用方法,不可以每个对象自己写一个流程,会变的很混乱。
  6. 待补充。

代码规范

基础要求是需要执行UE的代码标准:Coding Standard

额外的扩展要求:

  1. 每一个USTRUCT、UObject(可除去U)、函数库也必须单独位于一个同名的源文件,且函数库的命名为以Flib开头;
  2. 所有的头文件中所包含的其他头文件必须区分引擎和项目,在包含头文件里使用// Project Header// Engine Header注释;
  3. .h中应该只包含头文件中用到的符号声明的头文件,不要把所有的头文件都写到.h里;
  4. 项目里所有的类,不管具体是在蓝图里实现还是C++实现,必须要有C++的基类和C++的接口,注意基类和接口只写通用的东西,不要写具体的业务流程。
  5. 所有的非内部成员(不暴露给外部使用的成员)都写成UFUNCTION,并且加上BluepringNativeEvent标签,属性也同理,加了UFUNCTION和UPROPERTY可以被反射使用以及可以被蓝图继承;
  6. UCLASS必须加上Blueprintable/BlueprintType,USTRUCT必须加上USTRUCT,使其可以被蓝图继承和访问;
  7. C++写的类的数据成员的初始化顺序必须要与声明顺序一致;
  8. 函数内接收的任何参数都必须在自己的函数内做检测,如判空和clamp操作,不能够相信外部输入是合理的;
  9. 在有可能会执行失败的函数中必须要提供返回值,在Get函数中同样需要(错误码或者bool都可以),并且不可以直接在执行结尾return true;最终执行的结果要根据前面可能会执行失败的逻辑是否成功;
  10. 禁止所有的static成员的在全局作用域初始化操作(尤其是获取引擎数据的初始化),只允许字面值类型的static初始化,如static FString Name = TEXT("helloworld")等;
  11. 对于在局部作用域创建的资源(裸内存或者文件访问)要使用ON_SCOPE_EXIT来确保释放逻辑;
  12. Lambda引用捕获的对象不可做为异步逻辑操作的对象,如在一个函数内创建了lambda对象引用捕获了局部变量,将其传递给异步逻辑,这种操作是禁止的。
  13. UE中默认中禁止dynamic_cast,在UEC++的范畴内不要使用标准C++的转换,可以使用Cast<>
  14. 继承自UObject的对象统一使用GENERATED_UCLASS_BODY,并需要创建出XXXXX:XXXXX(const FObjectInitializer& InObjectInitializer)的构造函数;
  15. 函数内如果有可能具有某种竞争逻辑,则需要使用FScopeLock ScopeLock(&CriticalSection);为当前作用域加锁,防止资源竞争;
  16. 尽量使用防御式编程的Impl机制
  17. 项目中完全禁止使用异常,bForceEnableExceptions不可以被设置为true;
  18. 用C++创建的结构成员必须要提供==的操作符
  19. 用C++创建的结构成员如果要自定义构造函数,则需要提供默认构造函数、拷贝构造函数、移动构造函数、赋值操作符的实现,偷懒可以使用A()=default;,如果使用default就需要确保类内没有引用某种资源;
  20. 用C++创建的供外部使用的结构成员必须使用UPROPERTY并且需要提供BlueprintReadWrite属性;
  21. 所有暴露给其他模块使用的类必须有导出符号MODULE_NAME_API
  22. 所有的插件和单独的模块在build.cs中必须写OptimizeCode=CodeOptimization.InShippingBuildsOnly,方便调试。
  23. 所有Editor模块的插件加载顺序必须先于Default,可以使用PreDefault或者PostEngineInit
  24. 在代码中使用的宏统一需要由build.cs中来创建,使用PublicDefinitions来添加。
  25. 所有依赖的第三方代码必须要支持最少Windows/MacOS/Android/IOS四个平台。
  26. 代码文件的编码格式统一为UTF-8
  27. 注意不同平台之间对C++特性支持的差异,写代码时不能以单一平台的编译结果为准;
  28. 两个模块之间不可以互相包含比如,A包含了B,B又包含了A,这种循环包含在Mac上会编译失败;
  29. 如果整个项目中都要用到一个宏,必须在target.cs中使用ProjectDefinitions来添加,不要把同一个宏在每个模块都定义一遍;
  30. 所有与路径相关的操作都需要使用FPaths,不要自己写,注意路径使用FPath::Combine拼接之后要对其使用FPath::MakeStandardFilename
  31. 包含外部模块的头文件路径必须是相对于该模块的Public全路径。
  32. 对于不同平台不同处理的跨平台代码要参考FPlatformMisc的实现方式(参考UE4:PlatformMisc的跨平台实现
  33. 当需要将数组(TArray)的原生数据通过指针访问时,要注意元素动态增长导致的Reserve问题。
  34. 不要在*_Implementation的函数里调用Super本函数的无Implementation版本,会无限递归。要使用INTERFACE_NAME::Execute_*这种方式调用,不然在Unlua里调不到lua里重写的函数。
  35. 类成员模板函数的特化版本不要写在类声明内,在Android上会有错误。
  36. Runtime模块的依赖模块不可以包含Developer和Editor的模块,如果需要使用Editor或者Developer的功能,则在项目或者插件下新加一个Editor或者Developer的模块。
  37. 提供给IOS使用的静态链接库必须要启用bitcode。
  38. 不要通过TSubclassOf<>的对象调用GetClass(),它获取到的是所管理的UClassClass,要获取TSubclassOf所管理的UClass,直接调Get(),因为TSubclassOf重载了operator->操作符,所以才会有这样的歧义。
  39. 因为需要使用UnLua,而UnLua不支持导出非动态代理,所以项目中可以使用的Delegate类型为Dynamic DelegateDynamic Mulitcast Delegate
  40. 不要使用std::stream等C++原生读取文件的操作来读取打包到Pak里的文件,要使用FFileHelper::LoadFileToArray等API。
  41. 如果有些反射属性只想在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
    2
    static 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

  1. 变量名要能够标识出类型,比如b开头是bool,i默认是int32,特殊位长的需要特殊命名,无符号类型要加u;
  2. 函数库统一使用Flib开头;
  3. Delegate命名时要能够标识出其类型,可以使用缩写,Dy代表动态代理,Multi代表多播代理,Dlg代表是代理;
  4. 游戏框架内的Subsystem类必须要以Subsys开头,并且能够知道这个类是做什么的,比如SubsysGameUpdater和其子类SubsysHTTPGameUpdater
  5. 函数的命名要能够根据名字知道它要做什么事情、所有获取的方法统一使用Get,进行检测的可以使用TryGet命名;
  6. 函数传入的参数必须以In开头,返回的参数必须以Out开头(引用);
  7. 函数有可能会执行失败的要返回bool或者错误码(返回值必须要有意义,不能逻辑中什么都不判断直接在函数末尾写return true
  8. 蓝图的类统一使用BP_开头
  9. UI的类统一使用UMG_开头;
  10. 代码中的namespace要以NS开头;
  11. 在C++里继承自UserWidget的类命名以UW开头,以继承关系为头缩写,后缀以UI结尾;
  12. 插件和工程中的蓝图不可以有名字重复;
  13. 暴露给蓝图的函数,如果形参具有默认参数,则声明与定义里的形参名必须一致,不可以出现下列情况(在原生C++可以,但是暴露给蓝图的不行)
1
2
3
4
5
6
7
8
9
10
11
12
13
// .h
UCLASS(BlueprintType)
class UTestObject:public UObject
{
GENETATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void Func(int32 InIval=123);
};

// .cpp

void UTestObject::Func(int32 Ival){}

这种情况会导致在蓝图中调用的函数的实参传递不到定义里的Ival,相当于这个参数没有被传递。

未完待续,欢迎指出问题和交流意见。

微信扫描二维码,关注我的公众号。

本文标题:UE项目的设计规范和代码标准
文章作者:查利鹏
发布时间:2020/01/01 11:55
本文字数:4.8k 字
原始链接:https://imzlp.com/posts/25915/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!