在RPG游戏中,我们是不会直接修改生命值的属性,是因为在修改角色属性时,需要获取角色的属性并进行复杂的计算,所以,我们正常情况下使用元属性(Meta Attributes)作为计算的中间的媒。在服务器上先将属性计算到元属性上面,然后再通过元属性的数值去修改实际属性。
那元属性和普通的属性有什么区别?
普通的Gameplay Attribute:它是经常被复制的,在服务器上面修改,然后复制到每个客户端上面。
元属性:它不会被复制,只做为一个临时缓冲占位符,只在服务器上面计算,然后使用元属性修改实际需求修改的属性,这样就可以避免每个端需要复杂的计算。
比如,我们将实现一个名为IncomingDamage的元属性,然后在属性集里的PostGameplayEffectExecute获取角色属性(是否被格挡,减伤等等)来计算出对角色造成的最终伤害,然后将最终伤害去修改角色的血量值,而这个值再复制给客户端,将大大减少计算量。
所以,这些计算伤害的元属性将有传入的GE去计算,然后属性集去执行所需的计算,并播放对应的效果(比如显示不同颜色的伤害数字)并对目标应用buff(如果有)。
接下来,我们将从一个简单的版本开始实现,然后逐渐让功能符合游戏需求。
创建元属性
在测试使用了GE里面,我们是直接修改了血量值,我们需要创建一个元属性作为中间媒介。
首先,我们需要先在属性集(AttributeAet)里添加一个属性,用于作为临时占位符使用。原属性不需要赋值,所以不需要赋值函数,但是属性对应的宏还是需要的生成获取和设置的函数使用。
cpp
UPROPERTY(BlueprintReadOnly, Category="Meta Attributes")
FGameplayAttributeData IncomingDamage; //处理传入的伤害
ATTRIBUTE_ACCESSORS(UAttributeSetBase, IncomingDamage);
接下来,在PostGameplayEffectExecute函数里面,查看修改的是否为元属性
cpp
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
然后我们获取到元属性的值备用,并将属性集上的值设置为0,等待下一次设置。
cpp
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
接下来,判断值是否大于0,如果大于0则传入了伤害
cpp
if(LocalIncomingDamage > 0.f)
然后,我们先实现最基础的修改血量,作为基础模板,并且还判断角色是否存活,条件就是血量不为0则为存活,这样基础测试代码就实现了。
cpp
if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
{
const float LocalIncomingDamage = GetIncomingDamage();
SetIncomingDamage(0.f);
if(LocalIncomingDamage > 0.f)
{
const float NewHealth = GetHealth() - LocalIncomingDamage;
SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));
const bool bFatal = NewHealth <= 0.f; //血量小于等于0时,角色将会死亡
}
}
编译代码,打开UE,在GE里面修改的为IncomingDamage,并设置为25
就可以运行测试效果
使用Set by Caller来实现伤害数值的传递
制作到这里,朋友们会有疑问,我们实现GE里面的伤害都是通过固定的数值来造成的伤害,但是如果我们需要通过角色的属性计算呢,该如何实现?
这就需要将属性值修改的Set by Caller去实现
接下来我们将一步步实现这个。
要使用这种方式去设置,我们需要使用一个标签作为Key,在我们之前c++里添加一个标签
cpp
FGameplayTag Damage; //伤害 标签
然后初始化,添加到标签管理器,并缓存下来变量
cpp
GameplayTags.Damage = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("Damage"),
FString("伤害标签")
);
创建完成这个,我们需要在之前的火球术技能类里面,在生成GESpec时,使用标签设置。
首先获取到标签的单例
cpp
const FMyGameplayTags GameplayTags = FMyGameplayTags::Get(); //获取标签单例
然后使用函数库的方法设置,这里有两种方式,一种是FName,另一种是使用标签,对应在GE上获取的两种,由于Name类型容易写错,所以个人推荐使用标签。最后的值则是我们需要对目标造成的伤害,这里方便测试,我们直接写了固定数值,如果这个数值能够起作用,证明我们接下来可以在代码里面计算当前技能造成的伤害。
cpp
//UAbilitySystemBlueprintLibrary::AssignSetByCallerMagnitude() //使用DataName设置
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, 50.f);
然后编译代码,打开GE,将修改数值类型设置为Set by Caller,并在下面设置Tag为Damage,这样,GE将从标签获取对应的数值
在技能上通过技能等级设置伤害
我们实现了通过代码里面去设置技能造成的伤害,接下来,我们将实现增加配置项在技能上实现对伤害的设置,并且还使用到了曲线表格去编辑伤害。
首先我们在技能基类上面添加一个属性,虽然不是所有的属性都会用到,但是它占用的内存不大,所以我们直接写到基类,需要就用。
cpp
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
FScalableFloat Damage;
编译代码打开UE,可以查看到属性面板
所以,我们需要设置它的曲线表格来实现通过等级设置技能伤害,首先创建一个JSON文件,用来设置属性
然后导入到UE,生成曲线表格
设置为曲线表格
然后可以稍微修改一下表格,让曲线更陡一些
然后将属性缩放设置为1,选择我们创建的表格,然后设置好使用哪一行
有了表格以后,我们就需要实现如何通过属性获取到数值。如果你需要获取到整数类型的,我们可以通过Damage.AsInteger(GetAbilityLevel())
来获取从表格转换的整数数值。
这里我们使用Damage.GetValueAtLevel获取浮点数来设置,并且将数值打印到窗口
cpp
//创建一个GE的实例,并设置给投射物
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), SourceASC->MakeEffectContext());
const FMyGameplayTags GameplayTags = FMyGameplayTags::Get(); //获取标签单例
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel()); //根据等级获取技能伤害
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Red, FString::Printf(TEXT("火球术伤害:%f"), ScaledDamage));
//UAbilitySystemBlueprintLibrary::AssignSetByCallerMagnitude() //使用DataName设置
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Damage, ScaledDamage);
Projectile->DamageEffectHandle = SpecHandle;
编译并运行项目测试
在技能等级没有修改时,值为5,没问题
由于我们现在还没有实现设置技能等级,所以这里在代码里修改查看20级的数值,是否为我们设置的伤害值
cpp
const float ScaledDamage = Damage.GetValueAtLevel(GetAbilityLevel() + 19);
查看效果,没问题