92. UE5 GAS RPG 使用C++创建GE实现灼烧的负面效果

在正常游戏里,有些伤害技能会携带一些负面效果,比如火焰伤害的技能会携带燃烧效果,敌人在受到伤害后,会接受一个燃烧的效果,燃烧效果会在敌人身上持续一段时间,并且持续受到火焰灼烧。

我们将在这一篇文章里,实现伤害技能附带负面效果,并可以设置负面效果的参数,来实现对敌人添加负面buff

添加负面效果标签

首先我们添加对应的伤害类型的负面标签

cpp 复制代码
	FGameplayTag DeBuff_Burn; //火属性负面效果 燃烧
	FGameplayTag DeBuff_Stun; //雷属性负面效果 眩晕
	FGameplayTag DeBuff_Arcane; //魔法伤害负面效果
	FGameplayTag DeBuff_Physical; //物理伤害负面效果 流血

并且添加一个Map,用于负面效果标签和属性抵抗表情对应,抵抗可以用于降低负面效用的成功率

cpp 复制代码
TMap<FGameplayTag, FGameplayTag> DeBuffsToResistance; //属性伤害标签对应负面标签

将标签注册的标签管理器

cpp 复制代码
	/*
	 * 负面标签注册
	*/
	GameplayTags.DeBuff_Burn = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Burn"),
			FString("火属性燃烧负面标签")
			);
	GameplayTags.DeBuff_Stun = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Stun"),
			FString("雷属性眩晕负面标签")
			);
	GameplayTags.DeBuff_Arcane = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Arcane"),
			FString("魔法属性负面标签")
			);
	GameplayTags.DeBuff_Physical = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Physical"),
			FString("物理属性流血负面标签")
			);

我们在应用负面效果时,目标角色可以通过自身的对应类型的抵抗来降低负面效果应用的成功率,所以,我们需要一个对应的Map

cpp 复制代码
	/*
	 * 负面标签和属性抵抗标签对于对应
	*/
	GameplayTags.DeBuffsToResistance.Add(GameplayTags.DeBuff_Burn, GameplayTags.Attributes_Resistance_Fire);
	GameplayTags.DeBuffsToResistance.Add(GameplayTags.DeBuff_Stun, GameplayTags.Attributes_Resistance_Lightning);
	GameplayTags.DeBuffsToResistance.Add(GameplayTags.DeBuff_Arcane, GameplayTags.Attributes_Resistance_Arcane);
	GameplayTags.DeBuffsToResistance.Add(GameplayTags.DeBuff_Physical, GameplayTags.Attributes_Resistance_Physical);

添加负面效果配置项

如果我们需要添加一些负面效果相关的配置项,然后使用Set By Caller 的方式去设置,我们先在RPGDamageGameplayAbility.h里添加对负面效果的配置项

cpp 复制代码
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	FGameplayTag DeBuffDamageType = FGameplayTag(); //负面效果伤害类型
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float DeBuffChance = 20.f; //触发负面的机率
	
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float DeBuffDamage = 5.f; //负面伤害

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float DeBuffFrequency = 1.f; //负面伤害触发间隔时间

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float DeBuffDuration = 5.f; //负面效果持续时间

我们要通过Set ByCaller设置负面效果GE的属性,那么,我们选择使用标签,这样不会出错

接着,我们创建四个对应的标签

cpp 复制代码
	FGameplayTag DeBuff_Chance; //负面效果触发几率标签
	FGameplayTag DeBuff_Damage; //负面效果伤害标签
	FGameplayTag DeBuff_Duration; //负面效果持续时间标签
	FGameplayTag DeBuff_Frequency; //负面效果触发间隔标签

然后注册

cpp 复制代码
	/*
	 * 负面效果配置标签
	*/
	GameplayTags.DeBuff_Chance = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Chance"),
			FString("负面效果 触发几率")
			);
	GameplayTags.DeBuff_Damage = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Damage"),
			FString("负面效果 伤害")
			);
	GameplayTags.DeBuff_Duration = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Duration"),
			FString("负面效果 持续时间")
			);
	GameplayTags.DeBuff_Frequency = UGameplayTagsManager::Get()
		.AddNativeGameplayTag(
			FName("DeBuff.Frequency"),
			FString("负面效果 触发间隔")
			);

创建负面效果使用的结构体

接下来,我们创建一个结构体,用于在给目标应用负面效果时使用,由于这个结构体的数据需要序列化以后,传输到服务器端进行处理,所以,我们将其设置到RPGAbilityTypes.h文件内,之前我们创建它是为了在代码内创建GE句柄时,能够使用我们自定义的结构体,增加了暴击和格挡的数据。

我们在里面创建一个结构体,用来配置应用一个负面效果时,所需要的所有数据

cpp 复制代码
USTRUCT(BlueprintType)
struct FDamageEffectParams
{
	GENERATED_BODY()

	FDamageEffectParams(){}

	UPROPERTY()
	TObjectPtr<UObject> WorldContextObject = nullptr; //当前场景上下文对象

