49. UE5 RPG 使用Execution Calculations处理对目标造成的最终伤害

news2024/11/30 0:35:20

Execution Calculations是Unreal Engine中Gameplay Effects系统的一部分,用于在Gameplay Effect执行期间进行自定义的计算和逻辑操作。它允许开发者根据特定的游戏需求,灵活地处理和修改游戏中的属性(Attributes)。

  1. 功能强大且灵活:与ModifierMagnitudeCalculation类似,Execution Calculations可以捕获Attribute,并可选择性地为其创建Snapshot。不过,与MMC不同的是,Execution Calculations可以修改多个Attribute,基本上可以处理程序员想要做的任何事。
  2. 不可预测性:这种强大和灵活性的代价是它是不可预测的(Modifiers是可以预测的),且必须在C++中实现。这是因为Execution Calculations提供了很高的自由度,允许开发者根据特定的游戏逻辑进行自定义计算。
  3. 使用场景:Execution Calculations最普遍的应用场景是计算一个来自很多源(Source)和目标(Target)中Attribute伤害值的复杂公式。例如,在计算伤害时,可以考虑攻击者的攻击力、防御者的防御力、各种增益和减益效果等多个因素。
  4. 限制:需要注意的是,Execution Calculations只能由即刻(Instant)和周期性(Periodic)Gameplay Effect使用。功能强大也带来了劣势就是它不支持预测(Prediction)。属性获取时不会运行PreAttributeChange函数,所以,我们还需要在其内部实现一遍属性限制。只能够在服务器运行并且在Net Execution Policies设置为 Local Predicted,Server Initiated和Server Only时。

它和GameplayModMagnitudeCalculation很像,但是可以一次性修改多个属性。它不是只返回一个值,而是直接在函数内部对属性进行了修改。
由于Execution Calculations的复杂性,建议进行充分的测试和调试,以确保计算逻辑的正确性。

关于快照 Snapshoting,如果选择了它,快照的属性值将会在GE的Spec创建的时候设置,如果不选择,那它将会在应用GE时去获取属性值。注意,即使我们选择快照,目标的属性也是在应用GE时获取,没应用之前我们也不知道目标是谁。

创建类

接下来,我们将使用Execution Calculations来实现对技能伤害的计算,我们创建一个基于GameplayEffectExecutionCalculation的类
在这里插入图片描述
然后创建类,并专门设置一个文件夹存储同类型的类
在这里插入图片描述
在.h文件中,我们需要设置构造函数,和覆写父类的函数,在函数内进行逻辑处理

// 版权归暮志未晚所有。

#pragma once

#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "ExecCalc_Damage.generated.h"

/**
 * 
 */
UCLASS()
class AURA_API UExecCalc_Damage : public UGameplayEffectExecutionCalculation
{
	GENERATED_BODY()

public:
	UExecCalc_Damage();

	virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};

捕获属性

在C++中,我们首先实现通过快照获取到属性,它和我们之前制作的获取最大生命值和最大蓝量值的MMC里面的获取方式一致,在MMC里面,我们在构造函数内进行获取属性的设置,然后通过函数获取属性的值。
在这里插入图片描述
我们在C++中,首先创建一个结构体,由于不需要外部使用,所以不需要用F开头,方便区分

//这里结构体不加F是因为它是内部结构体,不需要外部获取,也不需要在蓝图中使用
struct SDamageStatics 
{
	SDamageStatics()
	{
		
	}
};

然后创建一个静态函数,这个函数返回一个静态实例,由于这个实例是静态的,因此它只会被创建一次,并且在随后的函数调用中重复使用。这种模式也称为单例模式(Singleton Pattern)。

static const SDamageStatics& DamageStatics()
{
	static SDamageStatics DStatics;
	return DStatics;
}

接下来,我们将在结构体内增加需要快照的属性,为了方便定义快照,gas里面也给我们定义相应的宏,我们不需要每次像在mmc里面那么复杂的去定义每个属性。
以下为gas代码定义的宏,一个用于申明属性,另一个则是实现了在构造函数中实现的赋值创建。

