Published on

GameplayAbilitySystem入门与实战(七):GameplayEffect(二)

前言

上一篇讲述了GE的一些基本概念, 非常概念化没有实战部分, 很容易混淆和忘记, 本片我们结合实际项目再配合GA的使用来模拟几个技能效果

伤害计算

我们在前面测试GA的时候已经简单测试了伤害,不过那个伤害就固定的一个值, 下面我们用自定义的类GEEC来计算这个伤害

USRGEEC_Damage : public UGameplayEffectExecutionCalculation

image-20201218172355966

给上我们的GEEC

GEEC中只需要重写方法

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

然后我们需要捕获我们角色的一些数据, 比如我们捕获我们发起者的攻击以及目标的护甲

为了方便使用, 我们自定义一个结构体

struct  SRDamageStatics
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Attack);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Health);


	SRDamageStatics()
	{
		//攻击力在创建GE时捕获, 因为技能可能有弹道, 需要在发射的时候就捕获而不是击中的时候,
		//类似的其他有关发射者的属性都所需要此时捕获
		DEFINE_ATTRIBUTE_CAPTUREDEF(USRAttributeSetBase, Attack, Source, true);
		//目标的防御在应用的时候捕获
		DEFINE_ATTRIBUTE_CAPTUREDEF(USRAttributeSetBase, Armor, Target, false);
		DEFINE_ATTRIBUTE_CAPTUREDEF(USRAttributeSetBase, Health, Target, false);
	}

};
static const SRDamageStatics& DamageStatics()
{
	static SRDamageStatics DmgStatics;
	return DmgStatics;
}

然后设置捕获

USRGEEC_Damage::USRGEEC_Damage()
{
	RelevantAttributesToCapture.Add(DamageStatics().AttackDef);
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
	RelevantAttributesToCapture.Add(DamageStatics().HealthDef);
}

后面就是加加减减一下最终反馈到目标的血量(临时方案)

void USRGEEC_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	UAbilitySystemComponent* TargetAbilitySystemComponent = ExecutionParams.GetTargetAbilitySystemComponent();
	UAbilitySystemComponent* SourceAbilitySystemComponent = ExecutionParams.GetSourceAbilitySystemComponent();

	AActor* SourceActor = SourceAbilitySystemComponent ? SourceAbilitySystemComponent->GetAvatarActor() : nullptr;
	AActor* TargetActor = TargetAbilitySystemComponent ? TargetAbilitySystemComponent->GetAvatarActor() : nullptr;

	const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

	// 获取双方所有tag
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;


	float ArmorPower = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, ArmorPower);

	float AttackValue = 0.f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().AttackDef, EvaluationParameters, AttackValue);

	float DamageDone = -1 * (AttackValue - ArmorPower);
	if (DamageDone < 0.f)
	{
		OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().HealthProperty, EGameplayModOp::Additive, DamageDone));
	}
}

测试

image-20201218175903444

录制_2020_12_18_17_57_55_376

运行正常,每次伤害为8(10-2)

自动回复

我们属性里面有设置生命值和魔法值, 那么一般情况下这两种属性都有自动回复的机制,所以我们用GE来实现这个效果

image-20201221093148509

配置两个GE到我们的玩家角色的默认启动的GE

image-20201221093257062

设置GE如上图, 设定持续事件为Infinite,计算方式是基于其他属性, 然后设置Period=1, 即每1秒执行一次

image-20201221093407727

在下面的移除标签内加上死亡状态, 即死亡了以后会移除自动回复效果

接下来简单测试一下添加了死亡tag以后的情况

我们用一个测试GE什么都不做,就添加一个Death标签

录制_2020_12_21_09_50_34_192

搞定!

但是问题又来了, 这个生命回复到超过MaxHealth的时候就不对了, 我们必须在特定的地方对此做一个限制

属性Clamp

属性篇中我们提过一个PostGameplayEffectExecute()函数, 用此函数就是官方建议我们来做属性Clamp处理的