	UPROPERTY()
	TSubclassOf<UGameplayEffect> DamageGameplayEffectClass = nullptr; //需要应用的GE的类

	UPROPERTY()
	TObjectPtr<UAbilitySystemComponent> SourceAbilitySystemComponent; //源ASC

	UPROPERTY()
	TObjectPtr<UAbilitySystemComponent> TargetAbilitySystemComponent; //目标ASC

	UPROPERTY()
	TMap<FGameplayTag, float> DamageTypes; //技能造成的多种伤害和伤害类型

	UPROPERTY()
	float AbilityLevel = 1.f; //技能等级

	UPROPERTY()
	FGameplayTag DeBuffDamageType = FGameplayTag(); //负面效果伤害类型

	UPROPERTY()
	float DeBuffChance = 0.f; //触发负面效果概率

	UPROPERTY()
	float DeBuffDamage = 0.f; //负面效果伤害

	UPROPERTY()
	float DeBuffDuration = 0.f; //负面效果持续时间

	UPROPERTY()
	float DeBuffFrequency = 0.f; //负面效果触发频率
};

在伤害技能基础类增加一个创建配置项的函数

有了配置项的结构体,我们需要实现一个函数,在伤害技能的基类上,通过伤害技能的配置生成结构体

cpp 复制代码
	//创建技能负面效果使用的结构体
	FDamageEffectParams MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor = nullptr);

然后实现通过技能上的配置生成配置项

cpp 复制代码
FDamageEffectParams URPGDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor)
{
	FDamageEffectParams Params;
	Params.WorldContextObject = GetAvatarActorFromActorInfo();
	Params.DamageGameplayEffectClass = DamageEffectClass;
	Params.SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
	Params.TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(TargetActor);
	for(auto& Pair : DamageTypes)
	{
		const float ScaledDamage = Pair.Value.GetValueAtLevel(GetAbilityLevel()); //根据等级获取技能伤害
		Params.DamageTypes.Add(Pair.Key, ScaledDamage);
	}
	Params.AbilityLevel = GetAbilityLevel();
	Params.DeBuffDamageType = DeBuffDamageType;
	Params.DeBuffChance = DeBuffChance;
	Params.DeBuffDamage = DeBuffDamage;
	Params.DeBuffDuration = DeBuffDuration;
	Params.DeBuffFrequency = DeBuffFrequency;
	return Params;
}

在函数库添加一个通过配置项实现GE的应用

我们有了配置项,可以将应用设置为通用的函数,所以我们在函数库里增加一个静态函数,只要结构体配置完全,我们可以直接调用此函数完成内部逻辑

我们添加一个函数,传入参数就是配置项

cpp 复制代码
	//通过技能生成的负面配置项应用技能负面效果
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static FGameplayEffectContextHandle ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams);

然后实现此函数,在函数内存创建GE的上下文和实例,并通过标签的SetByCaller设置GE所使用的值,最后应用到目标ASC上,并返回GE的上下文句柄。

注意,这里我们设置伤害的标签是属性伤害类型标签,在应用时,我们就可以负面效果类型获取当前GE是否设置了对应的负面效果

cpp 复制代码
FGameplayEffectContextHandle URPGAbilitySystemBlueprintLibrary::ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams)
{
	const FRPGGameplayTags& GameplayTags = FRPGGameplayTags::Get();
	const AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();

	//创建GE的上下文句柄
	FGameplayEffectContextHandle EffectContextHandle = DamageEffectParams.SourceAbilitySystemComponent->MakeEffectContext();
	EffectContextHandle.AddSourceObject(SourceAvatarActor);

	//根据句柄和类创建GE实例
	const FGameplayEffectSpecHandle SpecHandle = DamageEffectParams.SourceAbilitySystemComponent->MakeOutgoingSpec(DamageEffectParams.DamageGameplayEffectClass, DamageEffectParams.AbilityLevel, EffectContextHandle);

	//通过标签设置GE使用的配置
	for(auto& Pair : DamageEffectParams.DamageTypes)
	{
		UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, Pair.Key, Pair.Value);
	}
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.DeBuff_Chance, DamageEffectParams.DeBuffChance);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, DamageEffectParams.DeBuffDamageType, DamageEffectParams.DeBuffDamage);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.DeBuff_Duration, DamageEffectParams.DeBuffDuration);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.DeBuff_Frequency, DamageEffectParams.DeBuffFrequency);

	//将GE应用给目标ASC
	DamageEffectParams.TargetAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
	return EffectContextHandle;
}

修改生成发射物的函数

我们在ProjectileSpell.cpp文件里,由于修改了增加的负面效果增加的部分,所以,我们需要对发射物生成进行修改,不再创建发射物时创建GE,而是修改为生成一个配置结构体,供后续使用。