// -------------------------------------------------------------------------
//	Helper macros for declaring attribute captures 
// -------------------------------------------------------------------------

#define DECLARE_ATTRIBUTE_CAPTUREDEF(P) \
	FProperty* P##Property; \
	FGameplayEffectAttributeCaptureDefinition P##Def; \

#define DEFINE_ATTRIBUTE_CAPTUREDEF(S, P, T, B) \
{ \
	P##Property = FindFieldChecked<FProperty>(S::StaticClass(), GET_MEMBER_NAME_CHECKED(S, P)); \
	P##Def = FGameplayEffectAttributeCaptureDefinition(P##Property, EGameplayEffectAttributeCaptureSource::T, B); \
}

有了这两个宏,我们就创建快照代码就简洁了很多。我们使用宏定义属性,然后在构造是定义参数,即可完成对属性相关内容的创建,比之前简洁了很多。

struct SDamageStatics 
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	SDamageStatics()
	{
		//参数:1.属性集 2.属性名 3.目标还是自身 4.是否设置快照(true为创建时获取,false为应用时获取)
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Armor, Target, false);
	}
};

还有最后一步,就是需要将属性添加到捕获列表中,我们在构造函数中添加即可。

UExecCalc_Damage::UExecCalc_Damage()
{
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
}

以后,我们将需要的属性都按照这种方式实现即可。

Execute_Implementation

接下来就是主要的执行函数,也是我们需要覆写的内容,它有两个值:

  1. ExecutionParams:这是一个包含自定义执行参数的结构体,它提供了执行此游戏效果所需的所有信息。
  2. OutExecutionOutput:这是一个输出参数,用于存储执行结果或相关的输出信息。

在ExecutionParams里,我们可以获取到需要用于计算的对象,并从它身上获取相关内容。
下面相当于是一个模板的内容,以后制作相关的,我们可以直接复制使用

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
                                              FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	//获取ASC
	const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
	const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();

	//获取AvatarActor
	const AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
	const AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;

	//获取挂载此类的GE实例
	const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

	//设置评估参数
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

然后,我们可以对属性进行获取

	//获取护甲
	float Armor = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, Armor);
	Armor = FMath::Max(0.f, Armor);

接着就是最重要的一步,在返回接口,我们需要通过FGameplayModifierEvaluatedData 创建所需的计算,这里,我们对目标的护甲加上当前的护甲,相当于,我们攻击它,它的护甲增加了一倍。

	//输出计算结果
	const FGameplayModifierEvaluatedData EvaluatedData(DamageStatics().ArmorProperty, EGameplayModOp::Additive, Armor);
	OutExecutionOutput.AddOutputModifier(EvaluatedData);

在计算方式这里,我们可以选择多种计算方式,和在蓝图中设置一样。
在这里插入图片描述
接着编译打开UE,在Executions数组中增加一项,将我们创建的类设置上去。
在这里插入图片描述
在最终输出这里打个断点,查看每次是否Armor会增加一倍
在这里插入图片描述
第一次获取的值是
在这里插入图片描述
第二次是
在这里插入图片描述
刚好是一倍,证明逻辑正确。

使用Execution Calculations实现Set by Caller

我们之前实现伤害时通过Set by Caller通过标签传入值对角色造成的伤害,这里,我们将不使用这种方式,修改为使用Execution Calculations来实现当前的逻辑。
首先,我们将之前制作的Set by Caller的配置删除掉,不用了
在这里插入图片描述
然后通过在代码里配置去获取Set by Caller,这里为了区分,将伤害减少10,原来伤害时100,现在就应该是90

	//从Set by Caller 获取Damage的伤害值
	float Damage = Spec.GetSetByCallerMagnitude(FMyGameplayTags::Get().Damage);
	Damage -= 10;

