GameSubSystem简单实现

一种简单的维护 SubSystem 的解决方案,参考 UE 自带的 FSubsystemCollection 实现。

在维护各个业务时,经常需要将一个上层的 Manager,下面再拆出若干个子系统,需要一种快捷的方法快速扩展出一套 SUbSystem 系统。

基本结构

classDiagram
	Manager..*FGameSubSystemCollection
	class Manager {
	SubSystemCollections : FGameSubSystemCollection~USubSystemBase~
	}
	
	class FGameSubSystemCollection {
	FGameSubSystemCollection()
	
	}
	
	FGameSubSystemCollectionBase<|--FGameSubSystemCollection
	class FGameSubSystemCollectionBase {
        Outer : TWeakObjectPtr~UObject~
        SubSystemMap : TMap~UClass*,TStrongObjectPtr[UGameSubSystemBase]~
        BaseType : UClass*
        
        Init()
        Uninit()
        Tick()
        
        FGameSubSystemCollectionBase(UClass* InBaseType)
        AddSubSystemByClass(UClass* SubSystemClass)
        RemoveSubSystemByClass(UClass* SubSystemClass)
    }
   
    
    FGameSubSystemCollectionBase..>UGameSubSystemBase
    class UGameSubSystemBase {
    	LastTickTime : float
    	
    	+ Init()
    	+ Uninit()
    	+ Tick()
    	# OnInit()
    	# OnUnInit()
    	# OnTick(float DeltaTime)
    	
    	# GetTickInternal()
    	# GetTimeNow()
    	
    }
    
    UGameSubSystemBase<|--UGameSubSystem
    class UGameSubSystem {
    	# OnInit()
    	# OnUnInit()
    	# OnTick(float DeltaTime)
    	
    	# GetTickInternal()
    }

使用方法

Manager 持有对应类型的 SubSystemCollection,主动调用 InitUninitTick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Manager.h
{
public:
template<class T>
typename TEnableIf<TIsDerivedFrom<T, UGameSubSystem>::Value, T*>::Type
GetSubSystem()
{
return SubSystemCollections.GetSubSystem<T>();
}

private:
FGameSubSystemCollection <UGameSubSystem> SubSystemCollections;
}

// Manager.cpp
// 在合适的时机:
SubSystemCollections.Init()
SubSystemCollections.Uninit()
SubSystemCollections.Tick()

SubSystemCollection

SubSystemCollection 负责收集与管理所有的 SubSystem

SubSystemCollectionBase 中,提供一个 BaseClass,这个 BaseClass 由对应的 SubSystemCollection 在初始化的时候传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename TBaseType>
struct FGameSubSystemCollection : FGameSubSystemCollectionBase
{
static_assert(TIsDerivedFrom<TBaseType, UGameSubSystemBase>::Value, "TBaseType must inherit from UGameSubSystemBase");

FGameSubSystemCollection() : FGameSubSystemCollectionBase(TBaseType::StaticClass())
{
}
}


FGameSubSystemCollectionBase::FGameSubSystemCollectionBase(UClass* InBaseType)
: BaseType(InBaseType)
{
check(BaseType);
}

Init 的时候,根据 BaseClass,找到所有继承于该类的 ClassGetDerivedClasses(BaseType, SubSystemClasses, true)),然后进行 AddSubSystemByClass

用一个 TMap < UClass*, TStrongObjectPtr<UGameSubSystemBase> > SubSystemMap 将所有的 SubSystem 实例保存下来,这里由于我们期望 FGameSubSystemCollection 可以在编译期决定类型,所以使用了 template<typename TBaseType> ,导致无法走 UHT 的反射,挂上UPROPERTY() 来保证生命周期。所以这里需要用 TStrongObjectPtr 来保证这个 Collection 内部的 SubSystem 不会被 GC 掉,同时保证被 Manager 持有时生命周期正确。

同时我们需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// SubSystemCollectionBase.h