cpp 复制代码
void UProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, const FName SocketName, const bool bOverridePitch, const float PitchOverride)
{
	const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority(); //判断此函数是否在服务器运行
	if (!bIsServer) return;

	if (GetAvatarActorFromActorInfo()->Implements<UCombatInterface>())
	{
		const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocationByTag(GetAvatarActorFromActorInfo(), SocketTag, SocketName);
		FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation(); //将方向转为旋转
		if(bOverridePitch)
		{
			Rotation.Pitch = PitchOverride; //覆写发射角度
		}
		
		FTransform SpawnTransform;
		SpawnTransform.SetLocation(SocketLocation);
		SpawnTransform.SetRotation(Rotation.Quaternion());
		
		//SpawnActorDeferred将异步创建实例,在实例创建完成时,相应的数据已经应用到了实例身上
		AProjectile* Projectile = GetWorld()->SpawnActorDeferred<AProjectile>(
			ProjectileClass,
			SpawnTransform,
			GetOwningActorFromActorInfo(),
			Cast<APawn>(GetAvatarActorFromActorInfo()),
			ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

		Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();

		//确保变换设置被正确应用
		Projectile->FinishSpawning(SpawnTransform);
	}
}

在AbilityTypes.h里增加参数

在之前的文章里 51. UE5 RPG 自定义FGameplayEffectContext,我们实现自定义GE上下文的属性,增加了格挡和暴击两个属性是否触发,并实现了对其的序列化,可以在客户端将内容复制到服务器端,因为属性的处理都是在服务器端进行的,这里有个交互的过程,需要序列化。

我们给GE上下文增加多个属性,这样,生成的实例里可以设置这些属性

注意,这里的标签没有设置反射,需要我们自己实现类型,在序列化时也有所体现。

cpp 复制代码
protected:

	UPROPERTY()
	bool bIsBlockedHit = false; //格挡

	UPROPERTY()
	bool bIsCriticalHit = false; //暴击

	UPROPERTY()
	bool bIsSuccessfulDeBuff = false; //成功应用负面效果
	
	UPROPERTY()
	float DeBuffDamage = 0.f; //负面效果每次造成的伤害

	UPROPERTY()
	float DeBuffDuration = 0.f; //负面效果持续时间

	UPROPERTY()
	float DeBuffFrequency = 0.f; //负面效果触发频率间隔

	TSharedPtr<FGameplayTag> DamageType; //负面效果的伤害类型

并增加其对应的设置方法

cpp 复制代码
public:

	bool IsBlockedHit() const { return bIsBlockedHit; } //获取 格挡
	bool IsCriticalHit() const { return bIsCriticalHit; } //获取 暴击
	bool IsSuccessfulDeBuff() const { return bIsSuccessfulDeBuff; } //获取 应用负面效果
	float GetDeBuffDamage() const { return DeBuffDamage; } //获取 负面效果伤害
	float GetDeBuffDuration() const { return DeBuffDuration; } //获取 负面效果持续时间
	float GetDeBuffFrequency() const { return DeBuffFrequency; } //获取 负面效果伤害触发间隔
	TSharedPtr<FGameplayTag> GetDeBuffDamageType() const { return DamageType; } //获取 负面效果伤害类型

	void SetIsBlockedHit(const bool bInIsBlockedHit) { bIsBlockedHit = bInIsBlockedHit; } // 设置 格挡
	void SetIsCriticalHit(const bool bInIsCriticalHit) { bIsCriticalHit = bInIsCriticalHit; } // 设置 暴击
	void SetIsSuccessfulDeBuff(const bool bInIsSuccessfulDeBuff) { bIsSuccessfulDeBuff = bInIsSuccessfulDeBuff; } //设置 应用负面效果
	void SetDeBuffDamage(const float InDamage) { DeBuffDamage = InDamage; } //设置 负面效果伤害
	void SetDeBuffDuration(const float InDuration) { DeBuffDuration = InDuration; } //设置 负面效果伤害
	void SetDeBuffFrequency(const float InFrequency) { DeBuffFrequency = InFrequency; } //设置 负面效果伤害
	void SetDeBuffDamageType(const TSharedPtr<FGameplayTag>& InDamageType) { DamageType = InDamageType; } //设置 负面效果伤害类型

我们还要修改它的序列化的方法,让其能够复制到服务器端去处理。

首先是增加序列化内容

cpp 复制代码
		//自定义内容,增加暴击和格挡触发存储
		if(bIsBlockedHit)
		{
			RepBits |= 1 << 7;
		}
		if(bIsCriticalHit)
		{
			RepBits |= 1 << 8;
		}
		if(bIsSuccessfulDeBuff)
		{
			RepBits |= 1 << 9;
		}
		if(DeBuffDamage > 0.f)
		{
			RepBits |= 1 << 10;
		}
		if(DeBuffDuration > 0.f)
		{
			RepBits |= 1 << 11;
		}
		if(DeBuffFrequency > 0.f)
		{
			RepBits |= 1 << 12;
		}
		if(DamageType.IsValid())
		{
			RepBits |= 1 << 13;
		}

接着就是设置序列内容的长度

cpp 复制代码
	//使用了多少长度,就将长度设置为多少
	Ar.SerializeBits(&RepBits, 14);

接着就是在服务器端的反序列化

cpp 复制代码
	//新增对暴击格挡的序列化或反序列化处理
	if (RepBits & (1 << 7))
	{
		Ar << bIsBlockedHit;
	}
	if (RepBits & (1 << 8))
	{
		Ar << bIsCriticalHit;
	}
	if (RepBits & (1 << 9))
	{
		Ar << bIsSuccessfulDeBuff;
	}
	if (RepBits & (1 << 10))
	{
		Ar << DeBuffDamage;
	}
	if (RepBits & (1 << 11))
	{
		Ar << DeBuffDuration;
	}
	if (RepBits & (1 << 12))
	{
		Ar << DeBuffFrequency;
	}
	if (RepBits & (1 << 13))
	{
		if (Ar.IsLoading()) //判断是否在加载资源
		{
			if (!DamageType.IsValid())
			{
				DamageType = TSharedPtr<FGameplayTag>(new FGameplayTag());
			}
		}
		DamageType->NetSerialize(Ar, Map, bOutSuccess);
	}

这就完成了对自定义属性的添加,并且能够实现服务器获取,在AttributeSet里对属性进行处理。

接着,我们在函数库里增加一些函数,可以直接调用函数库内的函数实现对属性设置和获取

cpp 复制代码
	//获取当前GE是否成功应用负面效果
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static bool IsSuccessfulDeBuff(const FGameplayEffectContextHandle& EffectContextHandle);

	//获取当前GE负面效果伤害
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static float GetDeBuffDamage(const FGameplayEffectContextHandle& EffectContextHandle);

	//获取当前GE负面效果持续时间
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static float GetDeBuffDuration(const FGameplayEffectContextHandle& EffectContextHandle);

	//获取当前GE负面效果触发间隔
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static float GetDeBuffFrequency(const FGameplayEffectContextHandle& EffectContextHandle);

	//获取当前GE负面效果伤害类型
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static FGameplayTag GetDeBuffDamageType(const FGameplayEffectContextHandle& EffectContextHandle);
cpp 复制代码
bool URPGAbilitySystemBlueprintLibrary::IsSuccessfulDeBuff(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->IsSuccessfulDeBuff();
	}
	return false;
}