最后设置时,我们需要设置给IncomingDamage,我们这里直接从属性集上面获取对应的属性

	//输出计算结果
	const FGameplayModifierEvaluatedData EvaluatedData(UAttributeSetBase::GetIncomingDamageAttribute(), EGameplayModOp::Additive, Damage);
	OutExecutionOutput.AddOutputModifier(EvaluatedData);

编译查看结果
在这里插入图片描述

计算格挡率影响伤害

接下来,我们使用格挡率来计算是否被格挡。首先要增加对格挡率的值的获取,和护甲一样,我们需要去获取,这里不在赘述。

struct SDamageStatics 
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
	
	SDamageStatics()
	{
		//参数:1.属性集 2.属性名 3.目标还是自身 4.是否设置快照(true为创建时获取,false为应用时获取)
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Armor, Target, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, BlockChance, Target, false);
	}
};
UExecCalc_Damage::UExecCalc_Damage()
{
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
	RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
}

然后我们获取到格挡率,并防止它会出现负值

//获取格挡率,如果触发格挡,伤害减少一半
float TargetBlockChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, TargetBlockChance);
TargetBlockChance = FMath::Max(0.f, TargetBlockChance);

然后,我们生成一个从1到一百的随机数,并和格挡率进行判断,随机数小于格挡率的值,则触发伤害减半

//处理格挡触发
if(FMath::RandRange(1, 100) < TargetBlockChance) Damage *= 0.5f;

对于格挡率的测试,我们完全可以创建一个测试用的GE去测试效果。

使用护甲和护甲穿透影响伤害

接下来,我们还需要通过应用角色护甲值(目标)和护甲穿透(源)来影响伤害
首先获取对应的值,我们在结构体内增加对应的宏

struct SDamageStatics 
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
	DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
	
	SDamageStatics()
	{
		//参数:1.属性集 2.属性名 3.目标还是自身 4.是否设置快照(true为创建时获取,false为应用时获取)
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Armor, Target, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, BlockChance, Target, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, ArmorPenetration, Source, false);
	}
};

然后构造函数添加到捕获列表

UExecCalc_Damage::UExecCalc_Damage()
{
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
	RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
	RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
}

接着在函数内,获取两个属性

//获取目标护甲值
float TargetArmor = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, TargetArmor);
TargetArmor = FMath::Max(0.f, TargetArmor);
//获取源护甲穿透
float SourceArmorPenetration = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef, EvaluationParameters, SourceArmorPenetration);
SourceArmorPenetration = FMath::Max(0.f, SourceArmorPenetration);

然后计算护甲穿透影响护甲值

	//护甲穿透将按照比例忽略目标的护甲值,护甲穿透的值可以根据某个方程计算出实际护甲穿透率(可以根据等级,等级越高,每一点护甲穿透值的比例越低)
	const float EffectiveArmor = TargetArmor * (100.f - SourceArmorPenetration * 0.25f) / 100.f;

再计算护甲减少伤害

	//忽略后的护甲值将以一定比例影响伤害
	Damage *= (100.f - EffectiveArmor * 0.333f) / 100.f;

这样完成了护甲减少伤害的功能。

使用曲线表格修改护甲穿透和护甲的影响

在上面,我们在使用护甲穿透和护甲时,又乘以了一个数值来调整它们的影响。这是为了在RPG游戏中,前期游戏数值低,后期数值高,导致值的影响前期太低或者后期太高,所以,我们将其设置为一个可以跟随等级变动的数值,来让前期低数值时获取一个不错的比例,并且在高等级时,也能不会导致数值溢出的问题。
我们创建一个曲线表格来实现数值的设置,类型选择Constant
在这里插入图片描述
进入曲线表格,修改名称,比如第一行去修改护甲穿透,然后点击加新的一列
在这里插入图片描述
添加数值时,如果想修改更多的key,可以三个点来修改
在这里插入图片描述
添加完数值
在这里插入图片描述
如果你想查看每个等级获取到的值到底是多少,可以切换到曲线查看,会发现在constant模式下,它在补间中间获取的都是自己设定的值,而且还可以检查是否出现错误。
在这里插入图片描述
接着填完对应的数值
在这里插入图片描述
接着打开代码编辑器,在之前设置的配置属性类中设置CharacterClassInfo中添加一个设置曲线表格的配置项

	UPROPERTY(EditDefaultsOnly, Category="Common Class Defaults|Damgage")
	TObjectPtr<UCurveTable> DamageCalculationCoefficients;

