Recently, a new project has been initiated, summarizing some issues from previous projects and listing design specifications and code standards for UE development projects (the term “code standards” seems too strict; coding habits are quite subjective, and “coding conventions” would be a better term, but strict execution is necessary for promotion within the team). This article will be continuously updated and organized, and feedback and discussions are welcome.
Design Specifications
- All design logic and classes must be configurable so that specified logic can be replaced based on needs without requiring players to reinstall (especially logic implemented in C++).
- Before writing actual business code, the first step is to design interfaces as an intermediate layer. After designing the interface, it must be submitted first (it can contain no logic, just logging is fine) for others to use, avoiding dependency waiting.
- Maintain the stability and extensibility of interfaces; they should not change arbitrarily. Naming should be concise and intuitive, and parameters should be complete (all external dependencies must be passed), keeping the interface stateless.
- All business-dependent utility functions must be abstracted into general utilities. For example, the functionality for downloading the Pak list, which includes the ability to download any file, should be extracted as generic code. Any large functions containing multiple small functionalities need to be abstracted into independently callable functions or objects, minimizing dependencies and avoiding reliance on call order.
- All operations involving configuration reading/initialization must use a unified generic method; each object should not write its own process, as this can become chaotic.
- To be supplemented.
Code Specifications
The basic requirement is to adhere to UE’s coding standards: Coding Standard
Additional extended requirements:
- Each USTRUCT, UObject (excluding U), and function library must be located in a source file with the same name, with function libraries prefixed with
Flib
; - All header files must distinguish between engine and project dependencies, using
// Project Header
and// Engine Header
comments for included header files; .h
should only include headers for symbols declared within, not all headers;- All classes in the project, whether implemented in blueprints or C++, must have a C++ base class and interface. Note that the base class and interface should only contain generic elements and not specific business logic.
- All non-internal members (not exposed for external use) should be written as UFUNCTIONs and tagged with BlueprintNativeEvent; the same applies to properties—tagging with UFUNCTION and UPROPERTY allows reflection and blueprint inheritance;
- UCLASS must be tagged with Blueprintable/BlueprintType, and USTRUCT must be tagged with USTRUCT to allow blueprint inheritance and access;
- The initialization order of data members in C++-written classes must match the declaration order;
- Any parameters received within functions must be checked within their own scope, such as null checks and clamp operations; external input cannot be presumed valid;
- Functions that might fail must provide return values, including Get functions (either error codes or bool); do not simply use
return true;
at the end of execution—results must depend on previous logic’s success; - All static members’ global scope initialization (especially for obtaining engine data) is prohibited; only literal type static initializations like
static FString Name = TEXT("helloworld")
are allowed; - For resources created in local scope (raw memory or file access), use
ON_SCOPE_EXIT
to ensure release logic; - Lambda-captured objects must not be used as asynchronous operation objects; for example, capturing local variables in a lambda created within a function and passing it to asynchronous logic is prohibited.
dynamic_cast
is generally prohibited in UE; within UE C++, do not use standard C++ conversions, useCast<>
instead;- Objects inheriting from UObject must uniformly use
GENERATED_UCLASS_BODY
and create a constructorXXXXX:XXXXX(const FObjectInitializer& InObjectInitializer)
; - If there is potential for competitive logic within the function, use
FScopeLock ScopeLock(&CriticalSection);
to lock the current scope and prevent resource contention; - Prefer defensive programming with the Impl mechanism.
- The use of exceptions is completely prohibited in the project;
bForceEnableExceptions
must not be set to true; - Structures created in C++ must provide a
==
operator; - If a structure created in C++ requires custom constructors, default constructor, copy constructor, move constructor, and assignment operator implementations must be provided. The lazily can use
A()=default;
. If using default, ensure that the class does not reference any resource; - Structures created in C++ for external use must use
UPROPERTY
and need to provideBlueprintReadWrite
properties; - All classes exposed for use by other modules must have an export symbol
MODULE_NAME_API
; - All plugins and independent modules in
build.cs
must includeOptimizeCode=CodeOptimization.InShippingBuildsOnly
for easier debugging. - The load order of all Editor module plugins must precede
Default
, usingPreDefault
orPostEngineInit
as needed; - All macros used in the code must be created in
build.cs
, added viaPublicDefinitions
; - All dependent third-party code must support at least
Windows/MacOS/Android/IOS
four platforms. - Code files must use UTF-8 encoding.
- Be aware of differences in C++ feature support between platforms; code should not assume results based solely on a single platform’s compilation;
- Modules must not mutually include each other, e.g., if A includes B, B must not include A; such circular inclusion will fail during compilation on Mac;
- For macros needed across the entire project, add them using
ProjectDefinitions
intarget.cs
instead of defining the same macro in every module; - All path-related operations must use
FPaths
; do not write your own. Note that paths made withFPath::Combine
must then useFPath::MakeStandardFilename
. - Paths for external module headers must be relative to the module’s
Public
full path. - For cross-platform code handling different platforms, refer to
FPlatformMisc
implementation (see UE4: Cross-platform Implementation of PlatformMisc); - When accessing raw data from an array (
TArray
) using pointers, be cautious of theReserve
issue arising from dynamic growth of elements. - Do not call the
Super
version of the function withoutImplementation
in*_Implementation
functions as it will result in infinite recursion. UseINTERFACE_NAME::Execute_*
to call; otherwise, it won’t trigger overridden functions in Unlua. - Specializations of class member template functions should not be written within the class declaration; this will cause errors on Android.
- Runtime module dependency modules must not include Developer and Editor modules; if Editor or Developer functionalities are needed, add a new Editor or Developer module under the project or plugin.
- Static libraries for iOS must enable bitcode.
- Do not call
GetClass()
onTSubclassOf<>
objects; it retrieves theClass
of the managedUClass
. To obtain theUClass
managed byTSubclassOf
, directly callGet()
. TheTSubclassOf
redefinesoperator->
, leading to such ambiguities. - Since UnLua is in use, and it does not support exporting non-dynamic proxies, the usable Delegate types for the project are
Dynamic Delegate
andDynamic Multicast Delegate
. - Avoid using operations such as
std::stream
to read files packed in Pak; use APIs likeFFileHelper::LoadFileToArray
instead. - If certain reflection properties should only exist in the Editor, use
WITH_EDITORONLY_DATA
instead ofWITH_EDITOR
.
Cross-Platform Compilation Code Guidelines
Due to differences in compilers used in different platforms, supported C++ standard versions, and UE’s own compilation rules, the same code may have different compilation rules across platforms. Attention to the following issues is required when writing code.
- Full namespace should be used (e.g., when referring to code generated by protobuf)
- Avoid implicit type conversions (e.g., enum->int)
- Do not redefine macros that already exist in UE, such as basic math macros like
PI
, etc. - Use relative paths to include headers, and do not specify the full path (XXXX/Public/xxxx.h)
- Runtime modules must not include Editor and Developer modules (if some Runtime modules require different operations in Editor and packaging phases, they should check
bBuildEditor
in build.cs and include modules. In C++ code,WITH_EDITOR
checks are necessary). - Utilize UE’s cross-platform capabilities; do not directly reference specific platform header files such as
PlatformMisc
, and do not directly includeWindowsPlatformMisc
- Do not use
PublicAdditionalLibraries
in the Module’s build.cs to add link libraries from another Module. - Headers used in the code must be included manually.
- If using other modules in code, explicitly add them in build.cs rather than relying on automatic dependencies from other modules.
- Code file encoding must be UTF-8; avoid using GBK, as using GBK encoding will cause three-character sequence compilation issues in reflection information generated by UHT when comments contain Chinese characters.
- Avoid using three-character sequences in code, as support differs among compilers and they were deprecated after C++17.
- When specifying paths for included headers, use
/
instead of\
. Backslash paths will cause errors on Android/Mac/iOS. - The element initialization order in constructor initialization lists must follow the declaration order.
- Enable
bUseRTTI
in build.cs when using RTTI. Note that do not use RTTI on symbols from other modules, as this can lead to different undefined errors across platforms; RTTI usage is generally discouraged in UE C++. - Do not use
static
if there are no immediately defined objects after the class declaration. - Modules must not cyclically reference one another.
- Plugins dependent on external plugins must declare such dependencies in the plugin definition.
- Plugins must include platform whitelists.
- Value-type data members must have default initial values (for numeric types, enums, etc.).
1 | static class A{}AIns; // OK |
The code above compiles on Win but results in a compilation error on Mac because the default compilation parameters during Mac builds include -Werror,-Wmissing-declarations
.
Naming Rules
Naming within the UE architecture must also follow UE coding standards: Coding Standard.
- Variable names should identify types, e.g., names starting with b for bool, i for int32, customized names for special bit lengths, and use u for unsigned types;
- Function libraries should uniformly be prefixed with Flib;
- Delegate naming must identify the type, allowing abbreviations such as Dy for dynamic proxy, Multi for multicast proxy, and Dlg for delegates;
- Subsystem classes within the game framework must begin with
Subsys
and clearly convey their purpose, e.g.,SubsysGameUpdater
and its subclassSubsysHTTPGameUpdater
; - Function names should indicate their actions, with all getter methods prefixed with Get, and methods performing checks can be prefixed with TryGet;
- Function parameters must start with In for input and Out for output (reference);
- Functions that may fail must return bool or error codes (return values must be meaningful; logical assumptions should not allow directly ending with
return true
without checks); - Blueprint classes must uniformly begin with
BP_
; - UI classes must uniformly begin with
UMG_
; - Namespace in the code must start with
NS
; - Classes inheriting from UserWidget in C++ should be prefixed with
UW
, using initials for inheritance relationships and ending withUI
; - Plugins and blueprints in the project must not have duplicate names;
- For functions exposed to blueprints with parameters having default values, parameter names must be consistent in both declaration and definition; the following situation must be avoided (allowed in raw C++, but not when exposed to blueprints):
1 | // .h |
This situation will prevent the actual parameter from being passed to the defined Ival
in the function call from blueprints, rendering that parameter effectively unpassed.