float URPGAbilitySystemBlueprintLibrary::GetDeBuffDamage(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->GetDeBuffDamage();
	}
	return 0.f;
}

float URPGAbilitySystemBlueprintLibrary::GetDeBuffDuration(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->GetDeBuffDuration();
	}
	return 0.f;
}

float URPGAbilitySystemBlueprintLibrary::GetDeBuffFrequency(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->GetDeBuffFrequency();
	}
	return 0.f;
}

FGameplayTag URPGAbilitySystemBlueprintLibrary::GetDeBuffDamageType(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		//如果当前存在设置了伤害类型
		if(RPGEffectContext->GetDeBuffDamageType().IsValid())
		{
			//取消指针
			return *RPGEffectContext->GetDeBuffDamageType();
		}
	}
	return FGameplayTag();
}

在设置这里,因为是需要在同一地方使用,所以,我们直接将其合并为了一个函数设置

cpp 复制代码
	//设置GE是否应用负面效果
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetIsSuccessfulDeBuff(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsSuccessfulDeBuff);

	//设置GE负面效果相关数值 负面效果伤害类型 负面效果伤害 负面效果持续时间 负面效果触发间隔时间
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetDeBuff(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, FGameplayTag& InDamageType, float InDamage, float InDuration, float InFrequency);
cpp 复制代码
void URPGAbilitySystemBlueprintLibrary::SetIsSuccessfulDeBuff(FGameplayEffectContextHandle& EffectContextHandle, const bool bInIsSuccessfulDeBuff)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetIsSuccessfulDeBuff(bInIsSuccessfulDeBuff);
}

void URPGAbilitySystemBlueprintLibrary::SetDeBuff(FGameplayEffectContextHandle& EffectContextHandle, FGameplayTag& InDamageType, const float InDamage, const float InDuration, const float InFrequency)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	//通过标签创建一个共享指针
	const TSharedPtr<FGameplayTag> DamageType = MakeShared<FGameplayTag>(InDamageType);
	RPGEffectContext->SetDeBuffDamageType(DamageType);
	RPGEffectContext->SetDeBuffDamage(InDamage);
	RPGEffectContext->SetDeBuffDuration(InDuration);
	RPGEffectContext->SetDeBuffFrequency(InFrequency);
}

实现负面效果应用

实现了相应的函数后,我们需要接着实现负面效果的应用,我们在ExecCalc_Damage.cpp里,专门实现了对目标进行造成的最终伤害的计算,我们在内部实现负面效果的是否应用成功的计算,首先获取到负面效果命中率,然后通过目标抵抗降低命中率,接着通过随机数判断当前是否需要应用负面效果。