为了方便我们去获取当前在GameMode上面配置的数据,我们在静态函数库里面创建一个函数来获取数据。

	//获取角色配置数据
	UFUNCTION(BlueprintCallable, Category="MyAbilitySystemLibrary|CharacterClassDefaults")
	static UCharacterClassInfo* GetCharacterClassInfo(const UObject* WorldContextObject);

实现我们之前在其它函数实现了很多次了,就是获取到GameMode实例,然后从实例上获取到配置

UCharacterClassInfo* UMyAbilitySystemBlueprintLibrary::GetCharacterClassInfo(const UObject* WorldContextObject)
{
	//获取到当前关卡的GameMode实例
	const AMyGameModeBase* GameMode = Cast<AMyGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
	if(GameMode == nullptr) return nullptr;

	//返回关卡的角色的配置
	return  GameMode->CharacterClassInfo;
}

接着我们回到ExecCalc_Damage处理函数中,通过这个函数获取到角色数据使用

//获取到角色配置数据
const UCharacterClassInfo* CharacterClassInfo = UMyAbilitySystemBlueprintLibrary::GetCharacterClassInfo(SourceAvatar);

为了获取等级,我们需要将Avatar转换为战斗接口,然后通过接口内的函数获取角色等级

	//获取AvatarActor
	AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
	AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;

	//获取到战斗接口
	ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
	ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);

数据有了,等级有了,就可以去获取对应的系数
首先处理护甲穿透的系数,我们先通过数据配置项获取到哪一行的数据,然后通过数据以及角色的等级去获取系数
然后将值替换掉

//获取到数据表内的护甲穿透系数
const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("ArmorPenetration"), FString());
const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
//护甲穿透将按照比例忽略目标的护甲值,护甲穿透的值可以根据某个方程计算出实际护甲穿透率(可以根据等级,等级越高,每一点护甲穿透值的比例越低)
const float EffectiveArmor = TargetArmor * (100.f - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;

接着是护甲的系数,同理,注意,护甲穿透是获取源的,而护甲时获取的目标的不要弄错

//获取到数据表内的护甲系数
const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("EffectiveArmor"), FString());
const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
//忽略后的护甲值将以一定比例影响伤害
Damage *= (100.f - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;

这样就实现了代码逻辑,编译打开UE,在UE里,我们首先将曲线表格设置到数据资产上
在这里插入图片描述
接下来就是测试,我这里debug好像有点问题,获取不到ExecutionParams内容,所以采用打印的方式,来查看系数
在这里插入图片描述

实现暴击

我们还将在这里面实现暴击修改最终伤害的逻辑,同理,这里就简单讲解一下,
先增加一条影响暴击抵抗的系数曲线
在这里插入图片描述

在代码中增加获取到源的暴击概率和暴击伤害,以及目标的暴击抵抗

struct SDamageStatics 
{
	...
	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
	
	SDamageStatics()
	{
		//参数:1.属性集 2.属性名 3.目标还是自身 4.是否设置快照(true为创建时获取,false为应用时获取)
		...
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, CriticalHitChance, Source, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, CriticalHitDamage, Source, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, CriticalHitResistance, Target, false);
	}
};

...

UExecCalc_Damage::UExecCalc_Damage()
{
	...
	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitChanceDef);
	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitDamageDef);
	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitResistanceDef);
}

然后还是老套路,增加暴击伤害的逻辑处理,获取数据,处理数据

