我们期望能够通过FGameplayEffectContext将此次技能造成的伤害是否触发格挡和暴击的参数传递到AttributeSet中,所以需要实现自定义一个FGameplayEffectContext类,来增加对应的配置。
创建自定义类文件
首先在Public目录上右键,选择添加一个文件
创建一个.h文件
然后在Private目录添加一个cpp文件
然后,我们在蓝图中定义结构体,并继承FGameplayEffectContext
#pragma once //预处理指令 确保这个头文件只被包含(include)一次,防止重复定义。#include "GameplayEffectTypes.h"
#include "RPGAbilityTypes.generated.h"USTRUCT(BlueprintType) //在蓝图中可作为类型使用
struct FRPGGameplayEffectContext : public FGameplayEffectContext
{GENERATED_BODY() //宏 自动生成构造函数、析构函数、拷贝构造函数等public:protected:};
我们要增加两个参数,用于设置或者获取当前技能是否暴击或者格挡
protected:UPROPERTY()bool bIsBlockedHit = false; //格挡UPROPERTY()bool BIsCriticalHit = false; //暴击
然后增加他的获取和设置函数
public:bool IsBlockedHit() const { return bIsBlockedHit; }bool IsCriticalHit() const { return BIsCriticalHit; }void SetIsBlockedHit(const bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }void SetIsCriticalHit(const bool bInIsCriticalHit) { BIsCriticalHit = bInIsCriticalHit; }
要实现子类,我们有一些需要覆写父类的一些函数,这些函数主要用于序列化,序列化的目的是为了将数据转换为二进制数据用于网络通信,因为最终我们需要将数据提交到服务器端,客户端向服务器端提交数据无法直接传递,所以需要序列化为二进制数据,然后将二进制数据提交给服务器端进行反序列化后使用。
首先覆写用于用于返回用于序列化的实际结构体函数。序列化是将对象的状态转换为可以存储或传输的形式的过程,反序列化则是相反的过程。在游戏开发和网络通信中,序列化和反序列化是非常重要的。这个函数在父类里面注释必须要在子类里面覆写。
/** 返回用于序列化的实际结构体 */
virtual UScriptStruct* GetScriptStruct() const override
{return FGameplayEffectContext::StaticStruct();
}
接下来,我们还需要覆写用于序列化配置的函数,将对应的参数保存,这样,在服务器端,可以完整的复原这个实例。
FArchive& Ar 是Unreal Engine中用于序列化和反序列化对象的类,它支持以二进制的形式进行加载保存和垃圾回收。
class UPackageMap* Map 用于查找或记录对象间的引用关系
bool& bOutSuccess 输出是否序列化成功
/** 用于序列化类的参数 */
virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;
接下来,我们将覆写这个函数的实现,因为内部很多实现需要我们手动去实现。
二进制运算
首先我们看一下源码里的实现,它教给我们了如何去实现序列化,里面有一些二进制的运算
bool FGameplayEffectContext::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{uint8 RepBits = 0;if (Ar.IsSaving()) //判断当前是否在保存数据{//保存数据时,如果有对应的配置项数据,那么将对应位的值设置为1if (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;}}//序列化RepBitsAr.SerializeBits(&RepBits, 7);//如果对应位的值为1,那么将数据存入Arif (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)){if (Ar.IsLoading()){if (!HitResult.IsValid()){HitResult = TSharedPtr<FHitResult>(new FHitResult());}}HitResult->NetSerialize(Ar, Map, bOutSuccess);}if (RepBits & (1 << 6)){Ar << WorldOrigin;bHasWorldOrigin = true;}else{bHasWorldOrigin = false;}//如果是加载数据(即反序列化)时,需要调用对ASC进行初始化if (Ar.IsLoading()){AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent} bOutSuccess = true;return true;
}
刚开始看的时候确实有点懵,比如这个RepBits |= 1 << 0;
RepBits & (1 << 0)
还有Ar << Instigator;
,接下来,我们就分析一下这些内容是什么。
RepBits |= 1 << 0;
RepBits & (1 << 0)
属于二进制运算,首先我们先将它们分开理解。
|=
在正常运算中见过很多,*= /=这种,它的意思也一样
RepBits |= 1 << 0;
可以转换成 RepBits = RepBits | 1 << 0;
而 |
和&
是二进制的运算符
unit8 相当于8位二进制无符号8位整数类型 0000 0000
unit32 相当于32位二进制无符号8位整数类型 0000 0000 0000 0000 0000 0000
为了方便分析,我们后面使用八位的分析
1 << 0
中间使用了左移操作符,相当于1向左位移0位 结果为 0000 0001
如果是1 << 3
相当于1向左位移3位 结果为0000 1000
那么再说|
和&
二进制的运算符,它们都是位的运算符,对应的位置的运算
|
是两个二进制,如果同一个位置有一个值为1,那么返回的值就是1 举例:
1000 1000 | 1000 0001
的结果为1000 1001
而&
则是同一个位置,两个值都为1,返回的结果对应的位置的值才为1,举例:
1000 1000 & 1000 0001
的结果为1000 0000
所以,现在再看上面的代码,RepBits |= 1 << 0;
这些意思就是如果条件成为,将把参数对应位的值设置为1,而RepBits & (1 << 0)
则是判断条件,就是为了判断对应位置的值是否为1,如果返回的结果不是0000 0000
,条件则会成立。
先对而言,Ar << Instigator;
就简单了很多,它是一个简写,它用于序列化或反序列化对象。这里的 <<
不是一个标准的C++流插入操作符,而是在Unreal Engine的序列化系统中有特殊含义。
当执行 Ar << Instigator; 时,FArchive& Ar
类会调用Instigator对象的序列化方法。Instigator的当前状态会被写入到Ar所代表的存储介质中,根据右侧的类型<<
也会切换对应类型的方法去处理,并且它还可以通过判断Ar.IsSaving()
去序列化或者反序列化。
实现自定义的NetSerialize
首先,在函数中,我们将RepBits
设置成32位整型,因为之前的8位已经不足以存下我们的内容。
bool FRPGGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{uint32 RepBits = 0;
然后,在if (Ar.IsSaving())
判断后,在底部加上我们的新增的属性
if (Ar.IsSaving()){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;}//自定义内容,增加暴击和格挡触发存储if(bIsBlockedHit){RepBits |= 1 << 7;}if(BIsCriticalHit){RepBits |= 1 << 8;}}
接着在序列长度这里,由于增加了两项,所以将7修改为9
//使用了多少长度,就将长度设置为多少
Ar.SerializeBits(&RepBits, 9);
然后接着将源码中序列或反序列的逻辑代码复制过来
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)){if (Ar.IsLoading()){if (!HitResult.IsValid()){HitResult = TSharedPtr<FHitResult>(new FHitResult());}}HitResult->NetSerialize(Ar, Map, bOutSuccess);}if (RepBits & (1 << 6)){Ar << WorldOrigin;bHasWorldOrigin = true;}else{bHasWorldOrigin = false;}
然后在后面添加我们新增的两项
//新增对暴击格挡的序列化或反序列化处理if (RepBits & (1 << 7)){Ar << bIsBlockedHit;}if (RepBits & (1 << 8)){Ar << BIsCriticalHit;}
将加载时初始化ASC的逻辑复制过来
if (Ar.IsLoading()){AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent}
最后,将Success设置为true,完成设置
bOutSuccess = true;return true;
以下为完整代码,我们实现了对应的网络序列化工作
RPGAbilityTypes.h
#pragma once //预处理指令 确保这个头文件只被包含(include)一次,防止重复定义。#include "GameplayEffectTypes.h"
#include "RPGAbilityTypes.generated.h"USTRUCT(BlueprintType) //在蓝图中可作为类型使用
struct FRPGGameplayEffectContext : public FGameplayEffectContext
{GENERATED_BODY() //宏 自动生成构造函数、析构函数、拷贝构造函数等public:bool IsBlockedHit() const { return bIsBlockedHit; }bool IsCriticalHit() const { return BIsCriticalHit; }void SetIsBlockedHit(const bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; }void SetIsCriticalHit(const bool bInIsCriticalHit) { BIsCriticalHit = bInIsCriticalHit; }/** 返回用于序列化的实际结构体 */virtual UScriptStruct* GetScriptStruct() const override{return FGameplayEffectContext::StaticStruct();}/** 用于序列化类的参数 */virtual bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess) override;
protected:UPROPERTY()bool bIsBlockedHit = false; //格挡UPROPERTY()bool BIsCriticalHit = false; //暴击
};
RPGAbilityTypes.cpp
#include "RPGAbilityTypes.h"bool FRPGGameplayEffectContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{uint32 RepBits = 0;if (Ar.IsSaving()){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;}//自定义内容,增加暴击和格挡触发存储if(bIsBlockedHit){RepBits |= 1 << 7;}if(BIsCriticalHit){RepBits |= 1 << 8;}}//使用了多少长度,就将长度设置为多少Ar.SerializeBits(&RepBits, 9);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)){if (Ar.IsLoading()){if (!HitResult.IsValid()){HitResult = TSharedPtr<FHitResult>(new FHitResult());}}HitResult->NetSerialize(Ar, Map, bOutSuccess);}if (RepBits & (1 << 6)){Ar << WorldOrigin;bHasWorldOrigin = true;}else{bHasWorldOrigin = false;}//新增对暴击格挡的序列化或反序列化处理if (RepBits & (1 << 7)){Ar << bIsBlockedHit;}if (RepBits & (1 << 8)){Ar << BIsCriticalHit;}if (Ar.IsLoading()){AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent} bOutSuccess = true;return true;
}