cpp 复制代码
void UExecCalc_Damage::DetermineDeBuff(const FGameplayEffectCustomExecutionParameters& ExecutionParams, const FGameplayEffectSpec& Spec, const FAggregatorEvaluateParameters& EvaluationParameters, TMap<FGameplayTag, FGameplayEffectAttributeCaptureDefinition> TagsToCaptureDefs)
{
	const FRPGGameplayTags& GameplayTags = FRPGGameplayTags::Get();

	//遍历所有的负面效果伤害类型,根据伤害类型是否赋值来判断是否需要应用负面效果
	for(const TTuple<FGameplayTag, FGameplayTag>& Pair : GameplayTags.DeBuffsToResistance)
	{
		FGameplayTag DeBuffDamageType = Pair.Key; //获取到负面效果伤害类型
		const FGameplayTag ResistanceType = Pair.Value; //获取到负面效果抵抗类型
		const float TypeDamage = Spec.GetSetByCallerMagnitude(DeBuffDamageType, false, -1.f);

		//如果负面效果设置了伤害,即使为0,也需要应用负面效果
		if(TypeDamage > -.5f)
		{
			//获取负面效果命中率
			const float SourceDeBuffChance = Spec.GetSetByCallerMagnitude(GameplayTags.DeBuff_Chance, false, -1.f);

			//----------------获取负面效果抵抗------------
			float TargetDeBuffResistance = 0.f; //计算目标对收到的负面效果类型的抵抗
			//检查对应的属性快照是否设置,防止报错
			checkf(TagsToCaptureDefs.Contains(ResistanceType), TEXT("在ExecCalc_Damage中,无法获取到Tag[%s]对应的属性快照"), *ResistanceType.ToString());
			//通过抗性标签获取到属性快照的值
			const FGameplayEffectAttributeCaptureDefinition CaptureDef = TagsToCaptureDefs[ResistanceType];
			ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CaptureDef, EvaluationParameters, TargetDeBuffResistance);
			TargetDeBuffResistance = FMath::Clamp(TargetDeBuffResistance, 0.f, 100.f); //将抗住限制在0到100

			//----------------计算负面效果是否应用------------
			const float EffectiveDeBuffChance = SourceDeBuffChance * (100 - TargetDeBuffResistance) / 100.f; //计算出负面效果的实际命中率
			const bool bDeBuff = FMath::RandRange(1, 100) < EffectiveDeBuffChance; //判断此次效果是否实现命中
			if(bDeBuff)
			{
				//获取GE上下文设置负面效果相关配置
				FGameplayEffectContextHandle ContextHandle = Spec.GetContext();

				//设置当前应用负面效果成功
				URPGAbilitySystemBlueprintLibrary::SetIsSuccessfulDeBuff(ContextHandle, true);

				const float SourceDeBuffDuration = Spec.GetSetByCallerMagnitude(GameplayTags.DeBuff_Duration, false, 0.f);
				const float SourceDeBuffFrequency = Spec.GetSetByCallerMagnitude(GameplayTags.DeBuff_Frequency, false, 0.f);
				//设置负面效果 伤害类型 伤害 持续时间 触发频率
				URPGAbilitySystemBlueprintLibrary::SetDeBuff(ContextHandle, DeBuffDamageType, TypeDamage, SourceDeBuffDuration, SourceDeBuffFrequency);
			}
		}
	}
}

将一部分代码转换为函数

有个小技巧,我们还可以将其内容提取为单独函数

设置命名和参数

可以去除不需要的参数。

整理AttributeSet内的代码

代码写的越来越多,会变成一坨,所以我们需要将代码分离成函数,这样方便后期的维护。

我们在自定义的AttributeSet里增加三个函数,分别用于处理接收实际受到的伤害属性,获取到的经验值,以及需要处理应用负面效果的函数。

cpp 复制代码
	//处理传入的参数为伤害属性时,处理的逻辑
	void HandleIncomingDamage(const FEffectProperties& Props);

	//处理传入的参数为经验属性时,处理的逻辑
	void HandleIncomingXP(const FEffectProperties& Props);

	//如果当前伤害触发了负面效果,处理的逻辑
	void HandleDeBuff(const FEffectProperties& Props);

有了这几个函数,我们的代码就清晰了很多,比如接收到属性的函数代码,这里防止敌人在死亡后还会受到伤害,我们在进行属性处理前判断角色是否处于死亡状态。

cpp 复制代码
void URPGAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);

	FEffectProperties Props;
	SetEffectProperties(Data, Props);

	//判断当前目标是否已经死亡,如果死亡,将不再进行处理
	if(Props.TargetCharacter->Implements<UCombatInterface>() && ICombatInterface::Execute_IsDead(Props.TargetCharacter)) return;

	if(Data.EvaluatedData.Attribute == GetHealthAttribute())
	{
		SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
		// UE_LOG(LogTemp, Warning, TEXT("%s 的生命值发生了修改,当前生命值:%f"), *Props.TargetAvatarActor->GetName(), GetHealth());
	}

	if(Data.EvaluatedData.Attribute == GetManaAttribute())
	{
		SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
	}

	if(Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
	{
		HandleIncomingDamage(Props);
	}

	if(Data.EvaluatedData.Attribute == GetIncomingXPAttribute())
	{
		HandleIncomingXP(Props);
	}
	
}

由于它们都只需要在Props里拿值进行运算,我们只需要一个Props属性即可。

