BlueRose
文章97
标签28
分类7
在联机项目中使用GAS的注意点

在联机项目中使用GAS的注意点

前言

大多数人应该是会通过学习UE4的ActionRPG项目来入门GAS框架的。但在最近自己做Demo的过程中才发现里面的部分不适用网络联机。比如ActionRPG将Character与Controller的指针存在GameMode,因为Server与Client只有一个GameMode的关系,就不符合多人联机游戏的规则。所以在经过实践之后这里我分享一些要点仅供抛砖引玉。这里我再推荐各位读者去看一下GASDocument以及附带的工程,它展现了正确的设计思路。

GASDocument中的处理方式

将AbilitySystemComponent以及AttributeSet放在PlayerState中,之后再Character类中的PossesseBy()事件(ServerOnly)与OnRep_PlayerState()事件(Client)给角色类的ASC弱智能指针与AttributeSet弱智能指针赋值。

AGDPlayerState::AGDPlayerState()
{
    AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(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<UGDAttributeSetBase>(TEXT("AttributeSetBase"));

    NetUpdateFrequency = 100.0f;
}

//     TWeakObjectPtr<class UGDAbilitySystemComponent> AbilitySystemComponent;
//  TWeakObjectPtr<class UGDAttributeSetBase> AttributeSetBase;
// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
    Super::PossessedBy(NewController);

    AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
    if (PS)
    {
        // Set the ASC on the Server. Clients do this in OnRep_PlayerState()
        AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(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<AGDPlayerState>();
    if (PS)
    {
        // Set the ASC for clients. Server does this in PossessedBy.
        AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(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();
    }

    // ...
}

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<ARPGPlayerState>(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<FGameplayAttribute> 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<FGameplayAttribute> AttributesToListenFor;

    void AttributeChanged(const FOnAttributeChangeData& Data);
};
#include "Abilities/AsyncTasks/AsyncTaskAttributeChanged.h"
UAsyncTaskAttributeChanged* UAsyncTaskAttributeChanged::ListenForAttributeChange(UAbilitySystemComponent* AbilitySystemComponent, FGameplayAttribute Attribute)
{
    UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTaskAttributeChanged>();
    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<FGameplayAttribute> Attributes)
{
    UAsyncTaskAttributeChanged* WaitForAttributeChangedTask = NewObject<UAsyncTaskAttributeChanged>();
    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);
}