TeamSystem框架
一个简单的组队系统:支持玩家的 加入
、退出
队伍,以及维护队伍的各种数据(比如 Members
、Score
)。
首先需要一个全局的 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
一个组队系统,最基本的功能就是玩家的 JoinTeam
、LeaveTeam
,对应的,需要 TeamSystem
支持队伍的 Create
、Destroy
。
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_OwnerOnly
:Client-OwnerOnly Data
, 通过 TeamComponent(PlayerState)-OwnerOnly
同步(若某个字段,Client
只关心自己队伍上的,则放在这)
FTeamSyncData_Common
: All 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 { 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
进行同步:
对于 SYNC_COUNT
标记 UPROPERTY
并同步;
对于 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) ; bool operator ==(FTeamSyncCommonDatas const & Other) const { return (SYNC_COUNT == Other.SYNC_COUNT); } 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)
}
由于原始数据存在 DS
的 UTeam
上,实现一个 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); } } } NotifySyncDataChanged (ETeamSyncDataType::OwnerOnly);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); for (auto Member : Team->GetMembers ()) { Member->UpdateTeamSyncData ( SyncData ); } } void UTeamSyncSystem::SyncTeamData_CommonCallback (uint64 TeamID) { FTeamSyncData_Common SyncData; Team->CollectSyncData_Common (SyncData); 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 (); } } 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 virtual ETeamSyncDataType GetRegisterSyncDataType () override { return ETeamSyncDataType::OwnerOnly; }virtual ETeamSyncDataType GetRegisterSyncDataType () override { return ETeamSyncDataType::Common; }virtual ETeamSyncDataType GetRegisterSyncDataType () override { return ETeamSyncDataType::OwnerOnly | ETeamSyncDataType::Common; }
各个 SubSystem
自己重载需要的 OnSyncTeamDataOwnerOnlyChanged
或 OnSyncTeamDataCommonChanged
,可以根据 Data
、LastData
进行数据的 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)) { if (auto Component = UTeamUtils::GetComponentByUID (GetWorld (), MemberUID); IsValid (Component)) { Component->UpdateTeamID (TeamID); } UTeamUtils::NotifyTeamMemberChanged (TeamID, MemberUID, true ); } } for (auto MemberUID : LastData.MemberUIDs) { if (!NewData.MemberUIDs.Contains (MemberUID)) { if (auto Component = UTeamUtils::GetComponentByUID (GetWorld (), MemberUID); IsValid (Component)) { Component->UpdateTeamID (0 ); } UTeamUtils::NotifyTeamMemberChanged (TeamID, MemberUID, false ); } } }