## 前言 大多数人应该是会通过学习UE4的ActionRPG项目来入门GAS框架的。但在最近自己做Demo的过程中才发现里面的部分不适用网络联机。比如ActionRPG将Character与Controller的指针存在GameMode,因为Server与Client只有一个GameMode的关系,就不符合多人联机游戏的规则。所以在经过实践之后这里我分享一些要点仅供抛砖引玉。这里我再推荐各位读者去看一下GASDocument以及附带的工程,它展现了正确的设计思路。 ## Ability中的网络同步节点 WaitNetSync ## GASDocument中的处理方式 将AbilitySystemComponent以及AttributeSet放在PlayerState中,之后再Character类中的PossesseBy()事件(ServerOnly)与OnRep_PlayerState()事件(Client)给角色类的ASC弱智能指针与AttributeSet弱智能指针赋值。 ``` AGDPlayerState::AGDPlayerState() { AbilitySystemComponent = CreateDefaultSubobject(TEXT("AbilitySystemComponent")); AbilitySystemComponent->SetIsReplicated(true); // Mixed mode means we only are replicated the GEs to ourself, not the GEs to simulated proxies. If another GDPlayerState (Hero) receives a GE, // we won't be told about it by the Server. Attributes, GameplayTags, and GameplayCues will still replicate to us. AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); AttributeSetBase = CreateDefaultSubobject(TEXT("AttributeSetBase")); NetUpdateFrequency = 100.0f; } // TWeakObjectPtr AbilitySystemComponent; // TWeakObjectPtr AttributeSetBase; // Server only void AGDHeroCharacter::PossessedBy(AController * NewController) { Super::PossessedBy(NewController); AGDPlayerState* PS = GetPlayerState(); if (PS) { // Set the ASC on the Server. Clients do this in OnRep_PlayerState() AbilitySystemComponent = Cast(PS->GetAbilitySystemComponent()); // AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers. PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this); AttributeSetBase = PS->GetAttributeSetBase(); } //... } // Client only void AGDHeroCharacter::OnRep_PlayerState() { Super::OnRep_PlayerState(); AGDPlayerState* PS = GetPlayerState(); if (PS) { // Set the ASC for clients. Server does this in PossessedBy. AbilitySystemComponent = Cast(PS->GetAbilitySystemComponent()); // Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor. AbilitySystemComponent->InitAbilityActorInfo(PS, this); AttributeSetBase = PS->GetAttributeSetBase(); } // ... } ``` ## 预测 ![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/GAS%E9%A2%84%E6%B5%8B.jpg) ## PlayerState PlayerState只在开发多人游戏时有用,在设计上负责管理角色的变量(PlayerId、UniqueId、SessionName与自己定义的变量)。 ### 指定与初始化 在GameMode中设置实现的PlayerState类,之后在AController::InitPlayerState()中Spawn。 ### 访问方法 - Pawn->PlayerState - Controller->PlayerState(一般用这个) 另外还可以通过GameState->PlayerArray获取所有有效的GameState实例。 ### PlayerState PlayerState在切换地图时会默认销毁,为了解决这个问题可以通过实现CopyProperties方法来解决。 ``` class ARPGPlayerState : public APlayerState { GENERATED_BODY() public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "ActionRPG") ARPGCharacterBase* Character; protected: /** 这里是为了解决切换地图时的属性丢失问题 */ /** Copy properties which need to be saved in inactive PlayerState */ virtual void CopyProperties(APlayerState* PlayerState); }; ``` ``` void ARPGPlayerState::CopyProperties(APlayerState* PlayerState) { //如果不是因为断线 if (IsFromPreviousLevel()) { ARPGPlayerState* RPGPlayerState=Cast(PlayerState); RPGPlayerState->Character = Character; } } ``` ### NetUpdateFrequency PlayerState的NetUpdateFrequency和角色类的效果是一样的,都是调节更新频率。默认数值对于GAS太低了,会导致滞后性。100是个比较高的数值,你也可以按照需求自己调节。 ## Controller ### 获取PlayerControllers - GetWorld()->GetPlayerControllerIterator() - PlayerState->GetOwner() - Pawn->GetController() ### BeginPlay里不做任何与GAS相关的操作 因为在服务端,Possession会发生在在BeginPlay前,客户端Possession发生在BeginPlay后,所以我们不能在BeginPlay中写任何有关ASC的逻辑除非PlayerState已经同步。 ### 相关判断服务端客户端状态函数 在Ability类中一些操作可能需要判断是服务端还是客户端,比如应用GameplayEffect就在服务端执行(使用HasAuthority()),而创建UI就需要在客户端执行(使用IsLocallyControlled())。 #### 蓝图 - HasAuthority()判断是否是服务端或是作为主机的玩家 - IsLocallyControlled()判断是否是本地控制器 - IsServer()通过World的ENetMode来段是服务端 #### c++ - 蓝图函数HasAuthority()在c++中为`return (GetLocalRole() == ROLE_Authority);` - IsLocallyControlled(),c++中有对应函数。 ## NetSecurityPolicy GameplayAbility的网络安全策略决定了Ability应该在网络的何处执行. 它为尝试执行限制Ability的客户端提供了保护. - ClientOrServer:没有安全需求. 客户端或服务端可以自由地触发该Ability的执行和终止. - ServerOnlyExecution:客户端对该Ability请求的执行会被服务端忽略, 但客户端仍可以请求服务端取消或结束该Ability. - ServerOnlyTermination:客户端对该Ability请求的取消或结束会被服务端忽略, 但客户端仍可以请求执行该Ability. - ServerOnly:服务端控制该Ability的执行和终止, 客户端的任何请求都会被忽略. ## NetExecutionPolicy 推荐看这位大佬的文章:https://zhuanlan.zhihu.com/p/143637846 ## 属性集属性UI更新 这里推荐使用GASDocument中的GameplayTasks更新方法。 ``` #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintAsyncActionBase.h" #include "AbilitySystemComponent.h" #include "AsyncTaskAttributeChanged.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnAttributeChanged, FGameplayAttribute, Attribute, float, NewValue, float, OldValue); /** * Blueprint node to automatically register a listener for all attribute changes in an AbilitySystemComponent. * Useful to use in UI. */ UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask)) class RPGGAMEPLAYABILITY_API UAsyncTaskAttributeChanged : public UBlueprintAsyncActionBase { GENERATED_BODY() public: UPROPERTY(BlueprintAssignable) FOnAttributeChanged OnAttributeChanged; // Listens for an attribute changing. UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true")) static UAsyncTaskAttributeChanged* ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute); // Listens for an attribute changing. // Version that takes in an array of Attributes. Check the Attribute output for which Attribute changed. UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true")) static UAsyncTaskAttributeChanged* ListenForAttributesChange(UAbilitySystemComponent* AbilitySystemComponent, TArray Attributes); // You must call this function manually when you want the AsyncTask to end. // For UMG Widgets, you would call it in the Widget's Destruct event. UFUNCTION(BlueprintCallable) void EndTask(); protected: UPROPERTY() UAbilitySystemComponent* ASC; FGameplayAttribute AttributeToListenFor; TArray AttributesToListenFor; void AttributeChanged(const FOnAttributeChangeData& Data); }; ``` ``` #include "Abilities/AsyncTasks/AsyncTaskAttributeChanged.h" UAsyncTaskAttributeChanged* UAsyncTaskAttributeChanged::ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute) { UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject(); WaitForAttributeChangedTask->ASC = AbilitySystemComponent; WaitForAttributeChangedTask->AttributeToListenFor = Attribute; if (!IsValid(AbilitySystemComponent) || !Attribute.IsValid()) { WaitForAttributeChangedTask->RemoveFromRoot(); return nullptr; } AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTaskAttributeChanged::AttributeChanged); return WaitForAttributeChangedTask; } UAsyncTaskAttributeChanged * UAsyncTaskAttributeChanged::ListenForAttributesChange(UAbilitySystemComponent * AbilitySystemComponent, TArray Attributes) { UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject(); WaitForAttributeChangedTask->ASC = AbilitySystemComponent; WaitForAttributeChangedTask->AttributesToListenFor = Attributes; if (!IsValid(AbilitySystemComponent) || Attributes.Num() < 1) { WaitForAttributeChangedTask->RemoveFromRoot(); return nullptr; } for (FGameplayAttribute Attribute : Attributes) { AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(WaitForAttributeChangedTask, &UAsyncTaskAttributeChanged::AttributeChanged); } return WaitForAttributeChangedTask; } void UAsyncTaskAttributeChanged::EndTask() { if (IsValid(ASC)) { ASC->GetGameplayAttributeValueChangeDelegate(AttributeToListenFor).RemoveAll(this); for (FGameplayAttribute Attribute : AttributesToListenFor) { ASC->GetGameplayAttributeValueChangeDelegate(Attribute).RemoveAll(this); } } SetReadyToDestroy(); MarkPendingKill(); } void UAsyncTaskAttributeChanged::AttributeChanged(const FOnAttributeChangeData & Data) { OnAttributeChanged.Broadcast(Data.Attribute, Data.NewValue, Data.OldValue); } ```