涉及的文件及其说明:
代码示例:
DDTypes:
//#pragma region XXX是VS中的宏。使用这个宏后,代码编辑框左边会有+ -的符号,可以把代码块收展起来。
#pragma region LogDebug
class DATADRIVER_API DDRecord
{
private:
//使用单例模式
static TSharedPtr<DDRecord> RecordInst = nullptr;
//调试信息
FString RecordInfo;
//显示时间
float ShowTime;
//显示颜色
FColor ShowColor;
public:
//打印模式:0-Debug,1-Log,2-Warning,3-Error
uint8 PatternID;
public:
inline DDRecord() {
}
~DDRecord() {
}
//这里定义的Get是为后面DDH的命名空间使用,而不是在使用打印时以Get为开始
static TSharedPtr<DDRecord> Get()
{
if (!RecordInst.IsValid) {
RecordInst = MakeShareable(new DDRecord());
}
return RecordInst;
}
//设置颜色及显示时间参数
inline void InitParam(float InTime, FColor InColor)
{
ShowTime = InTime;
ShowColor = InColor;
}
//执行打印方法
inline void Output()
{
switch (PatternID)
{
case 0:
if (GEngine)
GEngine->AddOnScreenDebugMessage(-1, ShowTime, ShowColor, RecordInfo);
break;
case 1:
UE_LOG(LogTemp, Log, TEXT("%s"), *RecordInfo);
break;
case 2:
UE_LOG(LogTemp, Warning, TEXT("%s"), *RecordInfo);
break;
case 3:
UE_LOG(LogTemp, Error, TEXT("%s"), *RecordInfo);
break;
}
//清空字符串
RecordInfo.Empty();
}
//重写操作符
inline DDRecord& operator<<(FString Info){
RecordInfo.Append(Info); return *this; }
inline DDRecord& operator<<(FName Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FText Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(const char* Info) {
RecordInfo+=Info; return *this; }
inline DDRecord& operator<<(const char Info) {
RecordInfo.AppendChar(Info); return *this; }
inline DDRecord& operator<<(int32 Info) {
RecordInfo.Append(FString::FromInt(Info)); return *this; }
inline DDRecord& operator<<(float Info) {
RecordInfo.Append(FString::SanitizeFloat(Info)); return *this; }
inline DDRecord& operator<<(double Info) {
RecordInfo.Append(FString::SanitizeFloat(Info)); return *this; }
inline DDRecord& operator<<(bool Info) {
RecordInfo.Append(Info?"true":"false"); return *this; }
inline DDRecord& operator<<(FVector2D Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FVector Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FRotator Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FQuat Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FTransform Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FMatrix Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FColor Info) {
RecordInfo.Append(Info.ToString()); return *this; }
inline DDRecord& operator<<(FLinearColor Info) {
RecordInfo.Append(Info.ToString()); return *this; }
//当最后一个输入是DDRecord对象时,输出结果
inline void operator<<(DDRecord& Record) {
Record.Output(); }
};
#pragma endregion
DDCommon:
使用打印命令时,我们使用的是DDH命名空间提供的方法作为开头和结束。
namespace DDH
{
//Debug是打印的开始,可以设置打印的参数
FORCEINLINE DDRecord& Debug(float InTime = 100.f, FColor InColor = FColor::Yellow)
{
DDRecord::Get()->PatternID = 0;
DDRecord::Get()->InitParam(InTime, InColor);
return *DDRecord::Get();
}
FORCEINLINE DDRecord& Debug(FColor InColor)
{
DDRecord::Get()->PatternID = 0;
DDRecord::Get()->InitParam(100.f, InColor);
return *DDRecord::Get();
}
//输出日志
FORCEINLINE DDRecord& Log()
{
DDRecord::Get()->PatternID = 1;
return *DDRecord::Get();
}
//输出警告
FORCEINLINE DDRecord& Warning()
{
DDRecord::Get()->PatternID = 2;
return *DDRecord::Get();
}
//输出错误
FORCEINLINE DDRecord& Error()
{
DDRecord::Get()->PatternID = 3;
return *DDRecord::Get();
}
//结束输入,输出打印结果
FORCEINLINE DDRecord& Endl()
{
return *DDRecord::Get();
}
}
打印函数调用:
DDP::Debug() << "Hello, World!" << 2020 << "." <<DDP::Endl();
DDP::Debug(FColor::Red) << "Hello, World!" << 2020 << "." <<DDP::Endl();
下图是框架的各个模块及组件的初始化及运行流程:
首先,Driver初始化并构造CenterModule。再次期间,各个子组件(Module)也将被初始化。在PostInitializeComponents函数执行时,Driver除了将自身注册到Common外,还会迭代执行CenterModule子组件的Manager,而Manager负责创建Module的Model、Message、Wealth三个部分。另外各个子模块的Init、BeginPlay及Tick也将依次执行,这里通过各层级的三个函数依次调用实现。
Common
我们把Common写成单例模式,并且将Driver对象的引用保存到Common。这样,我们就可以随时方便的获取到Driver了。当然Driver也会包含子组件的引用等,所以通过这样逻辑包含关系就可以通过Common和Driver方便地获取到框架的大部分核心组件。
UCLASS()
class DATADRIVER_API UDDCommon : public UObject
{
GENERATED_BODY()
public:
static UDDCommon* Get()
{
if(!CommonInst)
{
CommonInst = NewObject<UDDCommon>();
CommonInst->AddToRoot(); //添加到根,防止被GC
}
return CommonInst;
}
void InitDriver(ADDDriver* Driver){
DriverInst = Driver;}
ADDDriver* GetDriver(){
return DriverInst;}
private:
static UDDCommon* CommonInst = nullptr;
static ADDDriver* DriverInst;
};
Driver
主要是初始化各模块组件,并将子组件的方法函数放入到Driver的PostInitializeComponents、BeginPlay、Tick等函数,借这些函数将子模块或组件驱动起来。Driver通过CenterModule的迭代函数,不断执行CenterModule下的子Module组件成员函数来带动执行。
UCLASS()
class DATADRIVER_API ADDDriver : public AActor
{
GENERATED_BODY()
public:
ADDDriver(); //构造器将创建根组件和CenterModule,CenterModule附着在根组件上
virtual void PostInitializeComponents() override
{
Super::PostInitializeComponents();
UDDCommon::Get()->InitDriver(this); //注册到Driver
CenterModule->IterChangeModuleType(CenterModule, ModuleType);
CenterModule->IterCreateManager(CenterModule);
}
virtual void Tick(float DeltaTime) override; //像其它函数一样,这里迭代BeginPlay和Tick
protected:
virtual void BeginPlay() override
{
Super::BeginPlay();
CenterModule->IterModuleInit(CenterModule);
}
public:
//NoClear-防止该对象引用在编辑器中被设置为None.隐藏编辑器的清除(以及浏览)按钮。
UPROPERTY(EditAnywhere, NoClear, BlueprintReadOnly, Category = "DataDriver")
USceneComponent* RootModule;
UPROPERTY(EditAnywhere, NoClear, BlueprintReadOnly, Category = "DataDriver")
UDDCenterModule* CenterModule;
UPROPERTY(EditDefaultsOnly, Category = "DataDriver")
FName ModuleType;
protected:
bool IsBeginPlay; //将IterModuleBeginPlay放到Tick的第一帧执行,此变量保存是否执行。
};
IDDMM
IDDM是Model、Message、Wealth的抽象父类,保存了Model、Message、Wealth的父对象引用(Module和Driver),注意这里的父对象指的是父组件而非父类。Module通过调用AssignModule来将其自身引用保存到子组件对象上。
class DATADRIVER_API IDDMM
{
GENERATED_BODY()
public:
void AssignModule(UDDModule* Module)
{
ParentModule = Module;
Driver = UDDCommon::Get()->GetDriver();
}
protected:
ADDDriver* Driver;
UDDModule* ParentModule;
};
Module
Module是Driver的子组件,每个Module负责一类事务处理,例如HUD或Player等。其包含三个子组件Model、Message、Wealth,说是三个子组件并不完全对,因为这三个类集成UObject而非USceneComponent。所以,为了更方便地获取与调用这三个部分,所以将保存他们的引用。
Module也将重新声明新的Init、BeginPlay及Tick函数,这三个函数驱动Model、Message、Wealth的三个函数执行。
Module通过CreateManager函数实例化三个组件对象,并将其注册到GC的根,并注册自身引用到他们的父对象(ParentModule)上。
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class DATADRIVER_API UDDModule : public USceneComponent
{
GENERATED_BODY()
public:
UDDModule();
void CreateManager()
{
//创建Model、Message、Wealth子对象
Model = NewObject<UDDModel>();
Message = NewObject<UDDMessage>();
Wealth = NewObject<UDDWealth>();
//防止被GC
Model->AddToRoot();
Message->AddToRoot();
Wealth->AddToRoot();
//将自身引用保存到三个子对象
Model->AssignModule(this);
Message->AssignModule(this);
Wealth->AssignModule(this);
}
virtual void ModuleInit()
{
//下边三个方法同此方法
Model->ModelInit();
Message->MessageInit();
Wealth->WealthInit();
}
virtual void ModuleBeginPlay();
virtual void ModuleTick(float DeltaSeconds);
public:
TArray<UDDModule*> ChildrenModule;
protected:
UDDModel* Model;
UDDMessage* Message;
UDDWealth* Wealth;
};
CenterModule
CenterModule是中央模组,下图为CenterModule及其他Module的层级关系
通过上图,我们也可以看出,其它模组位于CenterModule下。CenterModule是一个核心模组,包含其子模组引用及模组的迭代函数,Driver通过调用核心模组的迭代函数驱动所有模组的运行。
UCLASS()
class DATADRIVER_API UDDCenterModule : public UDDModule
{
GENERATED_BODY()
public:
void IterChangeModuleType(UDDModule* Module, FName ModuleType) {
for (int i = 0; i < Module->GetAttachChildren().Num(); ++i) {
//获取并保存附着在Module下的子Module
UDDModule* ChildModule = Cast<UDDModule>(Module->GetAttachChildren()[i]);
if (ChildModule) {
Module->ChildrenModule.Push(ChildModule);
IterChangeModuleType(ChildModule, ModuleType);
}
}
}
void IterCreateManager(UDDModule* Module){
//迭代执行子模块,下同
Module->CreateManager();
for (int i = 0; i < Module->ChildrenModule.Num(); ++i)
IterCreateManager(Module->ChildrenModule[i]);
}
void IterModuleInit(UDDModule* Module);
void IterModuleBeginPlay(UDDModule* Module);
void IterModuleTick(UDDModule* Module, float DeltaSeconds);
};
Model、Message、Wealth
这三个块是Module的三个子组件,分别负责数据、事件和资源三个内容。这节声明三个组件的Init、BeginPlay及Tick函数,并且不再适用通过继承UObject而来的相似函数。目的是为了统一他们的调用顺序。
UCLASS()
class DATADRIVER_API UDDModel : public UObject, public IDDMM
{
GENERATED_BODY()
public:
virtual void ModelInit(){
}
virtual void ModelBeginPlay(){
}
virtual void ModelTick(float DeltaSeconds){
}
};
枚举与Module一一对应
枚举值与字符串的互转
我们将写几个枚举类的辅助函数,实现枚举值和字符串的互转。这样,我们可以调用辅助函数方便地通过枚举值获取枚举类的字符串形式或者通过字符串获取到枚举类的值了。
//通过枚举值获取其对应的字符串对象
template<typename TEnum>
FORCEINLINE FString GetEnumValueAsString(const FString& EnumName, TEnum EnumValue)
{
const UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, *EnumName, true);
if (!Enum)
return FString("InValid");
return Enum->GetEnumName((int32)EnumValue);
}
//通过枚举值获取FName类型的字符串
template<typename TEnum>
FORCEINLINE FName GetEnumValueAsName(const FString& EnumName, TEnum EnumValue)
{
const UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, *EnumName, true);
if (!Enum)
return FName("InValid");
return FName(Enum->GetEnumName((int32)EnumValue));
}
//通过FName类型的字符串获取枚举值
template<typename TEnum>
FORCEINLINE TEnum GetEnumValueFromName(const FString& EnumName, FName ValueName)
{
const UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, *EnumName, true);
if (!Enum)
return TEnum(0);
return (TEnum)Enum->GetIndexByName(ValueName);
}
//通过FName类型的字符串获取枚举值索引
FORCEINLINE int32 GetEnumIndexFromName(const FString& EnumName, FName ValueName)
{
const UEnum* Enum = FindObject<UEnum>((UObject*)ANY_PACKAGE, *EnumName, true);
if (!Enum)
return -1;
return Enum->GetIndexByName(ValueName);
}
在Module中获取并保存ModuleType的索引
当Driver的ModuleType修改后,使用PostEditChangeProperty函数迭代修改ModuleType的索引
在CenterModule中迭代获取所有Module,并按照数组索引值等于枚举值保存Module的指针。