void USRAttributeSetBase::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data)
{
	UE_LOG(SRLog, Log, TEXT("PostGameplayEffectExecute  "));
	Super::PostGameplayEffectExecute(Data);
	
	FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext();
	UAbilitySystemComponent* SourceASC = Context.GetOriginalInstigatorAbilitySystemComponent();
	const FGameplayTagContainer& SourceTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags();
	FGameplayTagContainer SpecAssetTags;
	Data.EffectSpec.GetAllAssetTags(SpecAssetTags);

	AActor* TargetActor = nullptr;
	AController* TargetController = nullptr;
	ASRCharacterBase* TargetCharacter = nullptr;
	if (Data.Target.AbilityActorInfo.IsValid() && Data.Target.AbilityActorInfo->AvatarActor.IsValid())
	{
		TargetActor = Data.Target.AbilityActorInfo->AvatarActor.Get();
		TargetController = Data.Target.AbilityActorInfo->PlayerController.Get();
		TargetCharacter = `Cast<ASRCharacterBase>`(TargetActor);
	}

	AActor* SourceActor = nullptr;
	AController* SourceController = nullptr;
	ASRCharacterBase* SourceCharacter = nullptr;
	if (SourceASC && SourceASC->AbilityActorInfo.IsValid() && SourceASC->AbilityActorInfo->AvatarActor.IsValid())
	{
		SourceActor = SourceASC->AbilityActorInfo->AvatarActor.Get();
		SourceController = SourceASC->AbilityActorInfo->PlayerController.Get();
		if (SourceController == nullptr && SourceActor != nullptr)
		{
			if (APawn* Pawn = `Cast<APawn>`(SourceActor))
			{
				SourceController = Pawn->GetController();
			}
		}
		if (SourceController)
		{
			SourceCharacter = `Cast<ASRCharacterBase>`(SourceController->GetPawn());
		}
		else
		{
			SourceCharacter = `Cast<ASRCharacterBase>`(SourceActor);
		}
		if (Context.GetEffectCauser())
		{
			SourceActor = Context.GetEffectCauser();
		}


		if (Data.EvaluatedData.Attribute == GetHealthAttribute())
		{
			SetHealth(FMath::Clamp(GetHealth(), 0.0f, GetMaxHealth()));
		}
		else if (Data.EvaluatedData.Attribute ==  GetManaAttribute())
		{
			SetMana(FMath::Clamp(GetMana(), 0.0f, GetMaxMana()));
		}
	}
}

前面茫茫多的变量获取是为了后续做更多的内容, 如伤害处理等, 对于生命和魔法值的Clamp简单处理即可

注意

Source是发起者

Target是作用目标, 即被修改属性的角色,

两者可能会相同, 容易混淆概念

伤害随等级增加

前面我们用一个GEEC通过攻击-防御得到最后的生命值

我们继续扩展, 在下面的ModifierMagnitude内加入参数, 再给一个CurveTable参数,如下图

image-20201222135917433

此举意味着我们计算时的Attack值会额外增加对应等级的攻击力

CSV表格的编辑如下

image-20201222140009303

然后可以得到不同等级对应的攻击力加成

伤害计算中的属性关联

PostGameplayEffectExecute()函数会在引用GE以后调用, 同时也会触发属性改变的事件响应,

同时这里需要注意, 我们的Set***()函数能触发事件响应.

但是无法再次触发PostGameplayEffectExecute()

意味着,加入我们希望通过修改A属性后来修改B属性可以在PostGameplayEffectExecute()函数内处理

比如, Attack属性=AttackA+AttackB, 那么可以在PostGameplayEffectExecute()函数内实现

if (Data.EvaluatedData.Attribute == GetAttackAAttribute() || ...)
{
    SetAttack(GetAttackA()+GetAttackB());
}

类似这种形式也可以触发Attack的事件响应, 方便UI的同步显示效果