cpp 复制代码
void URPGAttributeSet::HandleIncomingXP(const FEffectProperties& Props)
{
	const float LocalIncomingXP = GetIncomingXP();
	SetIncomingXP(0);
	// UE_LOG(LogRPG, Log, TEXT("获取传入经验值:%f"), LocalIncomingXP);
		
	if(Props.SourceCharacter->Implements<UPlayerInterface>() && Props.SourceCharacter->Implements<UCombatInterface>())
	{
		//获取角色当前等级和经验
		const int32 CurrentLevel = ICombatInterface::Execute_GetPlayerLevel(Props.SourceCharacter);
		const int32 CurrentXP = IPlayerInterface::Execute_GetXP(Props.SourceCharacter);

		//获取获得经验后的新等级
		const int32 NewLevel = IPlayerInterface::Execute_FindLevelForXP(Props.SourceCharacter, CurrentXP + LocalIncomingXP);
		const int32 NumLevelUps = NewLevel - CurrentLevel; //查看等级是否有变化
		if(NumLevelUps > 0)
		{
			//如果连升多级,我们通过for循环获取每个等级的奖励
			for(int32 i = CurrentLevel; i < NewLevel; i++)
			{
				//获取升级提供的技能点和属性点
				const int32 AttributePointsReward = IPlayerInterface::Execute_GetAttributePointsReward(Props.SourceCharacter, i);
				const int32 SpellPointsReward = IPlayerInterface::Execute_GetSpellPointsReward(Props.SourceCharacter, i);

				//增加角色技能点和属性点
				IPlayerInterface::Execute_AddToAttributePoints(Props.SourceCharacter, AttributePointsReward);
				IPlayerInterface::Execute_AddToSpellPoints(Props.SourceCharacter, SpellPointsReward);
			}

			//提升等级
			IPlayerInterface::Execute_AddToPlayerLevel(Props.SourceCharacter, NumLevelUps);

			//播放升级效果
			IPlayerInterface::Execute_LevelUp(Props.SourceCharacter);

			//将血量和蓝量填充满, 我们将设置变量
			SetHealth(GetMaxHealth());
			SetMana(GetMaxMana());
		}
			
		//将经验应用给自身,通过事件传递,在玩家角色被动技能GA_ListenForEvents里接收
		IPlayerInterface::Execute_AddToXP(Props.SourceCharacter, LocalIncomingXP);
	}
}

在接收伤害的函数里,我们增加对负面效果应用的判断,如果当前应用的负面效果,我们将调用负面效果函数处理逻辑。

cpp 复制代码
void URPGAttributeSet::HandleIncomingDamage(const FEffectProperties& Props)
{
	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时,角色将会死亡
		if(bFatal)
		{
			//调用死亡函数
			ICombatInterface* CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
			if(CombatInterface)
			{
				CombatInterface->Die();
			}
			//死亡时,发送经验事件
			SendXPEvent(Props);
		}
		else
		{
			//激活受击技能
			FGameplayTagContainer TagContainer;
			TagContainer.AddTag(FRPGGameplayTags::Get().Effects_HitReact);
			// Props.TargetASC->CancelAbilities(&TagContainer); //先取消之前的受击
			Props.TargetASC->TryActivateAbilitiesByTag(TagContainer); //根据tag标签激活技能
		}

		//获取格挡和暴击
		const bool IsBlockedHit = URPGAbilitySystemBlueprintLibrary::IsBlockedHit(Props.EffectContextHandle);
		const bool IsCriticalHit = URPGAbilitySystemBlueprintLibrary::IsCriticalHit(Props.EffectContextHandle);

		//显示伤害数字
		ShowFloatingText(Props, LocalIncomingDamage, IsBlockedHit, IsCriticalHit);

		//判断当前是否应用负面效果
		if(URPGAbilitySystemBlueprintLibrary::IsSuccessfulDeBuff(Props.EffectContextHandle))
		{
			HandleDeBuff(Props);
		}
	}
}

通过代码创建GameplayEffect类

接下来,我们要实现设置GE,并将其应用到角色身上,这里,采用c++编写的方式实现,我会将代码和在UE里编辑的配置进行比对,方便大家更清晰的熟悉每个配置项。

我们将在之前添加的HandleDeBuff函数中增加处理,这个函数将在可以对目标应用负面效果时调用。

我们首先通过库函数获取到创建GE类的相关参数

cpp 复制代码
	//获取负面效果相关参数
	const FGameplayTag DeBuffType = URPGAbilitySystemBlueprintLibrary::GetDeBuffDamageType(Props.EffectContextHandle);
	const float DeBuffDamage = URPGAbilitySystemBlueprintLibrary::GetDeBuffDamage(Props.EffectContextHandle);
	const float DeBuffDuration = URPGAbilitySystemBlueprintLibrary::GetDeBuffDuration(Props.EffectContextHandle);
	const float DeBuffFrequency = URPGAbilitySystemBlueprintLibrary::GetDeBuffFrequency(Props.EffectContextHandle);

然后我们设置一个名称并创建一个GE类

