Unreal Engine Note

"游戏引擎"

Posted by A-SHIN on October 6, 2018

“Yeah It’s on. ”

前言

阅读InsideUE4精要提炼。

正文

GamePlay框架

实体组件模式推崇“组合优于继承”的概念
程序=数据+算法

Classes、Private、Public

Object->Actor+Component->Level->World->WorldContext->GameInstance->Engine

UObjectBaseUtility : public UObjectBase
UObject : public UObjectBaseUtility 元数据、反射生成、GC垃圾回收、序列化、编辑器可见、Class Default Object等
AActor : public UObject InputComponent
UActorComponent : public UObject
USceneComponent : public UActorComponent FTransform
UPrimitiveComponent : public USceneComponent
UMeshComponent : public UPrimitiveComponent Actor可以带多个SceneComponent来渲染多个Mesh实体
UChildActorComponent : public USceneComponent

AInfo : public AActor
ALevelScriptActor : public AActor
AWorldSettings : public AInfo
ULevel : public UObject ALevelScriptActor AWorldSettings
UWorld final : public UObject PersisitentLevel subLevels
FWorldContext
UGameInstance : public UObject FWorldContext LocalPlayers
UEngine : public UObject TIndirectArray< FWorldContext > WorldList;
UGameplayStatics : public UBlueprintFunctionLibrary



面向对象派生下来的Pawn和Character,支持组合的Controller们。
APawn : public AActor Controller PhysicsCollision MovementInput
ADefaultPawn : public APawn
ASpectatorPawn : public ADefaultPawn
ACharacter : public APawn
AController : public AActor APawn APlayerState
APlayerController : public AController UPlayer 在任一刻,Player:PlayerController:PlayerState是1:1:1的关系。但是PlayerController可以有多个备选用来切换,PlayerState也可以相应多个切换。UPlayer的概念会在之后讲解,但目前可以简单理解为游戏里一个全局的玩家逻辑实体,而PlayerController代表的就是玩家的意志,PlayerState代表的是玩家的状态。
AAIController : public AController
APlayerState : public AInfo 用来保存玩家的游戏数据

UE的世界观是,World更多是逻辑的概念,而Level是资源场景表示。
AGameModeBase : public AInfo
AGameMode : public AGameModeBase Class登记、游戏内实体的Spawn、游戏的进度、Level的切换、多人游戏的步调同步
AGameStateBase : public AInfo
AGameState : public AGameStateBase 保存当前游戏的状态数据
AGameSession : public AInfo

按照软件工程的理念,没有什么问题是不能通过加一个间接层解决的,不行就加两层!
UPlayer : public UObject PlayerController
ULocalPlayer : public UPlayer Viewport LocalPlayer才是PlayerController产生的源头,也因此才有了Input
UNetConnection : public UPlayer

UGameInstance : public UObject
UGameInstance里的接口大概有4类:

  1. 引擎的初始化加载,Init和ShutDown等(在引擎流程章节会详细叙述)
  2. Player的创建,如CreateLocalPlayer,GetLocalPlayers之类的。
  3. GameMode的重载修改,这是从4.14新增加进来改进,本来你只能为特定的某个Map配置好GameModeClass,但是现在GameInstance允许你重载它的PreloadContentForURL、CreateGameModeForURL和OverrideGameModeClass方法来hook改变这一流程。
  4. OnlineSession的管理,这部分逻辑跟网络的机制有关(到时候再详细介绍),目前可以简单理解为有一个网络会话的管理辅助控制类。
    USaveGame : public UObject

MVC:

GamePlay框架:

类型系统

BP
UHT(Unreal Header Tool,一个分析源码标记并生成代码的工具)
UBT

类型系统: 生成,收集,注册,链接
UMetaData : public UObject
UInterface : public UObject
UField : public UObject 可包含UMetaData
UProperty : public UField
UEnum : public UField
UStruct : public UField 可包含UProperty
UFunction : public UStruct
UClass : public UStruct 可包含UFunction
UScriptStruct : public UStruct 可包含UFunction