//--------------------暴击伤害--------------------
//暴击率
float SourceCriticalHitChance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef, EvaluationParameters, SourceCriticalHitChance);
SourceCriticalHitChance = FMath::Max(0.f, SourceCriticalHitChance);
//暴击伤害
float SourceCriticalHitDamage = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef, EvaluationParameters, SourceCriticalHitDamage);
SourceCriticalHitDamage = FMath::Max(0.f, SourceCriticalHitDamage);
//暴击抵抗
float TargetCriticalHitResistance = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef, EvaluationParameters, TargetCriticalHitResistance);
TargetCriticalHitResistance = FMath::Max(0.f, TargetCriticalHitResistance);
//获取到数据表内的暴击抵抗系数
const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("CriticalHitResistance"), FString());
const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetCombatInterface->GetPlayerLevel());
//计算当前是否暴击
const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance * CriticalHitResistanceCoefficient;
const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
//触发暴击 伤害乘以暴击伤害率
if(bCriticalHit) Damage = Damage * 2.f + SourceCriticalHitDamage;

我刚才测试了一下,如果不暴击的话伤害是5,暴击伤害是25 。这个值是两倍的伤害+暴击伤害 5*2 + 14.67约等于25 没问题。
在这里插入图片描述

源码

// 版权归暮志未晚所有。


#include "AbilitySystem/ExecCalc/ExecCalc_Damage.h"

#include "AbilitySystemComponent.h"
#include "MyGameplayTags.h"
#include "AbilitySystem/AttributeSetBase.h"
#include "AbilitySystem/MyAbilitySystemBlueprintLibrary.h"
#include "AbilitySystem/Data/CharacterClassInfo.h"
#include "Interaction/CombatInterface.h"

//这里结构体不加F是因为它是内部结构体,不需要外部获取,也不需要在蓝图中使用
struct SDamageStatics 
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	DECLARE_ATTRIBUTE_CAPTUREDEF(ArmorPenetration);
	DECLARE_ATTRIBUTE_CAPTUREDEF(BlockChance);
	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitChance);
	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitDamage);
	DECLARE_ATTRIBUTE_CAPTUREDEF(CriticalHitResistance);
	
	SDamageStatics()
	{
		//参数:1.属性集 2.属性名 3.目标还是自身 4.是否设置快照(true为创建时获取,false为应用时获取)
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Armor, Target, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, BlockChance, Target, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, ArmorPenetration, Source, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, CriticalHitChance, Source, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, CriticalHitDamage, Source, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, CriticalHitResistance, Target, false);
	}
};

static const SDamageStatics& DamageStatics()
{
	static SDamageStatics DStatics;
	return DStatics;
}

UExecCalc_Damage::UExecCalc_Damage()
{
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
	RelevantAttributesToCapture.Add(DamageStatics().BlockChanceDef);
	RelevantAttributesToCapture.Add(DamageStatics().ArmorPenetrationDef);
	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitChanceDef);
	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitDamageDef);
	RelevantAttributesToCapture.Add(DamageStatics().CriticalHitResistanceDef);
}