在案例里面, 我设置了5个主属性,如力量,敏捷

随着主属性的修改, 其他副属性如攻击力,护甲值,暴击率等会随之改变, 这些操作是可以放到PostGameplayEffectExecute()内处理的

可否把上述内容放到PreAttributeChange()内?

PreAttributeChange()只是对属性的预处理, 此函数内不适合去Get其他函数的值, 不能保证此时这个值是否正确

PreAttributeChange()比较适合对确定的某些属性最一些自适应处理, 如前面已经讲过的对当前生命值和最大生命值做处理函数AdjustAttributeForMaxChange()

伤害进阶处理

现在有这么一个需求,先不考虑闪避, 格挡等因素, 但需要计算暴击

玩家技能1:

  • 造成攻击力若干百分比的物理伤害
  • 造成攻击力若干百分比的火系伤害

计算方式需求:

  • 物理伤害需计算护甲, 物理抗性等
  • 法系伤害需计算相对应的法术抗性

首先需要在Set类内申明几个用于计算伤害的临时属性, 在GASDActionRPG项目中都采用了如此的方案

	//临时攻击数据, 通过技能初始化, 用于结算最终伤害
		UPROPERTY(BlueprintReadOnly, Category = "AttackTemp", ReplicatedUsing = OnRep_Attack_Physics)
		FGameplayAttributeData Attack_Physics;
	ATTRIBUTE_ACCESSORS(USRAttributeSetBase, Attack_Physics)
		UPROPERTY(BlueprintReadOnly, Category = "AttackTemp", ReplicatedUsing = OnRep_Attack_Fire)
		FGameplayAttributeData Attack_Fire;
	ATTRIBUTE_ACCESSORS(USRAttributeSetBase, Attack_Fire)
        
        //临时受伤数据
		UPROPERTY(BlueprintReadOnly, Category = "DamageTemp", ReplicatedUsing = OnRep_Damage_Physics)
		FGameplayAttributeData Damage_Physics;
	ATTRIBUTE_ACCESSORS(USRAttributeSetBase, Damage_Physics)
		UPROPERTY(BlueprintReadOnly, Category = "DamageTemp", ReplicatedUsing = OnRep_Damage_Fire)
		FGameplayAttributeData Damage_Fire;
	ATTRIBUTE_ACCESSORS(USRAttributeSetBase, Damage_Fire)

前面我们已经创建了一个GEEC, 然后我们进行扩展

已略过属性捕捉部分

void USRGEEC_Damage::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, OUT FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
    float PhysicsAttack = 0.f;
    float FireAttack = 0.f;
  ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().Attack_PhysicsDef, EvaluationParameters, PhysicsAttack);
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().Attack_FireDef, EvaluationParameters, FireAttack);
    
    float critPro = 0.f;
	float critMul = 1.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CritProDef, EvaluationParameters, critPro);
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().CritMulDef, EvaluationParameters, critMul);
    
    	float ArmorValue = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, ArmorValue);
    
    float PhysicsResistance = 0.f;
	float FireResistance = 0.f;
  ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().Resistance_PhysicsDef, EvaluationParameters, PhysicsResistance);	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().Resistance_FireDef, EvaluationParameters, FireResistance);
    
    float PhysicsDamage = CalcPhysicsMitigatedDamage(PhysicsAttack, ArmorValue, DamageReduceValue, PhysicsResistance);
	float FireDamage = CalcMagicMitigatedDamage(FireAttack, DamageReduceValue, FireResistance);
}

我们捕获了所需要的属性, 同时通过两个辅助函数计算物理和法术伤害