我们的类只是继承于IMyInterface,UMyInerface只是作为一个接口类型的载体,用以区分和查找不同的接口。
观察UHT生成的代码可知,其实就分两部分,一是各种Z_辅助方法用来构造出各种UClass*等对象;另一部分是都包含着一两个static对象用来在程序启动的时候驱动登记,继而调用到前者的Z_方法,最终完成注册。
在程序启动的时候,UE利用了Static自动注册模式把所有类的信息都一一登记一遍。而紧接着另一个就是顺序问题了,这么多类,谁先谁后,互相若是有依赖该怎么解决。众所周知,UE是以Module来组织引擎结构的(关于Module的细节会在以后章节叙述),一个个Module可以通过脚本配置来选择性的编译加载。在游戏引擎众多的模块中,玩家自己的Game模块是处于比较高级的层次的,都是依赖于引擎其他更基础底层的模块,而这些模块中,最最底层的就是Core模块(C++的基础库),接着就是CoreUObject,正是实现Object类型系统的模块
UE4利用了C++的static对象初始化模式,在程序最初启动的时候,main之前,就收集到了所有的类型元数据、函数指针回调、名字、CRC等信息。

object运行时通过反射获得类型系统对象

生成、收集(第一个UCLASS: UObject::StaticClass()) 、注册(TClass::StaticClass())、绑定(bind把函数指针绑定到正确的地址)、链接(link)、CDO创建、引用记号流构建

生成:

UClass、UEnum、UStruct、UInterface、UProperty、UFunction的代码生成

  • Class:
GENERATED_BODY Hello_Source_Hello_MyClass_h_11_GENERATED_BODY Hello_Source_Hello_MyClass_h_11_ENHANCED_CONSTRUCTORS NO_API UMyClass(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; static TClassCompiledInDefer AutoInitializeUMyClass(TEXT("UMyClass"), sizeof(UMyClass), 899540749);
- - - DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyClass) -
- - Hello_Source_Hello_MyClass_h_11_INCLASS DECLARE_CLASS(UMyClass, UObject, COMPILED_IN_FLAGS(0), 0, TEXT(“/Script/Hello”), NO_API) static FCompiledInDefer Z_CompiledInDefer_UClass_UMyClass(Z_Construct_UClass_UMyClass, &UMyClass::StaticClass, TEXT(“UMyClass”), false, nullptr, nullptr, nullptr);
- - - IMPLEMENT_CLASS(UMyClass, 899540749); -
  • Struct:
GENERATED_USTRUCT_BODY Hello_Source_Hello_MyStruct_h_8_GENERATED_BODY static class UScriptStruct* StaticStruct(); static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FMyStruct(FMyStruct::StaticStruct, TEXT(“/Script/Hello”), TEXT(“MyStruct”), false, nullptr, nullptr);
- - - static FScriptStruct_Hello_StaticRegisterNativesFMyStruct ScriptStruct_Hello_StaticRegisterNativesFMyStruct;
  • Enum:
static class UEnum* EMyEnum_StaticEnum() static FCompiledInDeferEnum Z_CompiledInDeferEnum_UEnum_EMyEnum(EMyEnum_StaticEnum, TEXT(“/Script/Hello”), TEXT(“EMyEnum”), false, nullptr, nullptr);
  • Interface:
GENERATED_UINTERFACE_BODY Hello_Source_Hello_MyInterface_h_8_GENERATED_BODY GENERATED_UINTERFACE_BODY_COMMON DECLARE_CLASS(UMyInterface, UInterface, COMPILED_IN_FLAGS(CLASS_Abstract | CLASS_Interface), 0, TEXT(“/Script/Hello”), NO_API) static void StaticRegisterNativesUMyInterface();  
- - - IMPLEMENT_CLASS(UMyInterface, 4286549343); - -
- - Hello_Source_Hello_MyInterface_h_8_ENHANCED_CONSTRUCTORS NO_API UMyInterface(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; - -
- - - DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyInterface) - -
GENERATED_IINTERFACE_BODY Hello_Source_Hello_MyInterface_h_13_GENERATED_BODY Hello_Source_Hello_MyInterface_h_8_INCLASS_IINTERFACE_NO_PURE_DECLS - - -
收集:

static时收集到各容器中

  • Class: TArray<FFieldCompiledInInfo> GetDeferredClassRegistration()、TArray<class UClass *()()> GetDeferredCompiledInRegistration()

  • Enum: TArray GetDeferredCompiledInEnumRegistration()

  • Struct: TArray GetDeferredCompiledInStructRegistration()、TMap<FName,UScriptStruct::ICppStructOps*> GetDeferredCppStructOps()

  • Function: void(UObject::*GCasts[255])(FFrame &Stack, RESULT_DECL)、UClass::AddNativeFunction、Native GNatives[256]

  • UObject: 提前注册:IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction) -> UObject::StaticClass()

注册:
  • static时生成第一个UCLASS: UObject::StaticClass()

  • Main函数中CoreUObject模块加载时调用UClassRegisterAllCompiledInClasses(消费GetDeferredClassRegistration): TClass::StaticClass(),把CoreUObject里面定义的类的UClass都给先构建出来。但是其实这些UClass对象内部的值还没有完成初始化设置
    描述对象类型的只有UClass,UScriptStruct和UEnum是两个保存结构和枚举元数据信息的对象,而构造对象就需要先有其UClass。

  • InitUObject调用UObjectProcessRegistrants->UObjectForceRegistration->UObjectBase::DeferredRegister(在UObjectBaseInit初始化结束后,就已经可以开始NewObject了,标志着整个UObject系统的成功创建)建立各UClass*对象之间互相的联系(OuterPrivate,ClassPrivate,SuperStruct,NamePrivate)
    1. NamePrivate:定义了对象的名字
    2. OuterPrivate:定义了对象的从属关系
    3. ClassPrivate:定义了对象的类型关系
    4. SuperStruct:定义了类型的继承关系
  • 多次调用ProcessNewlyLoadedUObjects,调用UClassRegisterAllCompiledInClasses(消费GetDeferredClassRegistration)进行TClass::StaticClass()生成UClass,调用GetDeferredCompiledInRegistration、GetDeferredCompiledInStructRegistration、GetDeferredCompiledInEnumRegistration(消费收集阶段信息)提取收集到的注册项信息,调用UObjectProcessRegistrants注册UClass,调用UObjectLoadAllCompiledInStructs->Z_Construct开头的函数为代码里的枚举和结构构造类型对象,调用UObjectLoadAllCompiledInDefaultProperties->Z_Construct_UClass_UMyClass为代码里的类继续构造UClass对象,调用UClass::AssembleReferenceTokenStreams构造引用记号流,为后续GC用
    1. UClassRegisterAllCompiledInClasses里面主要是为每一个编译进来的class调用TClass::StaticClass()来构造出UClass*对象。
    2. GetDeferredCompiledInRegistration()是之前信息收集的时候static FCompiledInDefer变量初始化时收集到的全局数组,和定义的class一对一。
    3. GetDeferredCompiledInStructRegistration()是之前信息收集的时候static FCompiledInDeferStruct变量初始化时收集到的全局数组,和定义的struct一对一。
    4. GetDeferredCompiledInEnumRegistration()是之前信息收集的时候static FCompiledInDeferEnum变量初始化时收集到的全局数组,和定义的enum一对一。
    5. UObjectProcessRegistrants()为之前生成的UClass注册,生成其Package。这里调用的目的是在后续的操作之前确保内存里已经把相关的类型UClass对象都已经注册完毕。
    6. UObjectLoadAllCompiledInStructs()里为enum和struct分别生成UEnum和UScriptStruct对象。
    7. UObjectLoadAllCompiledInDefaultProperties()里为UClass*们继续构造和创建类默认对象(CDO)。
    8. 最后一步判断如果有新UClass*对象生成了,并且现在不在初始化载入阶段(GIsInitialLoad初始=true,只有在后续开启GC后才=false表示初始化载入过程结束了),用AssembleReferenceTokenStreams为UClass创建引用记号流(一种辅助GC分析对象引用的数据结构)。

后记

  • GENERATED_UCLASS_BODY():属于之前版本的宏,自动生成带有指定参数的构造函数,不用用户再去生明构造函数,需要在CPP中实现,否则报错,之后成员是public。
  • GENERATED_BODY():属于新版本的宏,不生成构造函数,如果需要自定义,需要自己声明并定义,之后成员是private。
  • 总结:使用后者,不要使用前者。

参考文献

文章及图片主要参考InsideUE4,持续更新中…