void UExecCalc_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams,
                                              FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	//获取ASC
	const UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
	const UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();

	//获取AvatarActor
	AActor* SourceAvatar = SourceASC ? SourceASC->GetAvatarActor() : nullptr;
	AActor* TargetAvatar = TargetASC ? TargetASC->GetAvatarActor() : nullptr;

	//获取到战斗接口
	ICombatInterface* SourceCombatInterface = Cast<ICombatInterface>(SourceAvatar);
	ICombatInterface* TargetCombatInterface = Cast<ICombatInterface>(TargetAvatar);

	//获取挂载此类的GE实例
	const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

	//设置评估参数
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

	//获取到角色配置数据
	const UCharacterClassInfo* CharacterClassInfo = UMyAbilitySystemBlueprintLibrary::GetCharacterClassInfo(SourceAvatar);

	//从Set by Caller 获取Damage的伤害值
	float Damage = Spec.GetSetByCallerMagnitude(FMyGameplayTags::Get().Damage);

	//--------------------处理格挡路--------------------
	//获取格挡率,如果触发格挡,伤害减少一半
	float TargetBlockChance = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockChanceDef, EvaluationParameters, TargetBlockChance);
	TargetBlockChance = FMath::Max(0.f, TargetBlockChance);
	//根据格挡概率判断当前是否触发
	const bool bBlocked = FMath::RandRange(1, 100) < TargetBlockChance;
	if(bBlocked) Damage *= 0.5f;

	//--------------------处理目标护甲和源的护甲穿透影响伤害--------------------
	//获取目标护甲值
	float TargetArmor = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, TargetArmor);
	TargetArmor = FMath::Max(0.f, TargetArmor);
	//获取源护甲穿透
	float SourceArmorPenetration = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorPenetrationDef, EvaluationParameters, SourceArmorPenetration);
	SourceArmorPenetration = FMath::Max(0.f, SourceArmorPenetration);
	//获取到数据表内的护甲穿透系数
	const FRealCurve* ArmorPenetrationCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("ArmorPenetration"), FString());
	const float ArmorPenetrationCoefficient = ArmorPenetrationCurve->Eval(SourceCombatInterface->GetPlayerLevel());
	//护甲穿透将按照比例忽略目标的护甲值,护甲穿透的值可以根据某个方程计算出实际护甲穿透率(可以根据等级,等级越高,每一点护甲穿透值的比例越低)
	const float EffectiveArmor = TargetArmor * (100.f - SourceArmorPenetration * ArmorPenetrationCoefficient) / 100.f;
	//获取到数据表内的护甲系数
	const FRealCurve* EffectiveArmorCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("EffectiveArmor"), FString());
	const float EffectiveArmorCoefficient = EffectiveArmorCurve->Eval(TargetCombatInterface->GetPlayerLevel());
	//忽略后的护甲值将以一定比例影响伤害
	Damage *= (100.f - EffectiveArmor * EffectiveArmorCoefficient) / 100.f;

	//--------------------暴击伤害--------------------
	//暴击率
	float SourceCriticalHitChance = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitChanceDef, EvaluationParameters, SourceCriticalHitChance);
	SourceCriticalHitChance = FMath::Max(0.f, SourceCriticalHitChance);
	//暴击伤害
	float SourceCriticalHitDamage = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitDamageDef, EvaluationParameters, SourceCriticalHitDamage);
	SourceCriticalHitDamage = FMath::Max(0.f, SourceCriticalHitDamage);
	//暴击抵抗
	float TargetCriticalHitResistance = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CriticalHitResistanceDef, EvaluationParameters, TargetCriticalHitResistance);
	TargetCriticalHitResistance = FMath::Max(0.f, TargetCriticalHitResistance);
	//获取到数据表内的暴击抵抗系数
	const FRealCurve* CriticalHitResistanceCurve = CharacterClassInfo->DamageCalculationCoefficients->FindCurve(FName("CriticalHitResistance"), FString());
	const float CriticalHitResistanceCoefficient = CriticalHitResistanceCurve->Eval(TargetCombatInterface->GetPlayerLevel());
	//计算当前是否暴击
	const float EffectiveCriticalHitChance = SourceCriticalHitChance - TargetCriticalHitResistance * CriticalHitResistanceCoefficient;
	const bool bCriticalHit = FMath::RandRange(1, 100) < EffectiveCriticalHitChance;
	//触发暴击 伤害乘以暴击伤害率
	if(bCriticalHit) Damage = Damage * 2.f + SourceCriticalHitDamage;

	//输出计算结果
	const FGameplayModifierEvaluatedData EvaluatedData(UAttributeSetBase::GetIncomingDamageAttribute(), EGameplayModOp::Additive, Damage);
	OutExecutionOutput.AddOutputModifier(EvaluatedData);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1667937.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI 重塑产品设计

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