float USRGEEC_Damage::CalcPhysicsMitigatedDamage_Implementation(float UnmitigatedDamage, float TargetArmor, float critPro, float critMul, float ReducePercent /*= 0.0f*/, float PhysicsResistance /*= 0.0f*/)const
{
	float outDamage = 0.0f;
	bool bIsCrit = UKismetMathLibrary::RandomBoolWithWeight(critPro);
	float armorPerc = 1 - (TargetArmor / (TargetArmor + 500));
	float globlePerc = FMath::Clamp((1 - ReducePercent), 0.01f, 100.f);
	float resistancePerc = 1 - (PhysicsResistance / (PhysicsResistance + 50));
	float critValue = FMath::Clamp((bIsCrit ? critMul : 1.0f),1.0f,999.0f);
	
	outDamage = UnmitigatedDamage * armorPerc * globlePerc * resistancePerc * critValue;
	if (bDebug && outDamage>0)
	{
		UE_LOG(SRLog, Log, TEXT("USRGEEC_Damage::CalcPhysicsMitigatedDamage, SourceDamage = %f, ArmorReduce = %f, GlobleReduce = %f, ResistanceReduce = %f , CritValue = %f, \n OutDamage = %f"),
			UnmitigatedDamage, 1 - armorPerc, 1 - globlePerc, 1 - resistancePerc, critValue, outDamage);
	}
	return outDamage;
}

float USRGEEC_Damage::CalcMagicMitigatedDamage_Implementation(float UnmitigatedDamage, float critPro, float critMul, float ReducePercent /*= 0.0f*/, float Resistance /*= 0.0f*/)const
{
	float outDamage = 0.0f;
	bool bIsCrit = UKismetMathLibrary::RandomBoolWithWeight(critPro);
	float globlePerc = FMath::Clamp((1 - ReducePercent), 0.01f, 100.f);
	float resistancePerc = 1 - (Resistance / (Resistance + 50));
	float critValue = FMath::Clamp((bIsCrit ? critMul : 1.0f), 1.0f, 999.0f);
	
	outDamage = UnmitigatedDamage * globlePerc * resistancePerc * critValue;
	if (bDebug && outDamage > 0)
	{
		UE_LOG(SRLog, Log, TEXT("USRGEEC_Damage::CalcMagicMitigatedDamage, SourceDamage = %f, GlobleReduce = %f, ResistanceReduce = %f , CritValue = %f \n OutDamage = %f"), 
			UnmitigatedDamage, 1 - globlePerc, 1 - resistancePerc, critValue, outDamage);
	}
	return outDamage;
}

然后添加到修改

if (PhysicsDamage>0)
	{
		OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().Damage_PhysicsProperty, EGameplayModOp::Additive, PhysicsDamage));
	}
		
	if (FireDamage>0)
	{
		OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().Damage_FireProperty, EGameplayModOp::Additive, FireDamage));
	}

接下来会调用到目标单位的AttributeSet类中, 用一个函数HandlDamage()和宏DAMAGE_HANDLE来处理伤害和扣血

