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 个函数:
UScriptStruct* GetScriptStruct():为底层反射系统提供数据结构信息;bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess):负责网络序列化和反序列化;FAuraGameplayEffectContext* Duplicate():提供深拷贝功能,确保复制时不会丢失自定义数据,返回值须为本结构体类型;
其中,GetScriptStruct() 直接参考父类实现即可。下面重点介绍如何重写 NetSerialize() 函数。
GetScriptStruct()
直接沿用 FGameplayEffectContext 的实现方式即可:
代码实现点击展开/折叠代码
// AuraAbilityTypes.h
// 必须重写此方法, 用于为反射系统提供数据
virtual UScriptStruct* GetScriptStruct() const override;
// AuraAbilityTypes.cpp
UScriptStruct* FAuraGameplayEffectContext::GetScriptStruct() const
{
return StaticStruct();
}
NetSerialize()
该函数包含 3 个参数:
FArchive& Ar:序列化数据的载体,负责数据的存储和加载。通过<<运算符将数据写入或读取,可通过Ar.IsSaving()和Ar.IsLoading()判断当前操作类型。UPackageMap* Map:将对象和名称映射到索引,以进行网络通信。数据以字节流形式传输,此参数负责字节数据与具体对象之间的映射,确保目标客户端能正确还原对象引用。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:
