Epic Games 虚幻引擎 C++ 编码规范(UE 5.6 精要)
Epic Games 虚幻引擎 C++ 编码规范(UE 5.6 精要)
摘自 Epic 官方公开文档:Epic C++ Coding Standard for Unreal Engine
整理为中文学习笔记,便于自查与新人入门。
类组织
为读者而非作者组织类。Public 接口在前,私有实现在后。
UCLASS()
class EXAMPLEPROJECT_API AExampleActor : public AActor
{
GENERATED_BODY()
public:
AExampleActor();
protected:
virtual void BeginPlay() override;
};
版权声明
Epic 分发的 .h / .cpp / .xaml 必须以下面这行完全相同的版权头开始:
// Copyright Epic Games, Inc. All Rights Reserved.
自有项目可换成自己的版权行,但保持统一。
命名约定
PascalCase 用于所有类型与变量名(如
Health、UPrimitiveComponent)。单词之间不使用下划线(宏除外)。
前缀表:
前缀 用于 T模板类 U从 UObject派生的类A从 AActor派生的类S从 SWidget派生的类I抽象接口类 C类 Concept 的 struct E枚举 F其它大多数 class / struct bbool 变量( bPendingDestruction)typedef 使用底层类型的前缀(例:
typedef TArray<FMyType> FArrayOfMyTypes)。宏为
ALL_CAPS_WITH_UNDERSCORES,并带UE_前缀(如UE_LOG)。模板参数 / 嵌套类型别名免前缀;用
In前缀消除模板参数与别名的歧义:template <typename InElementType> class TContainer { public: using ElementType = InElementType; };类型 / 变量名为名词;方法名为动词,描述效果或返回值。
输出引用参数以
Out前缀(OutResult);同时是 bool 的写成bOutResult。返回 bool 的函数必须以"是不是 / 应不应该"提问:
IsVisible()、ShouldClearBuffer()。
包容性用词
- 命名、注释、提交信息、UI 字符串都使用包容性用词。
- 避免:blacklist / whitelist → 用 deny list / allow list。
- 避免:master / slave → 用 primary / secondary、source / replica 等。
- 假想的"某人"统一用 they / them / their。
- 避免性别化集合词(如 "guys")、俚语与脏话。
可移植的 C++ 代码
序列化 / 网络复制的数据使用显式宽度类型:
bool— 布尔(不可假设其大小,不要用BOOL)TCHAR— 字符(不可假设大小)uint8/int8— 1 字节uint16/int16— 2 字节uint32/int32— 4 字节uint64/int64— 8 字节float— 4 字节单精度double— 8 字节双精度PTRINT— 指针宽度整数- 不在乎宽度时
int/unsigned int可用(保证 ≥ 32 位)
标准库使用
在 UE 自带与 stdlib 之间取效果更好的一个:
| 设施 | 建议 |
|---|---|
<atomic> | 新代码用它,优先于 TAtomic |
<type_traits> | 与 UE traits 重叠时直接用,新 traits 沿用小写 value/type |
<initializer_list> | 支持花括号初始化必须使用 |
<regex> | 可用,但封装在 editor-only 代码 |
<limits> | std::numeric_limits 完整可用 |
<cmath> | 所有浮点函数可用 |
memcpy / memset | 性能收益明显时可优于 FMemory::Memcpy/Memset |
避免 std::vector / std::string 这类标准容器与字符串,除非是互操作代码。
注释
- 写自解释代码:
TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves优于t = s + l - b。 - 注释要说意图,不复述代码已经表达的内容。
- 不要给烂代码加注释——重写它。
- 注释不得与代码矛盾。
- 使用 JavaDoc 风格便于文档抽取:
- 类注释说明它解决什么问题、为什么存在;
- 多行方法注释含:目的、
@param(单位、范围、不合法值、错误码)、@return,可选@warning、@note、@see、@deprecated,每项独占一行; - 方法注释只放在公开声明处;
- 简单函数可把参数/返回值文档融入描述,不必一定写
@param/@return。
Const 正确性
不修改的参数按
const指针/引用传递。不修改对象的方法标
const。不修改容器时使用
const迭代。值传递参数与局部变量也优先
const,表达"不可变"。const 指针写法——
const放在*之后:T* const Ptr = ...; // const 指针,可改 T —— OK T& const Ref = ...; // 非法不要在返回类型上加
const(会抑制移动语义,触发警告):const TArray<FString> GetSomeArray(); // Bad const TArray<FString>& GetSomeArray(); // Fine const TArray<FString>* GetSomeArray(); // Fine const TArray<FString>* const GetSomeArray(); // Bad
现代 C++ 语法(C++20 起步)
static_assert
编译期断言可放心使用。
override 与 final
强烈推荐——重写时同时写 virtual + override:
class B : public A { public: virtual void F() override; };
nullptr
始终使用 nullptr,不用 NULL。
auto
避免,例外只有:
- 绑定 lambda(类型不可写出);
- 迭代器变量类型冗长影响可读性时;
- 模板代码中类型难以表达时。
用 auto 时必须显式加 const、& 或 *。C++20 结构化绑定同样不使用(等价于变参 auto)。
Range-Based For
优先使用,胜过手写迭代器:
for (TPair<FString, int32>& Kvp : MyMap) { ... }
for (UProperty* Property : TFieldRange<UProperty>(InStruct, ...)) { ... }
Lambda
- 控制在两三句话以内;
- 显式捕获——禁止
[&]/[=]; - 延迟执行的 lambda 禁止按引用 / 按指针(含
this)捕获; - 延迟执行用
CreateWeakLambda/CreateSPLambda; - 捕获共享对象用
TWeakObjectPtr/TWeakPtr,在 lambda 内部先校验有效性; - 任何不遵循以上规则的延迟 lambda 必须用注释说明"为什么仍然安全";
- 大 lambda 或返回另一个函数结果时使用显式返回类型。
强类型枚举
用 enum class,弃用旧的 namespace 包裹枚举:
UENUM()
enum class EThing : uint8 { Thing1, Thing2 };
UPROPERTY()
EThing MyProperty; // 不再用 TEnumAsByte<EThing::Type>
位标志枚举用 ENUM_CLASS_FLAGS(EFlags)。布尔语境判断要这么写:
if ((Flags & EFlags::Flag1) != EFlags::None) // Correct
移动语义
传 / 返容器时用 MoveTemp(UE 的 std::move)避免拷贝:
void FBlah::SetMemberArray(TArray<FString> InNewArray)
{
MemberArray = MoveTemp(InNewArray);
}
默认成员初始化
游戏代码推荐使用,把类型、UPROPERTY 标志、默认值集中在一处:
UPROPERTY()
int32 MaxCups = 10;
但如果会让一部分成员在头文件初始化、另一部分在构造函数初始化,则应避免,以免分裂。
第三方代码
对第三方库的修改用
//@UE5加说明标注。包含第三方头文件时包裹:
// @third party code - BEGIN PhysX #include <physx.h> // @third party code - END PhysX
代码格式
大括号
始终另起一行;单语句块也必须使用大括号:
int32 GetSize() const
{
return Size;
}
if (bThing)
{
return;
}
If-Else
始终使用大括号;else if 与 if 同级缩进:
if (X < 10) { ... }
else if (X < 100) { ... }
else { ... }
Tab 与缩进
- 按执行块缩进;
- 使用 Tab(宽度 4),不用空格;非 Tab 字符之后用空格做对齐可以;
- C# 同此规则。
Switch
- 显式标注落穿:
// falls through; - 始终带
default分支,并以break结尾。
命名空间
- UE 大部分代码不在 global 命名空间。
- 新的非 UCLASS / USTRUCT API 放进
UE::(更细化时如UE::Audio::)。 - 实现细节嵌套
Private命名空间(UE::Audio::Private::)。 using声明绝不放在全局作用域(即便在.cpp也不行);命名空间内、函数体内可用。- 前向声明的类型必须在其原命名空间内声明。
- 宏不能放在命名空间——用
UE_前缀代替。
物理依赖
- 文件名不加前缀(
Scene.cpp而非UScene.cpp); - 所有头文件使用
#pragma once; - 优先使用前向声明,能不
#include就不#include; - include 粒度尽量细,需要什么直接 include 什么;
- 模块分
Public(共享定义)与Private(实现); - 非平凡函数避免 inline(强制重建);
- 谨慎使用
FORCEINLINE(代码膨胀到调用方、拖慢构建)。
封装
- 类成员几乎都应该是
private; - 仅派生类需要访问的字段提供
protected访问器; - 不打算被继承的类用
final。
通用风格
最小化依赖距离:变量在临用前赋值;
拆分子方法提高可读性;
函数名与括号无空格:
Func(...),不是Func (...);处理所有编译器警告;
.cpp/.h文件末尾留一个空行;调试代码要么打磨好,要么不提交;
字符串字面量始终包
TEXT();循环里的公共子表达式提取到循环外;
注意 hot reload:频繁改动的函数避免内联 / 模板;
用中间变量简化复杂表达式:
const bool bIsLegalWindow = ...; const bool bIsPlayerDead = ...; if (bIsLegalWindow && !bIsPlayerDead) { DoSomething(); }指针 / 引用空格在
*/&右侧:FShaderType* Ptr // Correct FShaderType *Ptr // Wrong不允许变量隐藏(shadowing);
函数调用不传匿名字面量——抽出命名
const变量;避免头文件里的非平凡静态变量(每个翻译单元一份实例):
// Header: extern SOMEMODULE_API const FString GMyString; // Source: const FString GMyString = TEXT("String");避免在含行为变化的提交中夹带大量纯空白 / 纯改名改动。
API 设计
避免布尔形参作开关——改用
enum class+ENUM_CLASS_FLAGS:FCup* MakeCupOfTea(FTea* Tea, ETeaFlags Flags = ETeaFlags::None);例外:
SetEnabled(bool bEnabled)这种单一 setter 可以。避免过长参数表——抽出参数 struct。
避免在
bool与FString上重载(隐式转换可能误调)。接口类(
I-前缀)必须 abstract、不含成员变量。重写时同时写
virtual+override。UObject 按指针传递,不按引用传递。
平台相关代码
隔离在平台子目录:
Engine/Platforms/[PLATFORM]/Source/.../[PLATFORM]PlatformMemory.cpp共享代码避免出现
PLATFORM_[PLATFORM]宏;通过扩展 HAL 取而代之。必须用
#define时,在Platform.h里定义能力旗标 + 默认值,各平台覆盖:// Platform.h #ifndef PLATFORM_USE_PTHREADS #define PLATFORM_USE_PTHREADS 1 #endif跨平台代码绝不依赖某个平台代码的存在。
小结
读完这份规范,再看 Engine/Source 任何一个文件,你会发现 99% 的命名、缩进、API 形态都遵循这里的规则。这套规范对插件开发者和学习引擎源码的人尤其重要——按它来写代码,提交到引擎的 PR 才不会因为风格问题被打回。