BlueRoseNote/03-UnrealEngine/Gameplay/GAS/(16)在联机项目中使用GAS的注意点.md
2023-06-29 11:55:02 +08:00

9.9 KiB
Raw Permalink Blame History

前言

大多数人应该是会通过学习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<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);
}