TeamSystem框架

一个简单的组队系统:支持玩家的 加入退出 队伍,以及维护队伍的各种数据(比如 MembersScore)。

首先需要一个全局的 TeamManager ,以及一个在 Player 身上的 TeamComponent 负责维护 Player 相关的组队信息;

同时,将业务拆分为多个 TeamSubSystems

以及最重点的 数据同步,根据不同的数据类型,进行不同方式的数据同步。

TeamSubSystemBase

首先,需要将业务拆分为多个 SubSystem ,通过 TeamManager 持有 SubSystemCollection 来实现这个功能。

需要一个 TeamSubSystemBase ,在这里传入 System = TeamManager 以及为后续同步数据的分发做准备。

参考:[UE]GameSubSystem简单实现

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

protected:
virtual void OnInit() override;
virtual void OnUninit() override;

protected:
TWeakObjectPtr <class UTeamManager> System = nullptr;
}

void UTeamSubSystemBase::OnInit()
{
Super::OnInit();
System = Cast<UTeamManager>(GetOuter());
}

TeamMemberSystem

一个组队系统,最基本的功能就是玩家的 JoinTeamLeaveTeam,对应的,需要 TeamSystem 支持队伍的 CreateDestroy

classDiagram
    UTeamManager..>UTeam
    UTeamManager..>UTeamSubSystemBase
    class UTeamManager {
        SubSystemCollections : FGameSubSystemCollection~UTeamSubSystemBase~
        Teams : TMap~uint64|UTeam*~ 
        CreateTeam()
        DestoryTeam()
        GetTeam(uint64 TeamID)
    }
    
	class UTeamComponent {
        TeamID : uint64
        UpdateTeamID()
    }
    
    class UTeamSubSystemBase {
    	System : TWeakObjectPtr~class UTeamManager~
    	# OnInit()
    	# OnUninit()
    }
    
    
    UTeamMemberSystem..>UTeam
    UTeamSubSystemBase<|--UTeamMemberSystem
    class UTeamMemberSystem {
    	+JoinTeam(TeamID, PlayerUID)
    	+LeaveTeam(PlayerUID)
    	- GetOrCreateTeam(TeamID)
    }
    
    UTeam..>UTeamComponent
    class UTeam {
    	TeamID : uint64
    	Members : TArray~ TWeakObjectPtr[UTeamComponent] ~
    }

伪代码示例:

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
void UTeamMemberSystem::JoinTeam(uint64 TeamID, uint64 PlayerUID, bool bProcessID)
{
if (!IsStandaloneOrDS(this)) return;
auto Team = GetOrCreateTeam(TeamID);
TeamComponent->UpdateTeamID(Team->GetTeamID());
Team->AddMember(TeamComponent);
}

void UTeamMemberSystem::LeaveTeam(uint64 PlayerUID)
{
if (!IsStandaloneOrDS(this)) return;
Team->RemoveMember(TeamComponent);
TeamComponent->UpdateTeamID(0);
if (Team->GetMembers().Num() == 0)
{
System->DestoryTeam( TeamID );
}
}

