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

247 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 前言
大多数人应该是会通过学习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();
}
// ...
}
```
## 预测
![](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<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);
}
```