系统设计中的泛化调用

背景 目前在学习一些中间件&#xff0c;里面看到了一个词是叫泛化调用&#xff0c; 其实这个场景在JAVA中比较常见。我们常用的有反射&#xff0c;反射就是我知道类名称、类方法和参数&#xff0c;调用一个Object的类&#xff0c;但是在HTTP或者RPC远程调用过程中&#xff0c;…

【C++】stack和queue 适配器

&#x1f525;个人主页&#xff1a;北辰水墨 &#x1f525;专栏&#xff1a;C学习仓 本节内容我们来讲解栈和队列的模拟实现&#xff0c;文末会赋上模拟实现的代码 一、stack的使用和模拟实现 stack适配器的介绍&#xff1a; 1. stack是一种容器适配器&#xff0c;专门用在具…

Redis的数据淘汰策略——Java全栈知识(19)

Redis的数据淘汰策略 什么是数据淘汰策略 数据过期策略是 redis 中设置了 TTL 的数据过期的时候 Redis 的处理策略。数据淘汰策略是 Redis 内存不够的时候&#xff0c; 数据的淘汰策略&#xff1a;当 Redis 中的内存不够用时&#xff0c;此时在向 Redis 中添加新的 key, 那么…

物联网设计竞赛_2_Ubuntu联网配置

采用nat配置 随便定义一个VMnet虚拟网络接口&#xff0c;定义成nat模式 如果主机用的校园网&#xff0c;那么虚拟机发送消息将通过nat转换&#xff0c;转换成用户校园网ip进行发送&#xff0c;发送到校园网路由器再经过nat转换成公网ip访问互联网 点击NAT设置和DHCP设置记录好…

3kCTF2021 echo klibrary

文章目录 前言echoklibrary 前言 今天状态不好&#xff0c;很多事情都不想干&#xff0c;就做一做简单的题目 echo 内核版本&#xff1a;v5.9.10smap/smep/kaslr 开启modprobe_path 可写 题目给了源码&#xff0c;非常简单就是无限次的任意地址读写&#xff1a; #include …

js逆向-某投资平台参数分析

声明 本文仅供学习参考&#xff0c;如有侵权可私信本人删除&#xff0c;请勿用于其他途径&#xff0c;违者后果自负&#xff01; 如果觉得文章对你有所帮助&#xff0c;可以给博主点击关注和收藏哦&#xff01; 分析 aHR0cDovLzIyMS4yMTQuOTQuNTE6ODA4MS9pY2l0eS9pcHJvL2hhb…

如何在适用于 Linux 的 Visual Studio Code 中使用 .NET 8 上的 FastReport Avalonia

我们将继续撰写有关在各种操作系统上的 Visual Studio Code 中使用 FastReport Avalonia 的系列文章。在本文中&#xff0c;我们将详细分析如何使用 Visual Studio Code IDE 在 Linux 操作系统上运行 FastReport Avalonia。 Avalonia UI 是一个积极用于开发跨平台用户界面的 .…

Keysight 是德 N1077B 光/电时钟恢复设备,收藏保存

Keysight N1077B是一款光/电时钟恢复设备&#xff0c;支持115 MBd至24 GBd的数据速率范围&#xff0c;适用于多模和单模光信号以及电信号。该设备能够处理PAM4和NRZ两种类型的数据信号&#xff0c;并提供符合标准的时钟恢复功能。 N1077B具备可调峰值和环路带宽&#xff08;高…

第一课,idle的使用

一&#xff0c;什么是python&#xff1f; 是咱们用来和计算机“交流”、“发号施令”的编程语言。但是&#xff0c;计算机是看不懂python的&#xff0c;我们还需要一个翻译官&#xff0c;把python翻译成0和1组成的二进制&#xff0c;才能让计算机明白&#xff01; 0000001111…

四、VGA项目:联合精简帧+双fifo+sobel算法 实现VGA显示

