247 lines
9.9 KiB
Markdown
247 lines
9.9 KiB
Markdown
|
## 前言
|
|||
|
大多数人应该是会通过学习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);
|
|||
|
}
|
|||
|
```
|