虚幻四Gameplay Ability System入门(7)-Gameplay Effect详解(2)自定义Calculation Class

15,463 阅读4分钟

本篇文章主要参考EPIC商场的RPG游戏demo。实现GE计算中使用自定义的Calculation Class

新建Attribute

打开AttributeSetBase.h/cpp,增加两个属性Damage和Armor,方法和之前的一样,这里就直接贴代码了。

UCLASS()
class GAS_LEARN_API UAttributeSetBase : public UAttributeSet
{
	GENERATED_BODY()

public:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Attributes", ReplicatedUsing=OnRep_Damage)
	FGameplayAttributeData Damage;
	ATTRIBUTE_ACCESSORS(UAttributeSetBase, Damage);

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Attributes", ReplicatedUsing=OnRep_Armor)
	FGameplayAttributeData Armor;
	ATTRIBUTE_ACCESSORS(UAttributeSetBase, Armor);

	UFUNCTION()
  virtual void OnRep_Damage(const FGameplayAttributeData& OldDamage);

	UFUNCTION()
	virtual void OnRep_Armor(const FGameplayAttributeData& OldArmor);

};
void UAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME_CONDITION_NOTIFY(UAttributeSetBase, Damage, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UAttributeSetBase, Armor, COND_None, REPNOTIFY_Always);
}

void UAttributeSetBase::OnRep_Damage(const FGameplayAttributeData& OldDamage)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, Damage, OldDamage);
}

void UAttributeSetBase::OnRep_Armor(const FGameplayAttributeData& OldArmor)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UAttributeSetBase, Armor, OldArmor);
}

初始化两个属性。 Untitled.png

打开之前创建的DataTable。可能有人没有看过之前的文章,这里我再贴一次流程图。 Untitled 1.png

打开角色蓝图的ASC的Default Settings。

Untitled 2.png

Untitled 3.png

如果不想用DataTable,也可以使用一个gameplay effect赋初值。 Untitled 4.png 然后通过ApplyGameplayEffectToSelf来赋初值。

Custom Calculation Class

新建类

这里我命名为GMM_DamageExecutionCalculation。 Untitled 5.png

实现

打开GMM_DamageExecutionCalculation.h

这里通过重写方法CalculateBaseMagnitude_Implementation,返回的float值即为计算得到的Magnitude,用于修改指定的Attribute。

UCLASS()
class GAS_LEARN_API UGMM_DamageExecutionCalculation : public UGameplayModMagnitudeCalculation
{
	GENERATED_BODY()

public:
	UGMM_DamageExecutionCalculation();

	virtual float CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const override;
	
};

如果我们打开UGameplayModMagnitudeCalculation.cpp。可以看到被我们重写的函数比较简单,就是返回0值

float UGameplayModMagnitudeCalculation::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
	return 0.f;
}

GMM_DamageExecutionCalculation.cpp,创建结构体SDamageStatics和构造函数

里面用于存储我们需要获取的Attribute对象。其中Damage需要从施加伤害方获取,Armor需要从被攻击方获取,这个逻辑应该比较容易理解吧。

struct SDamageStatics
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);

	SDamageStatics()
	{
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Damage, Source, true);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Armor, Target, true);
	}
};

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

UGMM_DamageExecutionCalculation::UGMM_DamageExecutionCalculation()
{
	RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
}

这一部分不影响这里用到了两个Macro方法,分别是DECLARE_ATTRIBUTE_CAPTUREDEF和DEFINE_ATTRIBUTE_CAPTUREDEF。可以查看一下他们的源码。

DECLARE_ATTRIBUTE_CAPTUREDEF定义了两个对象,假设是Damage的,那就是DamageProperty和DamageDef。

DEFINE_ATTRIBUTE_CAPTUREDEF尝试对两个对象赋值。

// -------------------------------------------------------------------------
//	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); \
}

然后进一步看看FGameplayEffectAttributeCaptureDefinition结构体有什么对象。这里比较重要的是两个数据,一个是GameplayAttribute,即我们需要捕获的Attribute,还有一个是GameplayEffectAttributeCaptureSource,即Attribute的Source,有这两个就可以成功获得我们需要的属性了。

/** Struct defining gameplay attribute capture options for gameplay effects */
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayEffectAttributeCaptureDefinition
{
	GENERATED_USTRUCT_BODY()

	/** Gameplay attribute to capture */
	UPROPERTY(EditDefaultsOnly, Category=Capture)
	FGameplayAttribute AttributeToCapture;

	/** Source of the gameplay attribute */
	UPROPERTY(EditDefaultsOnly, Category=Capture)
	EGameplayEffectAttributeCaptureSource AttributeSource;
};

GMM_DamageExecutionCalculation.cpp,实现CalculateBaseMagnitude_Implementation

通过GetCapturedAttributeMagnitude方法获取Damage和Armor的属性值,这里计算最终伤害的方法很简单,就是Damage-Armor。

float UGMM_DamageExecutionCalculation::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) const
{
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

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

	float Damage = 0.0f;
	GetCapturedAttributeMagnitude(DamageStatics().DamageDef, Spec, EvaluationParameters, Damage);

	float Armor = 0.0f;
	GetCapturedAttributeMagnitude(DamageStatics().ArmorDef, Spec, EvaluationParameters, Armor);

	return Damage - Armor;
}

