[WIP] Unreal Source Explained

July 25, 2022 ยท View on GitHub

Unreal Source Explained (USE) is an Unreal source code analysis, based on profilers.
For full Table of Contents and more infomation, see the repo in github.

Initialization

Engine Initialization Overview

As you can see in the image above, Unreal is initialized by two main steps: FEngineLoop::PreInit()(link) and FEngineLoop::Init()(link). They are called in FAppEntry::Init()(link) in iOS, and AndroidMain()(link) in Android.

You may think PreInit() is the low-level initializaiton and Init() is the high-level.

Note in Unreal there are two ways to manage submodules: Module and Plugins. Module conatains only code, while Plugin can contain assets and/or Modules.

To name a few things get initialized in PreInit(), in order:

  • load core module(link): LoadCoreModules()(link) for "CoreUObject";
  • fundamental modules(link): LoadPreInitModules()(link) for "Engine", "Renderer", "AnimGraphRuntime", "Landscape", "RenderCore";
  • "application-like" modules(link): FEngineLoop::AppInit()(link)
    • localization(link): BeginInitTextLocalization()(link)
    • plugins (link): FPluginManager::LoadModulesForEnabledPlugins() (link);
    • configs (link): FConfigCacheIni::InitializeConfigSystem()(link)
  • scalability(link): InitScalabilitySystem()(link). Scalability adjusts the quality of various features in order to maintain the best performance for your game on different platforms and hardware;
  • game physics(link): InitGamePhys()(link)
  • slate application(link): FSlateApplication::Create()(link)
  • RHI(link): RHIInit()(link)
  • global shaders resources(link): CompileGlobalShaderMap()(link)
  • render thread (link): StartRenderingThread()(link);
  • most UObjects' reflection data(link): ProcessNewlyLoadedUObjects()(link);
  • start-up modules: (link): LoadStartupCoreModules()(link): "Core", "Networking", "Messaging", "Slate", "UMG";
  • load task graph module(link);
  • engine and game localizaiton(link);

and Init() initializes these in order:

  • Create the high-level game engine objects(link): UGameEngine::Init()(link)
    • UEngine::Init()(link), UEngine is abstract base class of UGameEngine and UEdtiorEngine, and is responsible for management of systems critical to editor or game systems.;
    • UGameUserSettings::LoadSettings()(link);
    • UGameInstance(link), UGameInstance is high-level manager object for an instance of the running game
      • create UWorld in UGameInstance::InitializeStandalone()(link), UWorld is the top level object representing a map or a sandbox in which Actors and Components will exist and be rendered;
    • UGameViewportClient (link)
      • create localplayer for the viewport(link): UGameViewportClient::SetupInitialLocalPlayer()(link)
  • and start the high level game engine(link): UGameEngine::Start()(link)

UObject Initialization

Archetype and CDO

To manage UObjects, Unreal uses UObjectBase::ObjectFlags(link) to record a UObject instance's states.

A UObject instance is called Archetype object if it has RF_ArchetypeObject flag, and it's called Class Default Object (CDO) if it has RF_ClassDefaultObject.

Unreal eventually calls StaticConstructObject_Internal()(link) to create every UObject instance, no matter it's via user's NewObject<T>() call (link) or via Unreal's internal call. Here the most important 3 parameters of StaticConstructObject_Internal() are its

  • UClass* Class: The class of the object to create. The return value of Class->GetDefaultObject() is indeed the CDO;
  • UObject* Template: If specified, the property values from this object will be copied to the new object, and the new object's ObjectArchetype value will be set to this object. If nullptr, the class default object is used instead.
  • EObjectFlags InFlags: The ObjectFlags to assign to the new object.

Parameter Template has higher priority than the CDO to copy its property value to the new object(link).

FObjectInitializer::~FObjectInitializer()
{
	//...
	UObject* Defaults = ObjectArchetype ? ObjectArchetype : BaseClass->GetDefaultObject(false);
	InitProperties(Obj, BaseClass, Defaults, bCopyTransientsFromClassDefaults);
	//...
}

The new object's flag is set by the InFlags argument. Usually it doesn't have RF_ArchetypeObject nor RF_ClassDefaultObject flag, therefore, the new object is just a normal instance.
However that's not the case when the new object is a new Archetype object or a new CDO.

So what's archetype and CDO anyway, what's their similarity and difference?

As the above image shows, either an Archetype object, a CDO, or any UObject instance can be a template object to copy its properties' values into a new object.

Any UObject instance can be a template object, with or without RF_ArchetypeObject or RF_ClassDefaultObject flag.

Archetype object iteself is initialized from asset data (e.g. uasset) in the disk. Its RF_ArchetypeObject flag is set when loading the asset in FLinkerLoad::CreateExport()(link):

UObject* FLinkerLoad::CreateExport( int32 Index )
{
	FObjectExport& Export = ExportMap[ Index ];
	...
	// RF_ArchetypeObject and other flags
	EObjectFlags ObjectLoadFlags = Export.ObjectFlags;
	...
	Export.Object = StaticConstructObject_Internal
	(
		LoadClass,
		ThisParent,
		NewName,
		ObjectLoadFlags,
		EInternalObjectFlags::None,
		Template
	);
}

CDO itself is initialized by

  • its constructor code,
  • its Superclass's CDO if it has SuperClass(link),
  • and also, by its asset data if it has linked asset in UClass::SerializeDefaultObject()(link).

CDOs have both the RF_ClassDefaultObject and RF_ArchetypeObject flags, they are set and explained when creating the CDO (link):

UObject* UClass::CreateDefaultObject()
{
	...
	// RF_ArchetypeObject flag is often redundant to RF_ClassDefaultObject, but we need to tag
	// the CDO as RF_ArchetypeObject in order to propagate that flag to any default sub objects.
	ClassDefaultObject = StaticAllocateObject(this, GetOuter(), NAME_None, EObjectFlags(RF_Public|RF_ClassDefaultObject|RF_ArchetypeObject));
	...
}

For the same reason, all Default Subobjects have both RF_DefaultSubObject and RF_ArchetypeObject flags.

AActor and UComponent initialization

Class reflection data

Most (near all) Z_Construct_UClass_XXX() fuctions are called only in the initialization stage via ProcessNewlyLoadedUObjects()(link).

Z_Construct_UClass_XXX() are functions that construct the Unreal intrinsic "class reflection data". These functions' code are generated by macro in IMPLEMENT_INTRINSIC_CLASS(link):

#define IMPLEMENT_INTRINSIC_CLASS(TClass, TRequiredAPI, TSuperClass, TSuperRequiredAPI, TPackage, InitCode) \
	IMPLEMENT_CLASS(TClass, 0) \
	TRequiredAPI UClass* Z_Construct_UClass_##TClass(); \
	struct Z_Construct_UClass_##TClass##_Statics \
	{ \
		static UClass* Construct() \
		{ \
			extern TSuperRequiredAPI UClass* Z_Construct_UClass_##TSuperClass(); \
			UClass* SuperClass = Z_Construct_UClass_##TSuperClass(); \
			UClass* Class = TClass::StaticClass(); \
			UObjectForceRegistration(Class); \
			check(Class->GetSuperClass() == SuperClass); \
			InitCode \
			Class->StaticLink(); \
			return Class; \
		} \
	}; \
	UClass* Z_Construct_UClass_##TClass() \
	{ \
		static UClass* Class = NULL; \
		if (!Class) \
		{ \
			Class = Z_Construct_UClass_##TClass##_Statics::Construct();\
		} \
		check(Class->GetClass()); \
		return Class; \
	} \

    ...