UTeam* UTeamMemberSystem::GetOrCreateTeam(uint64 TeamID)
{
if (auto Team = System->GetTeam(TeamID); !IsValid(Team))
{
System->CreateTeam(TeamID);
}
return System->GetTeam(TeamID);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UTeam* UTeamManager::CreateTeam(uint64 TeamID)
{
if (GetTeam(TeamID) != nullptr) return nullptr;
UTeam* NewTeam = NewObject<UTeam>(this);
NewTeam->Init(TeamID);
Teams.Add( TeamID, NewTeam );
return NewTeam;
}

void UTeamManager::DestoryTeam(uint64 TeamID)
{
auto Team = GetTeam(TeamID);
if (!IsValid(Team)) return;
Team->Uninit();
Teams.Remove(TeamID);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void UTeam::Init(uint64 InTeamID)
{
TeamID = InTeamID;
Members.Empty();
}

void UTeam::AddMember(TWeakObjectPtr<UTeamComponent> InMember)
{
Members.Add(InMember);
}

void UTeam::RemoveMember(TWeakObjectPtr<UTeamComponent> InMember)
{
Members.Remove(InMember);
}

TeamScoreSystem

组队需要得分,每个队伍维护一个 Score

classDiagram
    
    class UTeamSubSystemBase {
    }
    
    UTeamScoreSystem--|>UTeamSubSystemBase
    class UTeamScoreSystem {
    	+ AddScore(TeamID, InScore)
    	+ ClearScore(TeamID)
    }
   
    UTeamScoreSystem..>UTeam
    class UTeam {
    	Score : float
    	SetScore()
    	GetScore()
    }

伪代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
void UTeamScoreSystem::AddScore(uint64 TeamID, int InAddScore)
{
if (!IsStandaloneOrDS(this)) return;
int LastScore = GetScore(TeamID);
int NewScore = LastScore + InAddScore;
SetScore( TeamID, NewScore);
}

void UTeamScoreSystem::ClearScore(uint64 TeamID)

if (!IsStandaloneOrDS(this)) return;
SetScore( TeamID, 0);
}

TeamSyncSystem

数据的同步,是该组队系统的 重点 ,考虑一下对于各种各样的数据,有哪些本质的类型。

比如上述有 队伍成员 Members队伍分数 Score 这两种数据,这两个数据的差别在于:

① Members:对于玩家客户端,自己只关心自己队伍的 Members,不关心其它队伍的 Members

② Score:对于玩家客户端,不仅关心自己队伍的 Score,也关心其它队伍的 Score

于是将数据拆为这两种类型,进行不同的同步方式,同时需要将同步数据的 转发通知 分发到各个 SubSystem 里执行。

将这数据定义为这两种类型:

1
2
3
4
5
6
7
8
UENUM()
enum class ETeamSyncDataType
{
None = 0,
OwnerOnly = 1 << 1,
Common = 1 << 2,
};
ENUM_CLASS_FLAGS(ETeamSyncDataType);

FTeamSyncData_OwnerOnlyClient-OwnerOnly Data, 通过 TeamComponent(PlayerState)-OwnerOnly 同步(若某个字段,Client 只关心自己队伍上的,则放在这)

FTeamSyncData_CommonAll Data, 通过 TeamManager(GameState) 同步(若某个数据,1P 关心其它队伍上的数据,则放在这)

特别注意的是,有时候某些数据同时存在于两种类型中,比如 TeamID

数据类型:OwnerOnlyData

OwnerOnlyData 比较简单,直接将数据打包好,然后设置给 TeamComponent(PS) 来同步即可,将同步数据 Condition 设置为 COND_OwnerOnly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
USTRUCT()
struct FTeamSyncData_OwnerOnly
{
GENERATED_BODY()

FTeamSyncData_OwnerOnly() = default;

public:
UPROPERTY()
uint64 TeamID;
UPROPERTY()
TArray <uint64> MemberUIDs;

public:
bool operator==(const FTeamSyncData_OwnerOnly& Other) const;
bool operator!=(const FTeamSyncData_OwnerOnly& Other) const
{
return !(*this == Other);
}

public:
FString ToString() const;
};

数据类型:CommonData

CommonData 的原始数据比较简单:

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
USTRUCT()
struct FTeamSyncData_Common
{
GENERATED_BODY()

FTeamSyncData_Common() = default;

public:
UPROPERTY()
uint64 TeamID = 0;
UPROPERTY()
int Score = 0;

public:

friend FArchive& operator<<(FArchive& Ar, FTeamSyncData_Common& Data);
bool operator==(const FTeamSyncData_Common& Other) const;
bool operator!=(const FTeamSyncData_Common& Other) const
{
return !(*this == Other);
}

public:
FString ToString() const;
};

FArchive& operator<<(FArchive& Ar, FTeamSyncData_Common& Data)
{
Ar << Data.TeamID;
Ar << Data.Score;
return Ar;
}

但是由于这个数据通过 TeamManager 来同步,需要维护 TeamID->CommonData 这个映射。

但是由于 TMap 不能直接挂 UPROPERTY() 进行同步,所以需要自定义一下 NetSerialize 来进行同步。

TeamManager 中设置:

1
2
3
4
5
// Manager
{
UPROPERTY(Replicated, PushModelProperty, ReplicatedUsing = OnRep_SyncDatas)
FTeamSyncCommonDatas SyncDatas;
}

通过一个 SyncDatas 来同步数据,这个 SyncDatas 里保存了 TMap <uint64, FTeamSyncData_Common> Datas,以及一个用于触发同步的 SyncCount

每次数据 Update 或者 Remove 的时候,MARK_SYNC_DIRTY 来将 SyncCount++,并且 MarkDirty 一下,触发数据同步。

注意这里的 SYNC_COUNT,用于驱动数据同步,在数据同步的比较过程中:

flowchart LR
FObjectReplicator::ReplicateProperties --> FRepLayout::DiffProperties --> PropertiesAreIdentical

会比较 RepLayoutCmd 是否 Identical,这里的 RepLayout 由反射生成;

如果对于 USTRUCT 标记了 WithIdenticalViaEquality = true,则会在 UClass::Identical 时使用重载 operator == 来判断是否一致。

所以可以通过两种办法来标记自定义 USTRUCT 进行同步:

  1. 对于 SYNC_COUNT 标记 UPROPERTY 并同步;

  2. 对于 SYNC_COUNT 无需标记 UPROPERTY,而是对该 USTRUCT 标记 WithIdenticalViaEquality = true,并重载 operator ==:判断 SYNC_COUNT == Other.SYNC_COUNT;同时,为了保证 Client== 的有效性,在 IsLoading 时候也 SYNC_COUNT++

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
USTRUCT()
struct FTeamSyncCommonDatas
{
GENERATED_BODY()

FTeamSyncCommonDatas() = default;

public:
FTeamSyncData_Common GetData(uint64 TeamID) const;
const TMap<uint64, FTeamSyncData_Common>& GetDatas() const;
void UpdateData(uint64 TeamID, const FTeamSyncData_Common& InData);
void RemoveData(uint64 TeamID);
private:
TMap <uint64, FTeamSyncData_Common> Datas;

public:
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);

/** Comparison operator */
bool operator==(FTeamSyncCommonDatas const& Other) const
{
return (SYNC_COUNT == Other.SYNC_COUNT);
}

/** Comparison operator */
bool operator!=(FTeamSyncCommonDatas const& Other) const
{
return !(FTeamSyncCommonDatas::operator==(Other));
}

private:
void MARK_SYNC_DIRTY();
uint32 SYNC_COUNT = 0;
};