这里使用的GetCapturedAttributeMagnitude方法实际是一个bool函数,我们可以判断是否成功获取了对应的属性值

bool UGameplayModMagnitudeCalculation::GetCapturedAttributeMagnitude(const FGameplayEffectAttributeCaptureDefinition& Def, const FGameplayEffectSpec& Spec, const FAggregatorEvaluateParameters& EvaluationParameters, OUT float& Magnitude) const
{
	const FGameplayEffectAttributeCaptureSpec* CaptureSpec = Spec.CapturedRelevantAttributes.FindCaptureSpecByDefinition(Def, true);
	if (CaptureSpec == nullptr)
	{
		ABILITY_LOG(Error, TEXT("GetCapturedAttributeMagnitude unable to get capture spec."));
		return false;
	}
	if (CaptureSpec->AttemptCalculateAttributeMagnitude(EvaluationParameters, Magnitude) == false)
	{
		ABILITY_LOG(Error, TEXT("GetCapturedAttributeMagnitude unable to calculate attribute magnitude."));
		return false;
	}

	return true;
}

测试

自己设置一个gameplay effect。测试一下,设置的Damge=50,Armor=20,造成的伤害为30,没有问题。

Untitled 6.png

Untitled 7.png

Execution Calculation Class

Exeution类的实现方法和自定义计算单元类似,区别是对attribute的修改时直接在类中实现了,而不是返回一个float修改量。

新建类

Untitled 8.png

实现

打开UGEDamageExecutionCalculation.h

和之前唯一的区别是重写了Execute_Implementation函数。

UCLASS()
class GAS_LEARN_API UGEDamageExecutionCalculation : public UGameplayEffectExecutionCalculation
{
	GENERATED_BODY()

public:
	UGEDamageExecutionCalculation();

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

打开UGEDamageExecutionCalculation.cpp

在这里因为需要直接修改Target的Health Attribute,所以还需要声明一个Health Attribute。

struct SDmageStatics
{
	DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Armor);
	DECLARE_ATTRIBUTE_CAPTUREDEF(Health)

	SDmageStatics()
	{
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Damage, Source, true);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Armor, Target, true);
		DEFINE_ATTRIBUTE_CAPTUREDEF(UAttributeSetBase, Health, Target, true);
	}
};

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

UGEDamageExecutionCalculation::UGEDamageExecutionCalculation()
{
	RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
	RelevantAttributesToCapture.Add(DamageStatics().ArmorDef);
	RelevantAttributesToCapture.Add(DamageStatics().HealthDef);
}

在Execute_Implementation中,我们计算获得最终的伤害后,通过OutExecutionOutput直接修改了Target的Health。在Execution中我可以通过这种方法同时修改多个Attribute,可以同时修改Target和Source的属性。

更重要的是,这里我们获取了Target或Source的AbilitySystemComponent,我们可以通过delegate或者ASC中的public function直接向目标的ASC发送信息,调用函数。所以Execution可以实现的效果是丰富的。

void UGEDamageExecutionCalculation::Execute_Implementation(
	const FGameplayEffectCustomExecutionParameters& ExecutionParams,
	FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	UAbilitySystemComponent* TargetAbilitySystem = ExecutionParams.GetTargetAbilitySystemComponent();
	UAbilitySystemComponent* SourceAbilitySystem = ExecutionParams.GetSourceAbilitySystemComponent();

	AActor* TargetActor = TargetAbilitySystem ? TargetAbilitySystem->GetAvatarActor() : nullptr;
	AActor* SourceActor = SourceAbilitySystem ? SourceAbilitySystem->GetAvatarActor() : nullptr;

	const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();

	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();

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

	float Armor = 0.0f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().ArmorDef, EvaluationParameters, Armor);

	float Damage = 0.0f;
	ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);
	
	float UnmitigatedDamage = Damage;

	float MitigatedDamage = UnmitigatedDamage - Armor;

	if(MitigatedDamage > 0.0f)
	{
		OutExecutionOutput.AddOutputModifier(
			FGameplayModifierEvaluatedData(DamageStatics().HealthProperty, EGameplayModOp::Additive, -MitigatedDamage));

		OutExecutionOutput.AddOutputModifier(
			FGameplayModifierEvaluatedData(DamageStatics().ArmorProperty, EGameplayModOp::Additive, -10.0f));
	}
}

测试

同样创建一个Gameplay Effect。可以看到角色的属性中Health减少了30,Armor也减少了10。 Untitled 9.png Untitled 10.png

在这里我们还可以通过Calculation Modifiers对输入的属性在运算前进一步的修改。我们可以让Damage在计算前先增加10。可以看到这次最终的伤害变为40了,让health减小为60 Untitled 11.png Untitled 12.png

我们可以看到一个Execution同样可以拥有modifier和Conditional GE,其中的设置和modifier几乎一致,意味着我们可以对单个Execution进一步进行一些复杂的设计和计算。

OK,到这里这一篇的内容就完成了,下一篇会讲解GE的剩余部分。耐心看完,你一定会有所收获(笑)