概要
マルチプレイ時にGameplayAbilityクラスでクライアント側からのデータを受け取ってアクティベートするためのメモ書きです。
更新履歴
| 日付 | 内容 |
|---|---|
| 2026/06/30 | 初版 |
参考
以下を参考にさせて頂きました、ありがとうございます。
UE4でマルチプレイヤーゲームを作ろう【CEDEC 2019】
UE公式:ゲームプレイ アビリティ システムの概要
環境
Windows11
Visual Studio 2022
UnrealEngine 5.7
関連ソース
"Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\Abilities\Tasks\AbilityTask_WaitTargetData.h"
"Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\Abilities\GameplayAbilityTargetTypes.h"
クライアント側で起動したアビリティクラスでのデータ受け渡し
渡したいデータを AbilityTargetDataに入れて、そのハンドルをCallServerSetReplicatedTargetData()を使って送る。受け取り側は受け取り用タスクを起動して受け取り待ち、受け取ったあとにアビリティをアクティベートするというのが基本の流れです。
アビリティクラス
FGameplayAbilityTargetData_LocationInfoの LiteralTransform に座標と回転を入れてクライアント側から送信、サーバー側で受け取る例です。
UCLASS()
class TEST_API UMyGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
..省略..
// アビリティ発動時処理(アビリティ発動のために直接呼んではならない)
virtual void ActivateAbility(const FGameplayAbilitySpecHandle _Handle, const FGameplayAbilityActorInfo* _ActorInfo, const FGameplayAbilityActivationInfo _ActivationInfo, const FGameplayEventData* _TriggerEventData) override;
// クライアントからのデータを受け取った時に呼ばれる処理
UFUNCTION()
void OnClientDataReceived(const FGameplayAbilityTargetDataHandle& _Data);
protected:
UPROPERTY(transient)
FGameplayEventData TriggerEventData;
};
// アビリティ発動時処理
void UMyGameplayAbility::ActivateAbility(
const FGameplayAbilitySpecHandle _Handle,
const FGameplayAbilityActorInfo* _ActorInfo,
const FGameplayAbilityActivationInfo _ActivationInfo,
const FGameplayEventData* _TriggerEventData
)
{
if( !HasAuthorityOrPredictionKey(_ActorInfo, &_ActivationInfo) ){
return;
}
// ローカルプレイヤー
if( IsLocallyControlled() ){
if(IsPredictingClient()){
// テストデータの作成
auto* _Data = new FGameplayAbilityTargetData_LocationInfo;
_Data->TargetLocation.LiteralTransform.SetLocation(FVector(1, 2, 3));
auto _Rot = FRotator(45, 90, 0);
_Data->TargetLocation.LiteralTransform.SetRotation(_Rot.Quaternion());
FGameplayAbilityTargetDataHandle _DataHandle;
_DataHandle.Add(_Data);
FScopedPredictionWindow PredictionWindow(_ASC);
// データを送る
_ASC->CallServerSetReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), _DataHandle, FGameplayTag::EmptyTag, _ASC->ScopedPredictionKey);
}
// アビリティアクティブ化
Super::ActivateAbility(_Handle, _ActorInfo, _ActivationInfo, _TriggerEventData);
}
else if( IsForRemoteClient() ){
// クライアントからのデータ受け取り待ちタスク
auto _WaitTask = UAT_ServerWaitForClientTargetData::ServerWaitForClientTargetData(this, NAME_None, true);
_WaitTask->ValidData.AddDynamic(this, &ThisClass::OnClientDataReceived);
_WaitTask->ReadyForActivation();
if(_TriggerEventData){
TriggerEventData = *_TriggerEventData;
}
}
}
// クライアントからのデータ受け取り処理
void UMyGameplayAbility::OnClientDataReceived(const FGameplayAbilityTargetDataHandle& _Data)
{
if(const FGameplayAbilityTargetData_LocationInfo* _TargetData = static_cast<const FGameplayAbilityTargetData_LocationInfo*>(_Data.Get(0)) ){
// 受け取ったテストデータの表示
FVector _Loc = _TargetData->TargetLocation.LiteralTransform.GetLocation();
FRotator _Rot = _TargetData->TargetLocation.LiteralTransform.GetRotation().Rotator();
UE_LOG(LogTemp, Log, TEXT("TargetData : (%f,%f,%f), (%f,%f,%f)"),
_Loc.X, _Loc.Y, _Loc.Z,
_Rot.Pitch, _Rot.Roll, _Rot.Yaw
);
// 受け取ったデータに対応した処理を行う
}
// アビリティ起動
if (bHasBlueprintActivateFromEvent){
K2_ActivateAbilityFromEvent(TriggerEventData); // イベントデータ付きのアビリティアクティブ化
}
else if (bHasBlueprintActivate){
K2_ActivateAbility(); // アビリティアクティブ化
}
else if (bHasBlueprintActivateFromEvent){
UE_LOG(LogAppPlayer, Warning, TEXT("アビリティ %s に必要なイベントデータがない"), *GetName());
// アビリティ終了
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, false, true);
}
}
これでクライアント側がアビリティを起動し、そのデータを受け取ったサーバー側のアビリティがその後にアビリティを起動します。
ターゲットデータはプラグインに用意されているものをつかうか、FGameplayAbilityTargetDataを継承して必要なものを追加拡張することができます。
・IsPredictingClient()
クライアント側のローカルプレイヤーです。以下ソースのコメント
「アビリティがクライアント上で実行されているローカルに予測されたアビリティである場合は true を返します。 通常、これはサーバーに何かを伝える必要があることを意味します。」
・IsForRemoteClient()
サーバー側のローカルではない方のプレイヤーです。以下ソースのコメント
「ローカルに制御されていないクライアントの機能をサーバー上で実行している場合は true を返します。」
データ受け取りタスク
プラグインに用意されている
UAbilityTask_WaitTargetDataクラスを使うか、自前でタスククラスを作る必要があります。以下サンプルコード。
#pragma once
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "Abilities/Tasks/AbilityTask_WaitTargetData.h"
#include "AT_ServerWaitForClientTargetData.generated.h"
// データ受け取り待ちタスク
UCLASS()
class TEST_API UAT_ServerWaitForClientTargetData : public UAbilityTask
{
GENERATED_UCLASS_BODY()
UPROPERTY(BlueprintAssignable)
FWaitTargetDataDelegate ValidData;
UFUNCTION(BlueprintCallable, meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "true", HideSpawnParms = "Instigator"), Category = "Ability|Tasks")
static UAT_ServerWaitForClientTargetData* ServerWaitForClientTargetData(UGameplayAbility* _OwningAbility, FName _TaskInstanceName, bool _TriggerOnce);
virtual void Activate() override;
UFUNCTION()
void OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& _Data, FGameplayTag _ActivationTag);
protected:
virtual void OnDestroy(bool _AbilityEnded) override;
bool bTriggerOnce;
};
#include "./AT_ServerWaitForClientTargetData.h"
#include "AbilitySystemComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AT_ServerWaitForClientTargetData)
UAT_ServerWaitForClientTargetData::UAT_ServerWaitForClientTargetData(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{}
UAT_ServerWaitForClientTargetData* UAT_ServerWaitForClientTargetData::ServerWaitForClientTargetData(UGameplayAbility* _OwningAbility, FName _TaskInstanceName, bool _TriggerOnce)
{
UAT_ServerWaitForClientTargetData* _MyObj = NewAbilityTask<UAT_ServerWaitForClientTargetData>(_OwningAbility, _TaskInstanceName);
_MyObj->bTriggerOnce = _TriggerOnce;
return _MyObj;
}
void UAT_ServerWaitForClientTargetData::Activate()
{
if (!Ability || !Ability->GetCurrentActorInfo()->IsNetAuthority())
{
return;
}
FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();
FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();
// アビリティ/予測キーペアのTargetDataSetデリゲートに登録
AbilitySystemComponent->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject(this, &UAT_ServerWaitForClientTargetData::OnTargetDataReplicatedCallback);
// タスク登録前に TargetDataが既に届いてキャッシュされている場合を拾う
AbilitySystemComponent->CallReplicatedTargetDataDelegatesIfSet(SpecHandle, ActivationPredictionKey);
}
// データがレプリケートされた時に呼ばれる処理
void UAT_ServerWaitForClientTargetData::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& _Data, FGameplayTag _ActivationTag)
{
FGameplayAbilityTargetDataHandle _MutableData = _Data;
AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey()); // キャッシュされたTargetDataをクライアントから消す
if (ShouldBroadcastAbilityTaskDelegates()){
ValidData.Broadcast(_MutableData);
}
if (bTriggerOnce){
EndTask();
}
}
void UAT_ServerWaitForClientTargetData::OnDestroy(bool _AbilityEnded)
{
if (AbilitySystemComponent.Get()){
FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();
FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();
// デリゲートに登録された処理を削除
AbilitySystemComponent->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).RemoveAll(this);
}
Super::OnDestroy(_AbilityEnded);
}
ターゲットデータ
GameplayAbilityTargetDataを継承してint型プロパティを1つ追加する例です。
レプリケーションしているアクターならポインタなどもシステムが変換をしてくれるのでそのまま追加できます。
USTRUCT(BlueprintType)
struct FMyTargetData : public FGameplayAbilityTargetData
{
GENERATED_BODY()
public:
FMyTargetData(){}
FMyTargetData(int32 InLevel): Level(InLevel){}
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
Ar << Level;
bOutSuccess = true;
return(true);
}
virtual UScriptStruct* GetScriptStruct() const override
{
return FMyTargetData::StaticStruct();
}
public:
UPROPERTY()
int32 Level = 0;
};
template<>
struct TStructOpsTypeTraits< FMyTargetData > : public TStructOpsTypeTraitsBase2< FMyTargetData >
{
enum
{
WithNetSerializer = true,
};
};
シリアライザーを工夫して通信データサイズを減らすことも可能です。もし大量のデータがある場合は有効かもしれません。
また、FVector_NetQuantize などネットワークレプリケーション用に用意されている型を使うのもデータサイズ削減には有効になる場合もあると思われます(精度には要注意)。
まとめ
GameplayAbilityクラスを
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor()で起動する場合は FGameplayEventDataに送りたいデータを入れると良さそうですが、ネットワーク実行ポリシーが EGameplayAbilityNetExecutionPolicy::ServerInitiated になるためクライアント予測ができません(クライアント側からはそのままでは送れない)。
また移動を伴うアビリティの場合、サーバー/クライアントで座標の差が大きくなると位置ズレを起こすのでMovementComponent側での対応も必要になります。
やはりネットワークマルチプレイは難しいです。