template<>
struct TStructOpsTypeTraits<FTeamSyncCommonDatas> : TStructOpsTypeTraitsBase2<FTeamSyncCommonDatas>
{
enum
{
WithNetSerializer = true,
WithIdenticalViaEquality = true,
};
};
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
FTeamSyncData_Common FTeamSyncCommonDatas::GetData(uint64 TeamID) const
{
if (!Datas.Contains(TeamID)) return {};
return Datas[TeamID];
}

const TMap<uint64, FTeamSyncData_Common>& FTeamSyncCommonDatas::GetDatas() const
{
return Datas;
}

void FTeamSyncCommonDatas::UpdateData(uint64 TeamID, const FTeamSyncData_Common& InData)
{
Datas.Add( TeamID, InData );
MARK_SYNC_DIRTY();
}

void FTeamSyncCommonDatas::RemoveData(uint64 TeamID)
{
Datas.Remove(TeamID);
MARK_SYNC_DIRTY();
}

void FTeamSyncCommonDatas::MARK_SYNC_DIRTY()
{
SYNC_COUNT++;
}

bool FTeamSyncCommonDatas::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
Ar << Datas;
if (Ar.IsLoading())
{
SYNC_COUNT++;
}

return bOutSuccess = true;
}

数据同步

classDiagram
	class UTeam {
    	+ CollectSyncData_OwnerOnly(FTeamSyncData_OwnerOnly& SyncData)
    	+ CollectSyncData_Common(FTeamSyncData_Common& SyncData)
    	- NotifySyncDataChanged(ETeamSyncDataType DataType)
    }
    
    UTeam..>Delegate
    Delegate..>UTeamSubSystemBase
    
    class UTeamSubSystemBase {
    	- RegisterSyncData()
    	- UnregisterSyncData()
    	# OnSyncTeamDataOwnerOnlyChanged(TeamID, NewData, LastData)
    	# OnSyncTeamDataCommonChanged(TeamID, NewData, LastData)
    }