void USRAttributeSetBase::HandlDamage(FGameplayAttribute Attribute, ASRCharacterBase* TargetCharacter)
{
	float damage, newHp;
	DAMAGE_HANDLE(Attribute, TargetCharacter, Physics, damage, newHp);
	DAMAGE_HANDLE(Attribute, TargetCharacter, Fire, damage, newHp);
	DAMAGE_HANDLE(Attribute, TargetCharacter, Ice, damage, newHp);
	DAMAGE_HANDLE(Attribute, TargetCharacter, Poison, damage, newHp);
	DAMAGE_HANDLE(Attribute, TargetCharacter, Electricity, damage, newHp);
	DAMAGE_HANDLE(Attribute, TargetCharacter, Holy, damage, newHp);
	DAMAGE_HANDLE(Attribute, TargetCharacter, Arcane, damage, newHp);
}
#define DAMAGE_HANDLE(Attribute,C,Name,TempDamage,TempNewHp) \
{\
	if(Attribute == GetDamage_##Name##Attribute()) \
	{	\
		TempDamage = GetDamage_##Name(); \
		SetDamage_##Name(0);	\
		if(TempDamage>0)	\
		{	\
			ASRCharacterBase* character = `Cast<ASRCharacterBase>`(C);	\
			if (character&&character->IsAlive())\
			{\
				TempNewHp = GetHealth() - TempDamage;\
				SRLOGEX2(TEXT("USRAttributeSetBase::HandlDamage %s beDamage = %f"), *TargetCharacter->GetName(), TempDamage);\
			    SetHealth(TempNewHp);\
			}\
		}	\
	} \
}

然后是设置测试GE类, 我们同时用一张表格来配置技能对应等级的系数

image-20201225172537711

image-20201225172450545

测试样本: 初始10攻击力, 目标护甲10, 物理抗性10, 伤害减免10%

分别用1级技能和10级技能攻击测试物理部分

  • 1级

image-20201225173327395

  • 10级

image-20201225173355889

完成

伤害进阶处理:闪避和格挡

接下来我们再考虑加入闪避和格挡因素,这两者一般游戏针对的都是物理攻击,当然不排除也有闪避法术的设定,我们这里采用的是前者

还需要考虑一个设定, 格挡是格挡税前伤害还是税后, 即伤害减免是在格挡的前后问题,我们使用在税后(伤害减免后)在进行格挡计算, 这种情况下格挡的收益比较大

因为GEEC不适合做格挡和闪避的效果处理, 比如播放动画之类的, 那么此类消息我们打算传递给ASC去做处理或者转发

ASC中申明几个代理和函数

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnRecieveEventDodge, USRAbilitySystemComponent*, SourceASC, const FString&, ExtendInfo);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnReceiveEventBlock, USRAbilitySystemComponent*, SourceASC, const FHitResult&, HitResult, float, UnmitigatedDamage,float,BlockValue);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FiveParams(FOnReceiveEventDamage, USRAbilitySystemComponent*, SourceASC, const FHitResult&, HitResult, float, UnmitigatedDamage,float, MitigatedDamage, ESRDamageType,DamageType);

void USRAbilitySystemComponent::ReceiveDodge(USRAbilitySystemComponent* SourceASC, const FString& ExtendInfo)
{
	if (bDebugLog)
	{
		SRLOGEX2(TEXT("%s 闪避了 %s 的攻击"), *GetAvatarActor()->GetName(), *SourceASC->GetAvatarActor()->GetName());
	}
	
	OnDodge.Broadcast(SourceASC, ExtendInfo);
}




void USRAbilitySystemComponent::ReceiveDamage(USRAbilitySystemComponent* SourceASC, const FHitResult& HitResult, float UnmitigatedDamage, float MitigatedDamag, ESRDamageType DamageType)
{
	if (bDebugLog)
	{
		SRLOGEX5(TEXT("%s 收到 %s 的%s攻击, 税前伤害 = %f, 税后= %f"), *GetAvatarActor()->GetName(), *SourceASC->GetAvatarActor()->GetName(), 
			*UFlib_SRUtilities::DamageTypeToString(DamageType),UnmitigatedDamage, MitigatedDamag);
	}

	OnDamage.Broadcast(SourceASC, HitResult, UnmitigatedDamage, MitigatedDamag, DamageType);
}

void USRAbilitySystemComponent::ReceiveBlock(USRAbilitySystemComponent* SourceASC, const FHitResult& HitResult, float UnmitigatedDamage, float BlockValue)
{
	if (bDebugLog)
	{
		SRLOGEX4(TEXT("%s 格挡了 %s 攻击的 %f 点伤害(格挡前伤害= %f)"), *GetAvatarActor()->GetName(), *SourceASC->GetAvatarActor()->GetName(), BlockValue, UnmitigatedDamage);
	}
	OnBlock.Broadcast(SourceASC, HitResult, UnmitigatedDamage,BlockValue);
}

这里用到了一个辅助函数, 我们在一个函数库中做了枚举和字符串的转换(蓝图自带的)

