在前面一篇文章中,我们创建了自定义的FGameplayEffectContext结构体,用于存储所需的内容。在自定义的结构体内,我们主要是为了增加暴击和格挡两个参数,用于后面的UI显示给玩家,让玩家知道当前触发的状态。并且我们还对齐做了序列化处理,能够在后续处理中,数据能够成功传递到服务器端展示。
在这一篇中,我们将实现如何将自定义的FGameplayEffectContext应用到我们的代码逻辑中,替换默认的FGameplayEffectContext。
模版特化
模板参数在某种特定类型下的具体实现称为模板的特化。
在GameplayEffectTypes.h文件中,有这么一段代码
它是针对于FGameplayEffectContext进行了模版特化处理。当你在项目中使用到了对应的类型结构体时,模版特化修改的内容也将起作用。
template<>
struct TStructOpsTypeTraits< FGameplayEffectContext > : public TStructOpsTypeTraitsBase2< FGameplayEffectContext >
{enum{WithNetSerializer = true,WithCopy = true // Necessary so that TSharedPtr<FHitResult> Data is copied around};
};
这段代码是在针对于FGameplayEffectContext 类型时,实现的具体设置,它继承至TStructOpsTypeTraitsBase2,接下来我们看一下TStructOpsTypeTraitsBase2。
TStructOpsTypeTraitsBase2是专门用于针对C++结构体进行模版特化,它作为结构体的默认设置。
/** 用于为自定义的脚本结构体提供类型特性 **/
template <class CPPSTRUCT>
struct TStructOpsTypeTraitsBase2
{enum{WithZeroConstructor = false, // 结构体是否可以通过将其内存占用填充为零来构造为有效对象。WithNoInitConstructor = false, // 结构体是否具有一个构造函数,该构造函数接受一个 EForceInit 参数,用于强制执行初始化,而默认构造函数执行“未初始化”。WithNoDestructor = false, // 当结构体被销毁时,其析构函数是否不会被调用。WithCopy = !TIsPODType<CPPSTRUCT>::Value, // 结构体是否可以通过其复制赋值操作符进行复制。WithIdenticalViaEquality = false, // 结构体是否可以通过其 operator== 进行比较。这与 WithIdentical 应该是互斥的。WithIdentical = false, // 结构体是否可以通过一个 Identical(const T* Other, uint32 PortFlags) 函数进行比较。这与 WithIdenticalViaEquality 应该是互斥的。WithExportTextItem = false, // 结构体是否具有一个 ExportTextItem 函数,用于将其状态序列化为字符串。WithImportTextItem = false, // s结构体是否具有一个 ImportTextItem 函数,用于从字符串反序列化对象。WithAddStructReferencedObjects = false, // 结构体是否具有一个 AddStructReferencedObjects 函数,允许它向垃圾收集器添加引用。WithSerializer = false, // 结构体是否具有一个 Serialize 函数,用于将其状态序列化为 FArchiveWithStructuredSerializer = false, // 结构体是否具有一个 Serialize 函数,用于将其状态序列化为 FStructuredArchive。WithPostSerialize = false, // 结构体是否具有一个在序列化后被调用的 PostSerialize 函数。WithNetSerializer = false, // 结构体是否具有一个 NetSerialize 函数,用于将状态序列化为用于网络复制的 FArchive。WithNetDeltaSerializer = false, // 结构体是否具有一个 NetDeltaSerialize 函数,用于序列化与先前 NetSerialize 操作中的状态差异。WithSerializeFromMismatchedTag = false, // 结构体是否具有一个 SerializeFromMismatchedTag 函数,用于从其他属性标签进行转换。WithStructuredSerializeFromMismatchedTag = false, // 结构体是否具有一个基于 FStructuredArchive 的 SerializeFromMismatchedTag 函数,用于从其他属性标签进行转换。WithPostScriptConstruct = false, // 结构体是否具有一个在蓝图中构造后被调用的 PostScriptConstruct 函数。WithNetSharedSerialization = false, // 结构体的 NetSerialize 函数是否不需要包映射来序列化其状态。WithGetPreloadDependencies = false, // 结构体是否具有一个 GetPreloadDependencies 函数,用于在加载时序列化结构体时返回所有将被 Preload() 的对象。WithPureVirtual = false, // 结构体是否具有 PURE_VIRTUAL 函数,并且在 CHECK_PUREVIRTUALS 为true时无法构造。WithFindInnerPropertyInstance = false, // 结构体是否具有一个 FindInnerPropertyInstance 函数,该函数可以在给定属性 FName 时提供一个 FProperty 和数据指针。WithCanEditChange = false, // 结构体是否具有一个仅在编辑器中使用的 CanEditChange 函数,该函数可以有条件地使子属性在详细信息面板中变为只读(与 UObject::CanEditChange 相同的想法)。};static constexpr EPropertyObjectReferenceType WithSerializerObjectReferences = EPropertyObjectReferenceType::Conservative; // 当结构体(或类)的 Serialize 方法遇到这些类型的对象引用时,可能会进行序列化。默认情况下,使用 Conservative(保守)策略,意味着如果对象引用的处理方式未知,那么对象引用收集器(可能是负责序列化和反序列化的组件)应该序列化这个结构体。
};
所以,我们自定义的也需要一个模版特化进行处理,基于FGameplayEffectContext我们自定义个即可。
在这里,我们将网络序列化和可复制设置为true,但是对于Hit Result这种复杂类型来说,它只会复制引用。
template<>
struct TStructOpsTypeTraits< FRPGGameplayEffectContext > : public TStructOpsTypeTraitsBase2< FRPGGameplayEffectContext >
{enum{WithNetSerializer = true,WithCopy = true // Necessary so that TSharedPtr<FHitResult> Data is copied around};
};
所以,我们还需要在结构体内实现复制函数,对Hit Result进行深拷贝,这里直接复制父类的复制函数修改即可。
/** 创建一个副本,用于后续网络复制或者后续修改 */virtual FRPGGameplayEffectContext* Duplicate() const override{FRPGGameplayEffectContext* NewContext = new FRPGGameplayEffectContext();*NewContext = *this; //WithCopy 设置为true,就可以通过赋值操作进行拷贝if (GetHitResult()){// 深拷贝 hit resultNewContext->AddHitResult(*GetHitResult(), true);}return NewContext;}
使用自定义的FGameplayEffectContext类
既然要使用自定义的类,那么我们需要找到创建的位置,比如我们当前火球术使用的技能类里面,会去创建它
通过ASC的MakeEffectContext()去创建的
我们进入此方法查看它是如何它的内部实现
在函数内部,它输出的是FGameplayEffectContextHandle类型,FGameplayEffectContextHandle内部包含FGameplayEffectContext,而FGameplayEffectContext是通过UAbilitySystemGlobals::Get().AllocGameplayEffectContext()实现的创建
而AllocGameplayEffectContext的实现则是直接new FGameplayEffectContext()了一个新的实例返回。
看到这里,我们就明白了,我们需要去修改UAbilitySystemGlobals使用,然后实现以后创建FGameplayEffectContext时,都是创建我们自定义的FGameplayEffectContext来实例化。所以,我们我们需要自定义一个AbilitySystemGlobals类,然后覆写这个函数
打开UE,在AbilitySystem目录下面新增一个C++类,选择AbilitySystemGlobals
命名为RPGAbilitySystemGlobals
在.h文件中,我们设置覆写AllocGameplayEffectContext函数。
UCLASS()
class AURA_API URPGAbilitySystemGlobals : public UAbilitySystemGlobals
{GENERATED_BODY()virtual FGameplayEffectContext* AllocGameplayEffectContext() const override;
};
在函数实现的cpp文件中,我们返回创建的自定义的FGameplayEffectContext
FGameplayEffectContext* URPGAbilitySystemGlobals::AllocGameplayEffectContext() const
{return new FRPGGameplayEffectContext();
}
接下来就是重要的一点,如何使用自定义的AbilitySystemGlobals,我们需要在配置项内去设置,然后修改了初始化AbilitySystemGlobals的类指向我们自定义的AbilitySystemGlobals类。
然后编译,在创建FGameplayEffectContext的地方打断点,查看创建的实例里面是否包含我们自定义的属性。并且我还发现我的属性名称写错了,竟然写成了大写,bool类型的第一个字母推荐小写b,我去改一下。
还有就是在AttributeSet里面是否能够获取到,因为AS是在服务器端运行的,如果能够在AS里面获取到,证明我们真的成功了
实现设置获取格挡和暴击属性
既然我们实现了自定义FGameplayEffectContext,并且值已经能够获取得到,那么我们需要在获取格挡和暴击的位置去设置布尔值。
在 UGameplayEffectExecutionCalculation中,我们可以获取到GE的Spec,我们通过Spec的函数GetContext获取句柄,并通过句柄的Get获取到Context
FGameplayEffectContext* EffectContext = Spec.GetContext().Get();
可以在句柄的代码中找到获取函数
接下来,我们将FGameplayEffectContext转换成我们创建的自定义类型,然后在转换这里一定要用static_cast,不然会报错。
static_cast是强制类型转换操作符
FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContext);
如果你使用内置的这种:
FRPGGameplayEffectContext* RPGEffectContext = Cast<FRPGGameplayEffectContext>(EffectContext);
它会编译引发错误
获取到自定义类型的context的上下文后,我们可以通过调用函数设置格挡
RPGEffectContext->SetIsBlockedHit(bBlocked);
为了方便使用,我们准备将设置和获取方法写入到蓝图函数库中,可以通过直接传入参数获取内容,并且还可以在蓝图中调用。
在蓝图函数库中创建两个静态函数,用于获取暴击和格挡
//获取当前GE是否触发格挡
UFUNCTION(BlueprintPure, Category="MyAbilitySystemLibrary|GameplayEffects")
static bool IsBlockedHit(const FGameplayEffectContextHandle& EffectContextHandle);//获取当前GE是否触发暴击
UFUNCTION(BlueprintPure, Category="MyAbilitySystemLibrary|GameplayEffects")
static bool IsCriticalHit(const FGameplayEffectContextHandle& EffectContextHandle);
然后在实现中,我们需要从Handle中获取Context并转换为我们自定义的格式
我们需要用到static_cast去强制转换类型。
const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get())
然后通过内置的函数去获取是否暴击或者格挡
bool UMyAbilitySystemBlueprintLibrary::IsBlockedHit(const FGameplayEffectContextHandle& EffectContextHandle)
{if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get())){return RPGEffectContext->IsBlockedHit();}return false;
}bool UMyAbilitySystemBlueprintLibrary::IsCriticalHit(const FGameplayEffectContextHandle& EffectContextHandle)
{if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get())){return RPGEffectContext->IsCriticalHit();}return false;
}
编译在UE的技能蓝图中就可以通过名称去获取对应的值,我们只需要传入对应的Handle,就可以获取到对应的Context的暴击和格挡是否触发。
接下来,我们还要在函数库实现设置的两个函数,这里有个问题,就是没办法传入常量(前面加const)会出现问题,我们将函数编写完成以后编译在UE里面查看
设置需要传入两个值,一个是修改的Context的Handle,另一个则是bool值,用于设置的值,我们无法设置成静态函数,也就是
UFUNCTION(BlueprintCallable, Category="MyAbilitySystemLibrary|GameplayEffects")
static void SetIsBlockHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit);UFUNCTION(BlueprintCallable, Category="MyAbilitySystemLibrary|GameplayEffects")
static void SetIsCriticalHit(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsCriticalHit);
在实现这里,还是老套路,强制转换为自定义类的实例,然后通过实例方法设置
void UMyAbilitySystemBlueprintLibrary::SetIsBlockHit(FGameplayEffectContextHandle& EffectContextHandle,bool bInIsBlockedHit)
{FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());RPGEffectContext->SetIsBlockedHit(bInIsBlockedHit);
}void UMyAbilitySystemBlueprintLibrary::SetIsCriticalHit(FGameplayEffectContextHandle& EffectContextHandle,bool bInIsCriticalHit)
{FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());RPGEffectContext->SetIsCriticalHit(bInIsCriticalHit);
}
如果我们不设置const,编译出来Handle会在右边,无法在蓝图中设置Handle。但是设置了const为常量,就无法修改数值
声明函数时,我们在前面添加UPARAM(ref)
,即可实现在蓝图节点的左侧
UFUNCTION(BlueprintCallable, Category="MyAbilitySystemLibrary|GameplayEffects")
static void SetIsBlockHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsBlockedHit);UFUNCTION(BlueprintCallable, Category="MyAbilitySystemLibrary|GameplayEffects")
static void SetIsCriticalHit(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsCriticalHit);
实现在AttributeSet获取
有了我们上面设置的函数库的函数,我们可以很方便的去设置对应的参数,首先我们去计算伤害的地方获取到Handle
//获取GE的上下文句柄FGameplayEffectContextHandle EffectContextHandle = Spec.GetContext();
然后在计算完成格挡后,将格挡的变量设置,需要传入句柄和布尔
//设置格挡UMyAbilitySystemBlueprintLibrary::SetIsBlockHit(EffectContextHandle, bBlocked);
同理,设置暴击
//设置暴击UMyAbilitySystemBlueprintLibrary::SetIsCriticalHit(EffectContextHandle, bCriticalHit);
设置完成后,我们需要在AttributSet类中,将显示UI伤害数字的函数,增加格挡和暴击的参数
//显示伤害数字static void ShowFloatingText(const FEffectProperties& Props, const float Damage, bool IsBlockedHit, bool IsCriticalHit);
在PostGameplayEffectExecute函数中,我们调用了SetEffectProperties,整理了结构体方便获取数据
所以我们在设置ShowFloatingText之前,获取到暴击和格挡
//获取格挡和暴击
const bool IsBlockedHit = UMyAbilitySystemBlueprintLibrary::IsBlockedHit(Props.EffectContextHandle);
const bool IsCriticalHit = UMyAbilitySystemBlueprintLibrary::IsCriticalHit(Props.EffectContextHandle);
然后传入设置即可。
//显示伤害数字
ShowFloatingText(Props, LocalIncomingDamage, IsBlockedHit, IsCriticalHit);
这一篇文章就更新到这里,接着,我debug查看一下是否生效。
这里我用测试数值设置格挡率百分百,现在在AttributeSet里面实现了获取值为true。
在下一篇,我们将更新在格挡和暴击时,UI上面将显示对应的效果。