“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类:
- 引擎的初始化加载,Init和ShutDown等(在引擎流程章节会详细叙述)
- Player的创建,如CreateLocalPlayer,GetLocalPlayers之类的。
- GameMode的重载修改,这是从4.14新增加进来改进,本来你只能为特定的某个Map配置好GameModeClass,但是现在GameInstance允许你重载它的PreloadContentForURL、CreateGameModeForURL和OverrideGameModeClass方法来hook改变这一流程。
- 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 |
- | - | - | 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)
- NamePrivate:定义了对象的名字
- OuterPrivate:定义了对象的从属关系
- ClassPrivate:定义了对象的类型关系
- SuperStruct:定义了类型的继承关系
- 多次调用ProcessNewlyLoadedUObjects,调用UClassRegisterAllCompiledInClasses(消费GetDeferredClassRegistration)进行TClass::StaticClass()生成UClass,调用GetDeferredCompiledInRegistration、GetDeferredCompiledInStructRegistration、GetDeferredCompiledInEnumRegistration(消费收集阶段信息)提取收集到的注册项信息,调用UObjectProcessRegistrants注册UClass,调用UObjectLoadAllCompiledInStructs->Z_Construct开头的函数为代码里的枚举和结构构造类型对象,调用UObjectLoadAllCompiledInDefaultProperties->Z_Construct_UClass_UMyClass为代码里的类继续构造UClass对象,调用UClass::AssembleReferenceTokenStreams构造引用记号流,为后续GC用
- UClassRegisterAllCompiledInClasses里面主要是为每一个编译进来的class调用TClass::StaticClass()来构造出UClass*对象。
- GetDeferredCompiledInRegistration()是之前信息收集的时候static FCompiledInDefer变量初始化时收集到的全局数组,和定义的class一对一。
- GetDeferredCompiledInStructRegistration()是之前信息收集的时候static FCompiledInDeferStruct变量初始化时收集到的全局数组,和定义的struct一对一。
- GetDeferredCompiledInEnumRegistration()是之前信息收集的时候static FCompiledInDeferEnum变量初始化时收集到的全局数组,和定义的enum一对一。
- UObjectProcessRegistrants()为之前生成的UClass注册,生成其Package。这里调用的目的是在后续的操作之前确保内存里已经把相关的类型UClass对象都已经注册完毕。
- UObjectLoadAllCompiledInStructs()里为enum和struct分别生成UEnum和UScriptStruct对象。
- UObjectLoadAllCompiledInDefaultProperties()里为UClass*们继续构造和创建类默认对象(CDO)。
- 最后一步判断如果有新UClass*对象生成了,并且现在不在初始化载入阶段(GIsInitialLoad初始=true,只有在后续开启GC后才=false表示初始化载入过程结束了),用AssembleReferenceTokenStreams为UClass创建引用记号流(一种辅助GC分析对象引用的数据结构)。
后记
- GENERATED_UCLASS_BODY():属于之前版本的宏,自动生成带有指定参数的构造函数,不用用户再去生明构造函数,需要在CPP中实现,否则报错,之后成员是public。
- GENERATED_BODY():属于新版本的宏,不生成构造函数,如果需要自定义,需要自己声明并定义,之后成员是private。
- 总结:使用后者,不要使用前者。
参考文献
文章及图片主要参考InsideUE4,持续更新中…