FString UFlib_SRUtilities::DamageTypeToString(ESRDamageType type)
{
	return `SREnumToString<ESRDamageType>`("ESRDamageType", type, true);
}
ESRDamageType UFlib_SRUtilities::StringToDamageType(const FString& name)
{
	return `SRStringToEnum<ESRDamageType>`("ESRDamageType", name);
}

然后就是GEEC中额外加一些代码

//闪避和格挡只能作用到物理伤害
	if (PhysicsDamage>0)
	{
		//先计算闪避
		float dodge = 0.0f;
		ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DodgeProDef, EvaluationParameters, dodge);
		if (dodge > 0)
		{
			bool bIsMiss = UKismetMathLibrary::RandomBoolWithWeight(dodge);
			if (bIsMiss)
			{
				if (TargetAbilitySystemComponent&&SourceAbilitySystemComponent)
				{
					//如果闪避了, 那么不需要计算具体伤害
					TargetAbilitySystemComponent->ReceiveDodge(SourceAbilitySystemComponent);
					return;
				}
			}
		}

		//再计算格挡;格挡计算在税后,如果是税前那么需要在计算物理伤害之前进行;
		//有些设定需要正面才能格挡, 那么还需要计算方向
		float blockPro = 0.0f;
		float blockValue = 0.0f;
		ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockProDef, EvaluationParameters, blockPro);
		ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().BlockValueDef, EvaluationParameters, blockValue);
		if (blockPro > 0 && blockValue>0)
		{
			bool bBlocked = UKismetMathLibrary::RandomBoolWithWeight(blockPro);
			if (bBlocked)
			{
				if (blockValue>=PhysicsDamage)
				{
					TargetAbilitySystemComponent->ReceiveBlock(SourceAbilitySystemComponent, hit, PhysicsDamage, blockValue);
				}
				else
				{
					float Unmi = PhysicsDamage;
					PhysicsDamage = PhysicsDamage - blockValue;
					TargetAbilitySystemComponent->ReceiveBlock(SourceAbilitySystemComponent, hit, Unmi,blockValue);
					TargetAbilitySystemComponent->ReceiveDamage(SourceAbilitySystemComponent, hit, PhysicsAttack, PhysicsDamage,ESRDamageType::PHYSICS);
					OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().Damage_PhysicsProperty, EGameplayModOp::Additive, PhysicsDamage));
				}
			}
		}
		else
		{
			TargetAbilitySystemComponent->ReceiveDamage(SourceAbilitySystemComponent, hit, PhysicsAttack,PhysicsDamage, ESRDamageType::PHYSICS);
			OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(DamageStatics().Damage_PhysicsProperty, EGameplayModOp::Additive, PhysicsDamage));
		}
	}

测试

image-20201226110601324

完成!

伤害进阶处理:MMC

需求:

法术技能有额外加成, 比如某个装备上有词缀(火系伤害+50%),那么问题来了,

我们之前测试的火系技能伤害最后就变成是 FireDamage = 攻击力 * 等级系数 * (1 + 火系加成)

如果还是用AttributeBase基于属性的方式是不行了, 这里我们用一个自定义的MMC类就比较方便了

上一篇已经大概讲了,MMC需要封装几个蓝图函数方便使用, 不然就去cpp

	UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MMC")
		AActor* GetInstigator(const FGameplayEffectSpec& Spec)const;
	UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MMC")
		FGameplayAttribute GetAttributeFromDef(const FGameplayEffectAttributeCaptureDefinition& _Def)const;
	UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MMC")
		float TryGetCapturedAttributeMagnitude(const FGameplayEffectAttributeCaptureDefinition& Def, const FGameplayEffectSpec& Spec)const;
	UFUNCTION(BlueprintCallable, BlueprintPure, Category = "MMC")
		float GetLevel(const FGameplayEffectSpec& Spec)const;

image-20201226113630345

补充一个属性图, 我们初始化的时候给了50%的火系加成

image-20201226113716170

测试

image-20201226113752690

重点看税前已经变成1.5的伤害了, 正确