跳到主要内容
信息
· 文章中可能会出现一些错误,希望大佬们可以在评论区指出;

05 - Gameplay Effect Context

本文介绍 UE5 中 Gameplay 技能系统(Gameplay Ability System, GAS)里的 Gameplay Effect Context 相关内容。

概述

GameplayEffectContext(FGameplayEffectContext)是 GAS 中用于描述一次 GameplayEffect 应用来源及命中上下文的数据容器,通常包含以下信息:

  • Instigator:效果的发起者;
  • EffectCauser:效果的直接造成者,如武器、投射物、技能 Actor;
  • AbilityCDO:产生该效果的 Ability 类默认对象;
  • AbilityInstanceNotReplicated:未复制的 Ability 实例引用;
  • SourceObject:额外的来源对象,如装备、道具;
  • Actors:与本次效果相关的一组 Actor;
  • HitResult:命中结果,包含命中位置、法线、骨骼等信息;

其中,SourceObject 等数据需要通过 Set/AddXXXX() 方法手动添加。

自定义

如果默认的 GameplayEffectContext 无法满足需求,可以继承 FGameplayEffectContext 来添加自定义数据。

以 Aura 项目为例:角色通过 GE 对敌人造成伤害时,会经过格挡减伤、防御减伤、暴击判定等一系列运算。这些判定结果需要存储到自定义的 FAuraGameplayEffectContext 类中。下面以此为例,介绍自定义 GameplayEffectContext 的完整步骤:

重写必需函数

官方在 GameplayEffectTypes.h 中明确要求必须重写以下 3 个函数:

  1. UScriptStruct* GetScriptStruct():为底层反射系统提供数据结构信息;
  2. bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess):负责网络序列化和反序列化;
  3. FAuraGameplayEffectContext* Duplicate():提供深拷贝功能,确保复制时不会丢失自定义数据,返回值须为本结构体类型;

其中,GetScriptStruct() 直接参考父类实现即可。下面重点介绍如何重写 NetSerialize() 函数。

GetScriptStruct()

直接沿用 FGameplayEffectContext 的实现方式即可:

代码实现点击展开/折叠代码
// AuraAbilityTypes.h
// 必须重写此方法, 用于为反射系统提供数据
virtual UScriptStruct* GetScriptStruct() const override;

// AuraAbilityTypes.cpp
UScriptStruct* FAuraGameplayEffectContext::GetScriptStruct() const
{
return StaticStruct();
}

NetSerialize()

该函数包含 3 个参数:

  1. FArchive& Ar:序列化数据的载体,负责数据的存储和加载。通过 << 运算符将数据写入或读取,可通过 Ar.IsSaving()Ar.IsLoading() 判断当前操作类型。
  2. UPackageMap* Map:将对象和名称映射到索引,以进行网络通信。数据以字节流形式传输,此参数负责字节数据与具体对象之间的映射,确保目标客户端能正确还原对象引用。
  3. bool& bOutSuccess:标记序列化是否成功。引擎根据此值判断是否丢弃本次网络同步并记录错误日志。

返回值表示引擎是否能使用该序列化函数。实现示例如下(参考父类):

代码实现点击展开/折叠代码
// AuraAbilityTypes.h
// 必须重写此方法, 用于网络同步
virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;

// AuraAbilityTypes.cpp
bool FAuraGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;

// 序列化阶段: 通过按位或RepBits确认要存储多少数据
if (Ar.IsSaving())
{
#pragma region FGameplayEffectContext
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid())
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
#pragma endregion

// 自定义结构体添加了下面2个bool类型属性
if (bIsBlockedHit)
{
RepBits |= 1 << 7;
}
if (bIsCriticalHit)
{
RepBits |= 1 << 8;
}
}
// 让Archive知道要存储多少数据
Ar.SerializeBits(&RepBits, 9);

// 序列化/反序列化阶段: 通过按位与的结果存储/读取相应数据
#pragma region FGameplayEffectContext
if (RepBits & (1 << 0))
{
// 对于一般类型, 使用<<即可
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
// 对于数组类型, 需要使用该函数
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
// 特定类型拥有自己的NetSerialize()函数
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = MakeShared<FHitResult>();
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
#pragma endregion

if (RepBits & (1 << 7))
{
Ar << bIsBlockedHit;
}
if (RepBits & (1 << 8))
{
Ar << bIsCriticalHit;
}

#pragma region FGameplayEffectContext
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
#pragma endregion

bOutSuccess = true;
return true;
}

Duplicate()

同样参考父类实现即可:

代码实现点击展开/折叠代码
// AuraAbilityTypes.h
// 必须重写此方法, 保证GameplayEffectContext在被复制时不丢失自定义数据, 且返回值类型为本结构体类型
virtual FAuraGameplayEffectContext* Duplicate() const override;

// AuraAbilityTypes.cpp
FAuraGameplayEffectContext* FAuraGameplayEffectContext::Duplicate() const
{
FAuraGameplayEffectContext* NewContext = new FAuraGameplayEffectContext();
*NewContext = *this;
if (GetHitResult())
{
// Does a deep copy of the hit result
NewContext->AddHitResult(*GetHitResult(), true);
}
return NewContext;
}

编写 TStructOpsTypeTraits

还需参考 FGameplayEffectContext 编写 TStructOpsTypeTraits<> 模板特化。该模板用于告诉引擎该结构体支持哪些底层行为(如网络序列化、拷贝等),从而让引擎调用相应的处理逻辑:

AuraAbilityTypes.h点击展开/折叠代码
// 必须定义的结构体: 用于告诉引擎该结构体支持哪些底层行为(例如网络序列化, 拷贝等), 进而让引擎调用相关逻辑
template <>
struct TStructOpsTypeTraits<FAuraGameplayEffectContext> : TStructOpsTypeTraitsBase2<FAuraGameplayEffectContext>
{
enum
{
WithNetSerializer = true,
WithCopy = true // Necessary so that TSharedPtr<FHitResult> Data is copied around
};
};

创建并绑定 AbilitySystemGlobals 类

在使用自定义 GameplayEffectContext 前,需要创建一个 AbilitySystemGlobals 类(用于存放 GAS 相关的自定义全局变量和方法),并重写其 AllocGameplayEffectContext() 函数。该函数会在 ASC->MakeEffectContext() 时被调用,从而创建我们自定义的 GameplayEffectContext:

AuraAbilitySystemGlobals 类点击展开/折叠代码
// .h
UCLASS()
class AURA_API UAuraAbilitySystemGlobals : public UAbilitySystemGlobals
{
GENERATED_BODY()

public:
virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
};

// .cpp
FGameplayEffectContext* UAuraAbilitySystemGlobals::AllocGameplayEffectContext() const
{
return new FAuraGameplayEffectContext();
}

最后在 Config/DefaultGame.ini 中添加如下配置,使 GAS 使用我们创建的 AbilitySystemGlobals 类:

[/Script/GameplayAbilities.AbilitySystemGlobals]
+AbilitySystemGlobalsClassName="/Script/Aura.AuraAbilitySystemGlobals"

其中,/Script/[Aura].{AuraAbilitySystemGlobals} 的格式为:[ ] 内是项目模块名,{ } 内是自定义的 AbilitySystemGlobals 类名。配置完成后,运行时即可使用自定义的 GameplayEffectContext:

参考资料