前言&#xff1a;该项目实际上是在很多基础的小练习上合成起来的&#xff0c;例如涉及到uart&#xff08;rs232&#xff09;的数据传输、双fifo流水线操作、VGA图像显示&#xff0c;本次内容在此基础上又增添了sobel算法&#xff0c;能实现图像的边沿监测并VGA显示。 文章目录…

部署tomcat部署LNAMT

这里写目录标题 部署tomcatjava环境安装 部署LNAMT更改tomcat端口号 tomcat就是中间件之一&#xff0c;tomcat本身是一个容器&#xff0c;专门用来运行java程序&#xff0c;java语言开发的网页.jsp就应该运行于tomcat中。而tomcat本身的运行也依赖于jdk环境。 部署tomcat java…

Mask2former代码详解

1.整体流程 Mask2former流程如图所示&#xff0c;对于输入图片&#xff0c;首先经过Resnet等骨干网络获得多层级特征&#xff0c;对于获得的多层级特征&#xff0c;一个方向经过pixel decoder(基于DetrTransformerEncoderLayer)得到per-pixel embedding,另外一个方向经过transf…

【实战】算法思路总结

面试过程中&#xff0c;总是被拷打&#xff0c;信心都要没了。但是也慢慢摸索出一些思路&#xff0c;希望对大家有帮助。 &#xff08;需要多用一下ACM模式&#xff0c;力扣模式提供好了模板&#xff0c;自己在IDEA里面写的话&#xff0c;还是会有些陌生&#xff09; 0、基本…

Kafka的安装及接入SpringBoot

环境&#xff1a;windows、jdk1.8、springboot2 Apache KafkaApache Kafka: A Distributed Streaming Platform.https://kafka.apache.org/ 1.概述 Kafka 是一种高性能、分布式的消息队列系统&#xff0c;最初由 LinkedIn 公司开发&#xff0c;并于2011年成为 Apache 顶级项目…

【C/C++】内存分布

本文第一部分主要介绍了程序内存区域的划分以及数据的存储。第二部分有一段代码和一些题目&#xff0c;全面直观得分析了程序中的数组在内存中的存储。 因为不同的数据有不同的存储需求&#xff0c;各区域满足不同的需求&#xff0c;所以程序内存会有区域的划分。 根据需求的不…

第02章 计算机网络概述

2.1 本章目标 了解计算机网络的定义了解计算机网络的功能了解计算机网络的分类了解计算机网络的组成 2.2 计算机网络的定义 2.3 计算机网络的功能 2.4 计算机网络的分类 物理拓扑结构分类&#xff1a;总线型、环型、星型 2.5 计算机网络的组成 网络适配器(NIC)接口规格分类&a…

AI大模型探索之路-训练篇21:Llama2微调实战-LoRA技术微调步骤详解

系列篇章&#x1f4a5; AI大模型探索之路-训练篇1&#xff1a;大语言模型微调基础认知 AI大模型探索之路-训练篇2&#xff1a;大语言模型预训练基础认知 AI大模型探索之路-训练篇3&#xff1a;大语言模型全景解读 AI大模型探索之路-训练篇4&#xff1a;大语言模型训练数据集概…

DRF渲染之异常处理

异常处理 【1 】引言 Django REST Framework 这个就是我们常常说的DRF APIView的dispatch方法&#xff1a; 当请求到达视图时&#xff0c;DRF 的 APIView 类会调用 dispatch 方法来处理请求。在 dispatch 方法中&#xff0c;有一个关键的步骤是处理异常。如果在视图类的方法…

BGP综合大实验

实验要求 1.AS1中存在两个环回&#xff0c;一个地址是192.168.1.0/24&#xff0c;改地址不能在任何协议中宣告&#xff1b;AS3中存在两个环回&#xff0c;一个地址为192.168.2.0/24&#xff0c;该地址不能在任何协议中宣告&#xff0c;最终要求这两个环回可以ping通&#xff1b…