Design specifications and code standards for UE projects

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

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

  1. 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++).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. To be supplemented.

Code Specifications

The basic requirement is to adhere to UE’s coding standards: Coding Standard

Additional extended requirements:

  1. 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;
  2. All header files must distinguish between engine and project dependencies, using // Project Header and // Engine Header comments for included header files;
  3. .h should only include headers for symbols declared within, not all headers;
  4. 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.
  5. 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;
  6. UCLASS must be tagged with Blueprintable/BlueprintType, and USTRUCT must be tagged with USTRUCT to allow blueprint inheritance and access;
  7. The initialization order of data members in C++-written classes must match the declaration order;
  8. 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;
  9. 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;
  10. 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;
  11. For resources created in local scope (raw memory or file access), use ON_SCOPE_EXIT to ensure release logic;
  12. 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.
  13. dynamic_cast is generally prohibited in UE; within UE C++, do not use standard C++ conversions, use Cast<> instead;
  14. Objects inheriting from UObject must uniformly use GENERATED_UCLASS_BODY and create a constructor XXXXX:XXXXX(const FObjectInitializer& InObjectInitializer);
  15. If there is potential for competitive logic within the function, use FScopeLock ScopeLock(&CriticalSection); to lock the current scope and prevent resource contention;
  16. Prefer defensive programming with the Impl mechanism.
  17. The use of exceptions is completely prohibited in the project; bForceEnableExceptions must not be set to true;
  18. Structures created in C++ must provide a == operator;
  19. 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;
  20. Structures created in C++ for external use must use UPROPERTY and need to provide BlueprintReadWrite properties;
  21. All classes exposed for use by other modules must have an export symbol MODULE_NAME_API;
  22. All plugins and independent modules in build.cs must include OptimizeCode=CodeOptimization.InShippingBuildsOnly for easier debugging.
  23. The load order of all Editor module plugins must precede Default, using PreDefault or PostEngineInit as needed;
  24. All macros used in the code must be created in build.cs, added via PublicDefinitions;
  25. All dependent third-party code must support at least Windows/MacOS/Android/IOS four platforms.
  26. Code files must use UTF-8 encoding.
  27. Be aware of differences in C++ feature support between platforms; code should not assume results based solely on a single platform’s compilation;
  28. 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;
  29. For macros needed across the entire project, add them using ProjectDefinitions in target.cs instead of defining the same macro in every module;
  30. All path-related operations must use FPaths; do not write your own. Note that paths made with FPath::Combine must then use FPath::MakeStandardFilename.
  31. Paths for external module headers must be relative to the module’s Public full path.
  32. For cross-platform code handling different platforms, refer to FPlatformMisc implementation (see UE4: Cross-platform Implementation of PlatformMisc);
  33. When accessing raw data from an array (TArray) using pointers, be cautious of the Reserve issue arising from dynamic growth of elements.
  34. Do not call the Super version of the function without Implementation in *_Implementation functions as it will result in infinite recursion. Use INTERFACE_NAME::Execute_* to call; otherwise, it won’t trigger overridden functions in Unlua.
  35. Specializations of class member template functions should not be written within the class declaration; this will cause errors on Android.
  36. 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.
  37. Static libraries for iOS must enable bitcode.
  38. Do not call GetClass() on TSubclassOf<> objects; it retrieves the Class of the managed UClass. To obtain the UClass managed by TSubclassOf, directly call Get(). The TSubclassOf redefines operator->, leading to such ambiguities.
  39. Since UnLua is in use, and it does not support exporting non-dynamic proxies, the usable Delegate types for the project are Dynamic Delegate and Dynamic Multicast Delegate.
  40. Avoid using operations such as std::stream to read files packed in Pak; use APIs like FFileHelper::LoadFileToArray instead.
  41. If certain reflection properties should only exist in the Editor, use WITH_EDITORONLY_DATA instead of WITH_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 include WindowsPlatformMisc
  • 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
2
static class A{}AIns; // OK
static class A{}; // ERROR: 'static' is not permitted on a declaration of a type [-Werror,-Wmissing-declarations]

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.

  1. 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;
  2. Function libraries should uniformly be prefixed with Flib;
  3. Delegate naming must identify the type, allowing abbreviations such as Dy for dynamic proxy, Multi for multicast proxy, and Dlg for delegates;
  4. Subsystem classes within the game framework must begin with Subsys and clearly convey their purpose, e.g., SubsysGameUpdater and its subclass SubsysHTTPGameUpdater;
  5. Function names should indicate their actions, with all getter methods prefixed with Get, and methods performing checks can be prefixed with TryGet;
  6. Function parameters must start with In for input and Out for output (reference);
  7. 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);
  8. Blueprint classes must uniformly begin with BP_;
  9. UI classes must uniformly begin with UMG_;
  10. Namespace in the code must start with NS;
  11. Classes inheriting from UserWidget in C++ should be prefixed with UW, using initials for inheritance relationships and ending with UI;
  12. Plugins and blueprints in the project must not have duplicate names;
  13. 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
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){}

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.

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

Scan the QR code on WeChat and follow me.

Title:Design specifications and code standards for UE projects
Author:LIPENGZHA
Publish Date:2020/01/01 11:55
Word Count:10k Words
Link:https://en.imzlp.com/posts/25915/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!