struct FGameSubSystemCollectionBase
{
// 外部调用
public:
void Init(UObject* InOuter);
void Uninit();
void Tick();

protected:
FGameSubSystemCollectionBase(UClass* InBaseType);

template<class TSubSystemClass>
typename TEnableIf<TIsDerivedFrom<TSubSystemClass, UGameSubSystemBase>::Value, UGameSubSystemBase*>::Type
GetSubSystemInternal() const
{
if (auto SubSystem = SubSystemMap.FindRef( TSubSystemClass::StaticClass() ); SubSystem.IsValid())
{
return SubSystem.Get();
}
return nullptr;
}

template<class TSubSystemClass>
typename TEnableIf<TIsDerivedFrom<TSubSystemClass, UGameSubSystemBase>::Value, TArray<UGameSubSystemBase*>>::Type
GetSubSystemsInternal() const
{
TArray <TSubSystemClass*> OutArray;
for (auto& Pair : SubSystemMap)
{
if (!Pair.Value.IsValid()) continue;
{
OutArray.Add( Pair.Value.Get() );
}
}
return OutArray;
}

private:
UGameSubSystemBase* AddSubSystemByClass(UClass* SubSystemClass);
void RemoveSubSystemByClass(UClass* SubSystemClass);


private:
TWeakObjectPtr <UObject> Outer = nullptr;
TMap < UClass*, TStrongObjectPtr<UGameSubSystemBase> > SubSystemMap;
UClass* BaseType = nullptr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// SubSystemCollectionBase.cpp
FGameSubSystemCollectionBase::FGameSubSystemCollectionBase(UClass* InBaseType)
: BaseType(InBaseType)
{
check(BaseType);
}


void FGameSubSystemCollectionBase::Init(UObject* InOuter)
{
if (Outer.IsValid()) return; // already initialized
if (InOuter == nullptr) return; // invalid Outer

Outer = InOuter;

TArray<UClass*> SubSystemClasses;
GetDerivedClasses(BaseType, SubSystemClasses, true);
for (UClass* SubSystemClass : SubSystemClasses)
{
AddSubSystemByClass(SubSystemClass);
}
}

void FGameSubSystemCollectionBase::Tick()
{
for (auto& Pair : SubSystemMap)
{
auto SubSystem = Pair.Value;
if (SubSystem.IsValid())
{
SubSystem->Tick();
}
}
}

void FGameSubSystemCollectionBase::Uninit()
{
TArray <UClass*> SubSystemClasses;
SubSystemMap.GetKeys(SubSystemClasses);
for (auto SubSystemClass : SubSystemClasses)
{
RemoveSubSystemByClass(SubSystemClass);
}
}

UGameSubSystemBase* FGameSubSystemCollectionBase::AddSubSystemByClass(UClass* SubSystemClass)
{
if (SubSystemClass == nullptr || !Outer.IsValid()) return nullptr;
if (SubSystemClass->HasAnyClassFlags(CLASS_Abstract)) return nullptr;

if (SubSystemMap.Contains( SubSystemClass ))
return SubSystemMap.FindRef(SubSystemClass).Get();

UGameSubSystemBase* SubSystem = NewObject<UGameSubSystemBase>(Outer.Get(), SubSystemClass);
SubSystemMap.Add( SubSystemClass, TStrongObjectPtr(SubSystem) );
SubSystem->Init();
return SubSystem;
}

void FGameSubSystemCollectionBase::RemoveSubSystemByClass(UClass* SubSystemClass)
{
if (SubSystemClass == nullptr || !Outer.IsValid()) return;
if (!SubSystemMap.Contains( SubSystemClass )) return;

auto SubSystem = SubSystemMap.FindAndRemoveChecked(SubSystemClass);
if (!SubSystem.IsValid()) return;
SubSystem->Uninit();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SubSystemCollection.h

template<typename TBaseType>
struct FGameSubSystemCollection : FGameSubSystemCollectionBase
{
static_assert(TIsDerivedFrom<TBaseType, UGameSubSystemBase>::Value, "TBaseType must inherit from UGameSubSystemBase");

FGameSubSystemCollection() : FGameSubSystemCollectionBase(TBaseType::StaticClass())
{
}

public:
template<class TSubSystemClass>
typename TEnableIf<TIsDerivedFrom<TSubSystemClass, TBaseType>::Value, TSubSystemClass*>::Type
GetSubSystem() const
{
return static_cast<TSubSystemClass*>(GetSubSystemInternal<TSubSystemClass>());
}

template<class TSubSystemClass>
typename TEnableIf<TIsDerivedFrom<TSubSystemClass, TBaseType>::Value, TArray<TSubSystemClass*>>::Type
GetSubSystems() const
{
const TArray<UGameSubSystemBase*>& Array = GetSubSystemsInternal<TSubSystemClass>();
const TArray<TSubSystemClass*>* SpecificArray = reinterpret_cast<const TArray<TSubSystemClass*>*>(&Array);
return *SpecificArray;
}
};

特别地,注意这里的 FGameSubSystemCollection::GetSubSystems,由于 GetSubSystemsInternal 返回的都是 UGameSubSystemBase 指针,内存大小和 TSubSystemClass 指针一样,所以可以使用 reinterpret_cast 直接将整个数组的类型转化,节省一步 O(n)cast 的开销。

SubSystemBase

我们需要一个 SubSystemBase,负责管理给 SubSystemCollection 持有,同时给各自定义的 SubSystem 继承。

提供一些基础的方法,InitUninitTickSubSystemCollection 调用。

同时子类只需要关心:OnInitOnUninitOnTick

对于各自的业务,大家各自继承自定义的 SubSystemBase ,然后再自定义各自的 SubSytem 继承于这个业务扩展出来的 SubSystemBase 即可。

SubSystemBase->GetOuter() 就可以拿到对应的 Manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
UCLASS(Abstract)
class UGameSubSystemBase : public UObject
{
GENERATED_BODY()

public:
void Init();
void Tick();
void Uninit();

protected:
virtual void OnInit();
virtual void OnUninit();
virtual void OnTick(float DeltaTime);

protected:
virtual float GetTickInternal() { return -1.0f; }
virtual float GetTimeNow();

private:
float LastTickTime;
};

对于 Tick ,维护一个 LastTickTime,用于计算 DeltaTime

1
2
3
4
5
6
7
8
9
10
11
12
void UGameSubSystemBase::Tick()
{
float TickInternal = GetTickInternal();
if (TickInternal < 0) return;

float CurrentTickTime = GetTimeNow();
if (CurrentTickTime - LastTickTime > TickInternal)
{
OnTick(CurrentTickTime - LastTickTime);
LastTickTime = CurrentTickTime;
}
}