cpp 复制代码
	//创建GE所使用的名称,并创建一个可实例化的GE
	FString DeBuffName = FString::Printf(TEXT("DynamicDeBuff_%s"), *DeBuffType.ToString());
	UGameplayEffect* Effect = NewObject<UGameplayEffect>(GetTransientPackage(), FName(DeBuffName));

接下来就是修改GE类的配置项,它们将在我们应用GE实例时,产生效果。

首先我们设置GE为有时间限制的,并设置对应时间

cpp 复制代码
	//设置动态创建GE的属性
	Effect->DurationPolicy = EGameplayEffectDurationType::HasDuration; //设置GE为有时间限制的效果
	Effect->DurationMagnitude = FScalableFloat(DeBuffDuration); //设置GE的持续时间

然后就是设置Period

cpp 复制代码
	Effect->Period = FScalableFloat(DeBuffFrequency); //设置GE的触发策略,间隔时间
	Effect->bExecutePeriodicEffectOnApplication = false; //在应用后不会立即触发,而是在经过了Period后才会触发
	Effect->PeriodicInhibitionPolicy = EGameplayEffectPeriodInhibitionRemovedPolicy::NeverReset; //设置每次应用后不会重置触发时间

接着就是叠加层数相关设置,这里和我们所需的不同,但是为了显示所有设置,我全添加了

cpp 复制代码
	//设置可叠加层数
	Effect->StackingType = EGameplayEffectStackingType::AggregateBySource; //设置GE应用基于释放者查看
	Effect->StackLimitCount = 1; //设置叠加层数
	Effect->StackDurationRefreshPolicy = EGameplayEffectStackingDurationPolicy::RefreshOnSuccessfulApplication; //在应用后重置时,重置持续时间
	Effect->StackPeriodResetPolicy = EGameplayEffectStackingPeriodPolicy::ResetOnSuccessfulApplication; //在应用时,触发并重置Period时间
	Effect->StackExpirationPolicy = EGameplayEffectStackingExpirationPolicy::ClearEntireStack; //GE时间到了默认清除所有层数,还有可以清除单层的设置
	//Effect->OverflowEffects.Add() //在叠加层数超出时,将触发此数组内的GE应用到角色
	Effect->bDenyOverflowApplication = true; //设置为true时,叠加层数超出时,将不会刷新GE实例
	Effect->bClearStackOnOverflow = true; //设置为true时,叠加层数超出时,将清除GE

在5.3版本修改为了通过GEComponent来设置Actor身上的标签,在老版是可以直接通过InheritableOwnedTagsContainer获取容器去修改

cpp 复制代码
	//在5.3版本修改为通过GEComponent来设置GE应用的标签,向目标Actor增加对应的标签
	UTargetTagsGameplayEffectComponent& TargetTagsGameplayEffectComponent = Effect->AddComponent<UTargetTagsGameplayEffectComponent>();
	FInheritedTagContainer InheritableOwnedTagsContainer = TargetTagsGameplayEffectComponent.GetConfiguredTargetTagChanges(); //获取到标签容器
	InheritableOwnedTagsContainer.AddTag(DeBuffType); //添加标签
	TargetTagsGameplayEffectComponent.SetAndApplyTargetTagChanges(InheritableOwnedTagsContainer); //应用并更新

接着就是设置属性的修改

cpp 复制代码
	//设置属性修改
	const int32 Index = Effect->Modifiers.Num(); //获取当前修改属性的Modifiers的长度,也就是下一个添加的modify的下标索引
	Effect->Modifiers.Add(FGameplayModifierInfo()); //添加一个新的Modify
	FGameplayModifierInfo& ModifierInfo = Effect->Modifiers[Index]; //通过下标索引获取Modify

	ModifierInfo.ModifierMagnitude = FScalableFloat(DeBuffDamage); //设置应用的属性值
	ModifierInfo.ModifierOp = EGameplayModOp::Additive; //设置属性运算符号
	ModifierInfo.Attribute = URPGAttributeSet::GetIncomingDamageAttribute(); //设置修改的属性

然后就通过GE创建GE实例,并应用到目标身上

cpp 复制代码
	//创建GE实例,并添加伤害类型标签,应用GE
	FGameplayEffectContextHandle EffectContextHandle = Props.SourceASC->MakeEffectContext();
	EffectContextHandle.AddSourceObject(Props.SourceCharacter);
	if(const FGameplayEffectSpec* MutableSpec = new FGameplayEffectSpec(Effect, EffectContextHandle, 1.f))
	{
		FRPGGameplayEffectContext* RPGContext = static_cast<FRPGGameplayEffectContext*>(MutableSpec->GetContext().Get());
		const TSharedPtr<FGameplayTag> DeBuffDamageType = MakeShareable(new FGameplayTag(DeBuffType));
		RPGContext->SetDeBuffDamageType(DeBuffDamageType);
		
		Props.TargetASC->ApplyGameplayEffectSpecToSelf(*MutableSpec);
	}

最后,粘贴一下完整代码