由于原始数据存在 DSUTeam 上,实现一个 Notify 方法,在数据变化的时候,通过 Delegate 调用到 TeamSyncSystem,进行数据的分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void UTeam::NotifySyncDataChanged(ETeamSyncDataType DataType)
{
if (EnumHasAllFlags(DataType, ETeamSyncDataType::OwnerOnly))
{
if (auto Delegate = UTeamCommonDelegate::GetDelegate()->OnSyncTeamData_OwnerOnly; Delegate.IsBound())
{
Delegate.Execute(TeamID);
}
}

if (EnumHasAllFlags(DataType, ETeamSyncDataType::Common))
{
if (auto Delegate = UTeamCommonDelegate::GetDelegate()->OnSyncTeamData_Common; Delegate.IsBound())
{
Delegate.Execute(TeamID);
}
}
}

// AddMember、RemoveMember 时 Members 发生变化后:
NotifySyncDataChanged(ETeamSyncDataType::OwnerOnly);
// SetScore 时 Score发生变化
NotifySyncDataChanged(ETeamSyncDataType::Common);

TeamSyncData : TeamSubSystemBase 里绑定两种数据变化的 Delegate,对数据进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UTeamSyncSystem::SyncTeamData_OwnerOnlyCallback(uint64 TeamID)
{
FTeamSyncData_OwnerOnly SyncData;
Team->CollectSyncData_OwnerOnly(SyncData);
// 对队伍里的所有成员修改 SyncData
for (auto Member : Team->GetMembers())
{
Member->UpdateTeamSyncData( SyncData );
}
}

void UTeamSyncSystem::SyncTeamData_CommonCallback(uint64 TeamID)
{
FTeamSyncData_Common SyncData;
Team->CollectSyncData_Common(SyncData);
// 将数据设置到 Manager 里
System->UpdateTeamSyncData( TeamID, SyncData );
}

特别地,在 DestroyTeam 时,进行 RemoveSyncData;在玩家 LeaveTeam 时,UpdateTeamSyncData( {} ) 清空数据。

数据分发

显然需要在 DS/Clinet 进行数据的分发,将数据变化重新分发回各个业务。

于是在对应数据的 OnRep 里将数据传给 TeamSubSystemBase,并且分发数据给各个子系统。

flowchart TD
Team--Notify-->TeamSyncSystem

TeamSyncSystem--SetData-->TeamComponent
TeamSyncSystem--SetData-->TeamManager

TeamComponent--OnRep_SyncData : OwnerOnly-->TeamSubSystemBase
TeamManager--OnRep_SyncDatas : Common-->TeamSubSystemBase

TeamSubSystemBase--OnSync : OwnerOnlyData-->TeamMemberSystem
TeamSubSystemBase--OnSync : CommonData-->TeamScoreSystem
TeamSubSystemBase--OnSync-->OtherSystem...

RepData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void UTeamManager::UpdateTeamSyncData(uint64 TeamID, const FTeamSyncData_Common& InData)
{
GetSyncDatas_Mutable().UpdateData( TeamID, InData );
}

void UTeamManager::RemoveTeamSyncData(uint64 TeamID)
{
GetSyncDatas_Mutable().RemoveData(TeamID);
}

void UTeamManager::OnRep_SyncDatas(const FTeamSyncCommonDatas& LastSyncDatas)
{
for (const auto& [TeamID, NewData] : SyncDatas.GetDatas())
{
if (TeamID == 0) continue;
if (auto LastData = LastSyncDatas.GetData(TeamID); LastData != NewData)
{
if (auto Delegate = UTeamCommonDelegate::GetDelegate()->OnSyncTeamDataCommonChanged; Delegate.IsBound())
{
Delegate.Execute(TeamID, NewData, LastData);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void UTeamManager::OnRep_SyncDatas(const FTeamSyncCommonDatas& LastSyncDatas)
{
for (const auto& [TeamID, NewData] : SyncDatas.GetDatas())
{
if (TeamID == 0) continue;
if (auto LastData = LastSyncDatas.GetData(TeamID); LastData != NewData)
{
if (auto Delegate = UTeamCommonDelegate::GetDelegate()->OnSyncTeamDataCommonChanged; Delegate.IsBound())
{
Delegate.Execute(TeamID, NewData, LastData);
}
}
}
}

Data Register&Changed

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
UCLASS(Abstract)
class UTeamSubSystemBase : public UGameSubSystemBase
{
GENERATED_BODY()

// ----- 客户端收到数据后的事件分发 ------

protected:
virtual ETeamSyncDataType GetRegisterSyncDataType() { return ETeamSyncDataType::None; }

private:
void RegisterSyncData();
void UnregisterSyncData();
};


void UTeamSubSystemBase::RegisterSyncData()
{
if (!IsClient(this)) return;

auto Type = GetRegisterSyncDataType();

if (EnumHasAllFlags(Type, ETeamSyncDataType::OwnerOnly))
{
UTeamCommonDelegate::GetDelegate()->OnSyncTeamDataOwnerOnlyChanged.BindDynamic(this, &ThisClass::OnSyncTeamDataOwnerOnlyChanged );
}

if (EnumHasAllFlags(Type, ETeamSyncDataType::Common))
{
UTeamCommonDelegate::GetDelegate()->OnSyncTeamDataCommonChanged.BindDynamic(this, &ThisClass::OnSyncTeamDataCommonChanged );
}
}

void UTeamSubSystemBase::UnregisterSyncData()
{
if (!IsClient(this)) return;
auto Type = GetRegisterSyncDataType();

if (EnumHasAllFlags(Type, ETeamSyncDataType::OwnerOnly))
{
UTeamCommonDelegate::GetDelegate()->OnSyncTeamDataOwnerOnlyChanged.Unbind();
}

if (EnumHasAllFlags(Type, ETeamSyncDataType::Common))
{
UTeamCommonDelegate::GetDelegate()->OnSyncTeamDataCommonChanged.Unbind();
}
}

// 供 SubSytem 重载
void UTeamSubSystemBase::OnSyncTeamDataOwnerOnlyChanged(uint64 TeamID, const FTeamSyncData_OwnerOnly& NewData, const FTeamSyncData_OwnerOnly& LastData)
{
}

void UTeamSubSystemBase::OnSyncTeamDataCommonChanged(uint64 TeamID, const FTeamSyncData_Common& NewData, const FTeamSyncData_Common& LastData)
{
}

在每个不同的 SubSystem 里,通过 override GetRegisterSyncDataType 来决定自己需要监听哪种类型的数据(也可以都监听)

1
2
3
4
5
6
// TeamMemberSystem
virtual ETeamSyncDataType GetRegisterSyncDataType() override { return ETeamSyncDataType::OwnerOnly; }
// TeamScoreSystem:
virtual ETeamSyncDataType GetRegisterSyncDataType() override { return ETeamSyncDataType::Common; }
// OtherSystem:
virtual ETeamSyncDataType GetRegisterSyncDataType() override { return ETeamSyncDataType::OwnerOnly | ETeamSyncDataType::Common; }

各个 SubSystem 自己重载需要的 OnSyncTeamDataOwnerOnlyChangedOnSyncTeamDataCommonChanged,可以根据 DataLastData 进行数据的 Diff 并进行事件通知,比如:

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
void UTeamMemberSystem::OnSyncTeamDataOwnerOnlyChanged(uint64 TeamID, const FTeamSyncData_OwnerOnly& NewData, const FTeamSyncData_OwnerOnly& LastData)
{
Super::OnSyncTeamDataOwnerOnlyChanged(TeamID, NewData, LastData);

for (auto MemberUID : NewData.MemberUIDs)
{
if (!LastData.MemberUIDs.Contains(MemberUID))
{
// ADD
if (auto Component = UTeamUtils::GetComponentByUID(GetWorld(), MemberUID); IsValid(Component)) // 客户端设置 TeamID, 防止同步延迟问题
{
Component->UpdateTeamID(TeamID);
}
UTeamUtils::NotifyTeamMemberChanged(TeamID, MemberUID, true);
}
}

for (auto MemberUID : LastData.MemberUIDs)
{
if (!NewData.MemberUIDs.Contains(MemberUID))
{
//Remove
if (auto Component = UTeamUtils::GetComponentByUID(GetWorld(), MemberUID); IsValid(Component)) // 客户端设置 TeamID, 防止同步延迟问题
{
Component->UpdateTeamID(0);
}
UTeamUtils::NotifyTeamMemberChanged(TeamID, MemberUID, false);
}
}

}