cpp 复制代码
void URPGAttributeSet::HandleDeBuff(const FEffectProperties& Props)
{
	//获取负面效果相关参数
	const FGameplayTag DeBuffType = URPGAbilitySystemBlueprintLibrary::GetDeBuffDamageType(Props.EffectContextHandle);
	const float DeBuffDamage = URPGAbilitySystemBlueprintLibrary::GetDeBuffDamage(Props.EffectContextHandle);
	const float DeBuffDuration = URPGAbilitySystemBlueprintLibrary::GetDeBuffDuration(Props.EffectContextHandle);
	const float DeBuffFrequency = URPGAbilitySystemBlueprintLibrary::GetDeBuffFrequency(Props.EffectContextHandle);

	//创建GE所使用的名称,并创建一个可实例化的GE
	FString DeBuffName = FString::Printf(TEXT("DynamicDeBuff_%s"), *DeBuffType.ToString());
	UGameplayEffect* Effect = NewObject<UGameplayEffect>(GetTransientPackage(), FName(DeBuffName));

	//设置动态创建GE的属性
	Effect->DurationPolicy = EGameplayEffectDurationType::HasDuration; //设置GE为有时间限制的效果
	Effect->DurationMagnitude = FScalableFloat(DeBuffDuration); //设置GE的持续时间
	
	Effect->Period = FScalableFloat(DeBuffFrequency); //设置GE的触发策略,间隔时间
	Effect->bExecutePeriodicEffectOnApplication = false; //在应用后不会立即触发,而是在经过了Period后才会触发
	Effect->PeriodicInhibitionPolicy = EGameplayEffectPeriodInhibitionRemovedPolicy::NeverReset; //设置每次应用后不会重置触发时间

	//设置可叠加层数
	Effect->StackingType = EGameplayEffectStackingType::AggregateBySource; //设置GE应用基于释放者查看
	Effect->StackLimitCount = 1; //设置叠加层数
	Effect->StackDurationRefreshPolicy = EGameplayEffectStackingDurationPolicy::RefreshOnSuccessfulApplication; //在应用后重置时,重置持续时间
	Effect->StackPeriodResetPolicy = EGameplayEffectStackingPeriodPolicy::ResetOnSuccessfulApplication; //在应用时,触发并重置Period时间
	Effect->StackExpirationPolicy = EGameplayEffectStackingExpirationPolicy::ClearEntireStack; //GE时间到了默认清除所有层数,还有可以清除单层的设置
	//Effect->OverflowEffects.Add() //在叠加层数超出时,将触发此数组内的GE应用到角色
	// Effect->bDenyOverflowApplication = true; //设置为true时,叠加层数超出时,将不会刷新GE实例
	// Effect->bClearStackOnOverflow = true; //设置为true时,叠加层数超出时,将清除GE

	//在5.3版本修改为通过GEComponent来设置GE应用的标签,向目标Actor增加对应的标签
	UTargetTagsGameplayEffectComponent& TargetTagsGameplayEffectComponent = Effect->AddComponent<UTargetTagsGameplayEffectComponent>();
	FInheritedTagContainer InheritableOwnedTagsContainer = TargetTagsGameplayEffectComponent.GetConfiguredTargetTagChanges(); //获取到标签容器
	InheritableOwnedTagsContainer.AddTag(DeBuffType); //添加标签
	TargetTagsGameplayEffectComponent.SetAndApplyTargetTagChanges(InheritableOwnedTagsContainer); //应用并更新

	//设置属性修改
	const int32 Index = Effect->Modifiers.Num(); //获取当前修改属性的Modifiers的长度,也就是下一个添加的modify的下标索引
	Effect->Modifiers.Add(FGameplayModifierInfo()); //添加一个新的Modify
	FGameplayModifierInfo& ModifierInfo = Effect->Modifiers[Index]; //通过下标索引获取Modify

	ModifierInfo.ModifierMagnitude = FScalableFloat(DeBuffDamage); //设置应用的属性值
	ModifierInfo.ModifierOp = EGameplayModOp::Additive; //设置属性运算符号
	ModifierInfo.Attribute = URPGAttributeSet::GetIncomingDamageAttribute(); //设置修改的属性

	//创建GE实例,并添加伤害类型标签,应用GE
	FGameplayEffectContextHandle EffectContextHandle = Props.SourceASC->MakeEffectContext();
	EffectContextHandle.AddSourceObject(Props.SourceCharacter);
	if(const FGameplayEffectSpec* MutableSpec = new FGameplayEffectSpec(Effect, EffectContextHandle, 1.f))
	{
		FRPGGameplayEffectContext* RPGContext = static_cast<FRPGGameplayEffectContext*>(MutableSpec->GetContext().Get());
		const TSharedPtr<FGameplayTag> DeBuffDamageType = MakeShareable(new FGameplayTag(DeBuffType));
		RPGContext->SetDeBuffDamageType(DeBuffDamageType);
		
		Props.TargetASC->ApplyGameplayEffectSpecToSelf(*MutableSpec);
	}

}

我们就可以去项目中测试是否有bug。

相关推荐
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
唐诺4 小时前
几种广泛使用的 C++ 编译器
c++·编译器
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc
冷眼看人间恩怨5 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget