102. UE5 GAS RPG 实现范围技能奥术伤害

在上一篇文章里,我们在技能蓝图里实现了通过技能实现技能指示,再次触发按键后,将通过定时器触发技能效果表现,最多支持11个奥术个体效果的播放。
在这一篇里,我们将实现技能播放时,对目标敌人应用技能伤害。

首先,我们将在GE里增加一些额外的参数,并且会设置序列化,可以同步到服务器,并在伤害技能类里创建配置项时增加对应参数,通过函数库应用时,将参数设置到GE实例,并在计算伤害的代码里,获取参数,并计算最终伤害。

添加范围伤害属性

首先,我们需要添加范围伤害相关的属性,需要在以下几个地方添加,由于之前制作技能时,也添加过,这里就不细说了,只列出对应的相关属性。
在RPGAbilityTypes.h中,伤害技能生成的配置项里,添加对应的参数

	//当前伤害类型是否为范围伤害
	UPROPERTY(BlueprintReadWrite)
	bool bIsRadialDamage = false;

	//内半径:在此半径内的所有目标都将受到完整的伤害
	UPROPERTY(BlueprintReadWrite)
	float RadialDamageInnerRadius = 0.f;

	//外半径:超过这个距离的目标受到最小伤害,最小伤害如果设置为0,则圈外不受到伤害
	UPROPERTY(BlueprintReadWrite)
	float RadialDamageOuterRadius = 0.f;

	//伤害源的中心点
	UPROPERTY(BlueprintReadWrite)
	FVector RadialDamageOrigin = FVector::ZeroVector;

在GE的实例上面设置对应的属性
在这里插入图片描述
添加对应的get和set函数
在这里插入图片描述
在这里插入图片描述
对其进行序列化,可以和服务器同步数据
在这里插入图片描述
在这里插入图片描述
在函数库里,增加对GE设置属性和获取,我们可以通过函数库的函数,传入GE实例对象进行获取和设置

	/**
	 * 获取当前GE是否为范围伤害GE
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 *
	 * @return 如果是范围伤害 返回true
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static bool IsRadialDamage(const FGameplayEffectContextHandle& EffectContextHandle);

	/**
	 * 获取当前GE 范围伤害内半径
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 *
	 * @return 返回负面效果触发间隔
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static float GetRadialDamageInnerRadius(const FGameplayEffectContextHandle& EffectContextHandle);

	/**
	 * 获取当前GE 范围伤害外半径
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 *
	 * @return 返回负面效果触发间隔
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static float GetRadialDamageOuterRadius(const FGameplayEffectContextHandle& EffectContextHandle);

	/**
	 * 获取当前GE 伤害中心点
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 *
	 * @return 攻击的击退会根据概率计算,如果有值,则为应用成功
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static FVector GetRadialDamageOrigin(const FGameplayEffectContextHandle& EffectContextHandle);
	/**
	 * 设置GE是否为范围伤害
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 * @param bInIsRadialDamage true为设置为范围伤害
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetIsRadialDamage(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, bool bInIsRadialDamage);

	/**
	 * 设置GE 范围伤害 内半径距离
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 * @param InRadialDamageInnerRadius 内半径距离 内半径内受到完整伤害
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetRadialDamageInnerRadius(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InRadialDamageInnerRadius);

	/**
	 * 设置GE 范围伤害 外半径距离
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 * @param InRadialDamageOuterRadius 外半径距离,超出此距离外的敌人将无法受到伤害
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetRadialDamageOuterRadius(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, float InRadialDamageOuterRadius);
	
	/**
	 * 设置GE伤害源的中心点
	 *
	 * @param EffectContextHandle 当前GE的上下文句柄
	 * @param InRadialDamageOrigin 伤害源的中心点
	 *
	 * @note 此属性是RPGAbilityTypes.h内自定义属性,可实现复制。
	 */
	UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
	static void SetRadialDamageOrigin(UPARAM(ref) FGameplayEffectContextHandle& EffectContextHandle, const FVector& InRadialDamageOrigin);

接着在CPP文件里实现

bool URPGAbilitySystemLibrary::IsRadialDamage(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->IsRadialDamage();
	}
	return false;
}

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

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

FVector URPGAbilitySystemLibrary::GetRadialDamageOrigin(const FGameplayEffectContextHandle& EffectContextHandle)
{
	if(const FRPGGameplayEffectContext* RPGEffectContext = static_cast<const FRPGGameplayEffectContext*>(EffectContextHandle.Get()))
	{
		return RPGEffectContext->GetRadialDamageOrigin();
	}
	return FVector::ZeroVector;
}
void URPGAbilitySystemLibrary::SetIsRadialDamage(FGameplayEffectContextHandle& EffectContextHandle, bool bInIsRadialDamage)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetIsRadialDamage(bInIsRadialDamage);
}

void URPGAbilitySystemLibrary::SetRadialDamageInnerRadius(FGameplayEffectContextHandle& EffectContextHandle, float InRadialDamageInnerRadius)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetRadialDamageInnerRadius(InRadialDamageInnerRadius);
}

void URPGAbilitySystemLibrary::SetRadialDamageOuterRadius(FGameplayEffectContextHandle& EffectContextHandle, float InRadialDamageOuterRadius)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetRadialDamageOuterRadius(InRadialDamageOuterRadius);
}

void URPGAbilitySystemLibrary::SetRadialDamageOrigin(FGameplayEffectContextHandle& EffectContextHandle, const FVector& InRadialDamageOrigin)
{
	FRPGGameplayEffectContext* RPGEffectContext = static_cast<FRPGGameplayEffectContext*>(EffectContextHandle.Get());
	RPGEffectContext->SetRadialDamageOrigin(InRadialDamageOrigin);
}

接着,我们在GE伤害类RPGDamageGameplayAbility.h,用于设置技能的相关配置

	//当前伤害类型是否为范围伤害
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	bool bIsRadialDamage = false;

	//内半径:在此半径内的所有目标都将受到完整的伤害
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float RadialDamageInnerRadius = 0.f;

	//外半径:超过这个距离的目标受到最小伤害,最小伤害如果设置为0,则圈外不受到伤害
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	float RadialDamageOuterRadius = 0.f;

	//伤害源的中心点
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Damage")
	FVector RadialDamageOrigin = FVector::ZeroVector;

然后在函数创建配置项时,添加将配置数值应用给生成的配置项上。

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;
	Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
	Params.KnockbackForceMagnitude = KnockbackForceMagnitude;
	Params.KnockbackChance = KnockbackChance;
	//如果是范围伤害,将设置对应属性
	if(bIsRadialDamage)
	{
		Params.bIsRadialDamage = bIsRadialDamage;
		Params.RadialDamageOrigin = RadialDamageOrigin;
		Params.RadialDamageInnerRadius = RadialDamageInnerRadius;
		Params.RadialDamageOuterRadius = RadialDamageOuterRadius;
	}
	return Params;
}

最后,就通过配置项,将配置项设置到GE实例上,这个我们是在函数库的函数实现的,我们增加对范围伤害属性的支持

FGameplayEffectContextHandle URPGAbilitySystemLibrary::ApplyDamageEffect(const FDamageEffectParams& DamageEffectParams)
{
	const FRPGGameplayTags& GameplayTags = FRPGGameplayTags::Get();
	const AActor* SourceAvatarActor = DamageEffectParams.SourceAbilitySystemComponent->GetAvatarActor();

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

	//设置击退相关
	SetDeathImpulse(EffectContextHandle, DamageEffectParams.DeathImpulse);
	SetKnockbackForce(EffectContextHandle, DamageEffectParams.KnockbackForce);

	//设置范围伤害相关配置
	SetIsRadialDamage(EffectContextHandle, DamageEffectParams.bIsRadialDamage);
	SetRadialDamageInnerRadius(EffectContextHandle, DamageEffectParams.RadialDamageInnerRadius);
	SetRadialDamageOuterRadius(EffectContextHandle, DamageEffectParams.RadialDamageOuterRadius);
	SetRadialDamageOrigin(EffectContextHandle, DamageEffectParams.RadialDamageOrigin);

	//根据句柄和类创建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;
}

到这里,我们实现了在技能蓝图可以配置相关属性,然后生成到配置项里,然后通过函数库将其应用到GE实例上,GE实例会将其序列化,并同步到所有的客户段和服务器上。

实现伤害的应用

相关参数有了,我们还需要实现修改伤害,将范围伤害的功能应用上去。
实现范围伤害的计算,UE的内置里实现了对应的一套,我们可以通过调用内置的函数UGameplayStatics::ApplyRadialDamageWithFalloff去实现对应的伤害计算
在这里插入图片描述
函数计算完成后,会调用TakeDamage,去实现应用到角色身上,我们可以通过增加一个委托,然后覆写TakeDamage,实现委托的广播。
在这里插入图片描述
我们在战斗接口增加一个新的委托类型,用于广播受到的伤害

DECLARE_MULTICAST_DELEGATE_OneParam(FOnDamageSignature, float /*范围伤害造成的最终数值*/); //返回范围伤害能够对自身造成的伤害,在TakeDamage里广播

并增加一个获取伤害委托的函数

	/**
	 * 获取角色受到伤害触发的委托,由于委托是创建在角色基类里的,这里可以通过添加struct来实现前向声明,不需要在头部声明一遍。
	 * @return 委托
	 */
	virtual FOnDamageSignature& GetOnDamageDelegate() = 0; 

在角色基类里创建一个对应类型的变量

FOnDamageSignature OnDamageDelegate; //传入伤害后得到结果后的委托

覆写获取委托函数

virtual FOnDamageSignature& GetOnDamageDelegate() override;

在cpp里实现函数

FOnDamageSignature& ARPGCharacterBase::GetOnDamageDelegate()
{
	return OnDamageDelegate;
}

我们接着覆写范围伤害调用的TakeDamage函数

	/**
	 * 覆写 应用伤害给自身
	 * @see https://www.unrealengine.com/blog/damage-in-ue4
	 * @param DamageAmount		要施加的伤害数值
	 * @param DamageEvent		描述伤害细节的结构体,支持不同类型的伤害,如普通伤害、点伤害(FPointDamageEvent)、范围伤害(FRadialDamageEvent)等。
	 * @param EventInstigator	负责造成伤害的 Controller,通常是玩家或 AI 的控制器。
	 * @param DamageCauser		直接造成伤害的 Actor,例如爆炸物、子弹或掉落的石头。
	 * @return					返回实际应用的伤害值。这允许目标修改或减少伤害,然后将最终的值返回。
	 */
	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;

在从父函数获取的值通过委托返回

float ARPGCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	const float DamageTaken = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	OnDamageDelegate.Broadcast(DamageTaken);
	return DamageTaken;
}

接下来,我们在计算最终应用伤害的ExecCalc_Damage.cpp里,这个是自定义计算伤害的GE类,可以自己定义获取属性,和设置影响目标的属性值。
我们在里面首先绑定委托,在匿名函数里修改造成的伤害,然后通过调用内置函数计算范围伤害造成的最终伤害,如果超出外圈范围,将不受到伤害,所以,第二个伤害只我们传入了0,

		if(URPGAbilitySystemLibrary::IsRadialDamage(EffectContextHandle))
		{
			// 1. 覆写 TakeDamage 函数,通过函数获取范围技能能够造成的最终伤害
			// 2. 创建一个委托 OnDamageDelegate, 在TakeDamage里向外广播最终伤害数值
			// 3. 在战斗接口声明一个函数用于返回委托,并在角色基类实现,在计算伤害时通过战斗接口获取到委托,并绑定匿名函数
			// 4. 调用 UGameplayStatics::ApplyRadialDamageWithFalloff 函数应用伤害,函数内会调用角色身上的TakeDamage来广播委托。
			// 5. 在匿名函数中,修改实际造成的伤害。
			
			if(ICombatInterface* CombatInterface = Cast<ICombatInterface>(TargetAvatar))
			{
				CombatInterface->GetOnDamageDelegate().AddLambda([&](float DamageAmount)
				{
					DamageTypeValue = DamageAmount;
				});
			}

			UGameplayStatics::ApplyRadialDamageWithFalloff(
				TargetAvatar,
				DamageTypeValue,
				0.f,
				URPGAbilitySystemLibrary::GetRadialDamageOrigin(EffectContextHandle),
				URPGAbilitySystemLibrary::GetRadialDamageInnerRadius(EffectContextHandle),
				URPGAbilitySystemLibrary::GetRadialDamageOuterRadius(EffectContextHandle),
				1.f,
				UDamageType::StaticClass(),
				TArray<AActor*>(),
				SourceAvatar,
				nullptr);
		}

到这里,我们实现范围伤害的应用,在计算伤害这里有点逻辑复杂,现绑定委托,然后调用函数触发委托,修改伤害值,这种相当于绕了一圈又回来了。
我比较推荐直来直去的逻辑,可以减少后期维护成本,希望有能力的同学可以实现对应的函数,直接返回值即可,没必要通过委托绕一圈。

在蓝图实现伤害的应用

我们在伤害数据资产里增加奥术爆发的伤害设置
在这里插入图片描述
然后应用给技能
在这里插入图片描述
这里,我们将不使用应用负面效果,但技能带有击飞效果,并将范围相关配置设置
在这里插入图片描述
设置完成,我们设置调试节点,来查看每次调用是否能够正确的显示内圈和外圈。
在这里插入图片描述
然后运行查看打印效果。
在这里插入图片描述
接着我们处理在应用伤害时的中心位置,在创建配置时,我们增加一个新的参数,用于可以设置目标位置

	//创建技能负面效果使用的结构体
	UFUNCTION(BlueprintPure)
	FDamageEffectParams MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor = nullptr, FVector InRadialDamageOrigin = FVector::ZeroVector);

接着修改实现,我们将击退的相关数据也移动到了此函数内,用于计算技能的击退和正确的中心。

FDamageEffectParams URPGDamageGameplayAbility::MakeDamageEffectParamsFromClassDefaults(AActor* TargetActor, FVector InRadialDamageOrigin)
{
	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;
	Params.DeathImpulseMagnitude = DeathImpulseMagnitude;
	
	//击退相关
	Params.KnockbackForceMagnitude = KnockbackForceMagnitude;
	Params.KnockbackChance = KnockbackChance;
	if(IsValid(TargetActor))
	{
		//获取到攻击对象和目标的朝向,并转换成角度
		FRotator Rotation;
		//如果设置了伤害中心,则使用中心的设置,否则采用攻击造成的
		if(InRadialDamageOrigin.IsZero())
		{
			Rotation = (TargetActor->GetActorLocation() - GetAvatarActorFromActorInfo()->GetActorLocation()).Rotation();
			Rotation.Pitch = 45.f; //设置击退角度垂直45度
		}
		else
		{
			Rotation = (TargetActor->GetActorLocation() - InRadialDamageOrigin).Rotation();
			Rotation.Pitch = 90.f; //设置为击飞效果
		}
		const FVector ToTarget = Rotation.Vector();
		Params.DeathImpulse = ToTarget * DeathImpulseMagnitude;
		//判断攻击是否触发击退
		if(FMath::RandRange(1, 100) < Params.KnockbackChance)
		{
			Params.KnockbackForce = ToTarget * KnockbackForceMagnitude;
		}
	}
	
	//如果是范围伤害,将设置对应属性
	if(bIsRadialDamage)
	{
		Params.bIsRadialDamage = bIsRadialDamage;
		Params.RadialDamageOrigin = InRadialDamageOrigin.IsZero() ? RadialDamageOrigin : InRadialDamageOrigin;
		Params.RadialDamageInnerRadius = RadialDamageInnerRadius;
		Params.RadialDamageOuterRadius = RadialDamageOuterRadius;
	}
	return Params;
}

编译代码,我们在技能蓝图里,将获取到所有技能可命中的角色,然后将结果保存为变量,防止for循环多次调用前面的函数。
在这里插入图片描述
接着for循环遍历所有的目标,创建伤害配置,并应用给目标。
在这里插入图片描述
运行查看效果
在这里插入图片描述

修改计算伤害方式

之前,我们通过委托回调的方式修改,那种方式有些反人类,这里,我们可以将所需的计算封装为一个函数,并直接返回计算后的伤害。
这里,我在函数库里增加了一个新的函数,专门用于计算范围伤害,并且保留了距离减伤和障碍物阻挡功能。

	/** 此函数为计算范围性伤害,可以根据距离和障碍物进行精准控制最终造成的伤害
	 * @param TargetActor - 需要计算攻击的目标
	 * @param BaseDamage - 在伤害内半径(DamageInnerRadius)内应用的最大伤害值。
	 * @param MinimumDamage - 在伤害外半径(DamageOuterRadius)处应用的最小伤害值。如果为0将不受伤害
	 * @param Origin - 爆炸的原点(中心位置),即伤害的起点。
	 * @param DamageInnerRadius - 全伤害半径:在该范围内的所有对象会受到最大伤害(BaseDamage)。
	 * @param DamageOuterRadius - 最小伤害半径:在该范围之外的对象只会受到**MinimumDamage**。
	 * @param DamageFalloff - 控制伤害递减的速率。值越高,伤害递减得越快。
	 * @param DamageCauser - 伤害的直接来源,如爆炸的手雷或火箭弹。
	 * @param InstigatedByController - 造成伤害的控制器,通常是执行该行为的玩家控制器。
	 * @param DamagePreventionChannel - 阻挡伤害的通道。如果某个对象阻挡了该通道上的检测,则不会对目标应用伤害(如墙壁阻挡了视线)。
	 * @return 返回对目标计算后的范围攻击应造成的伤害
	 */
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="RPGAbilitySystemLibrary|GameplayMechanics", meta=(WorldContext="WorldContextObject", AutoCreateRefTerm="IgnoreActors"))
	static float ApplyRadialDamageWithFalloff(AActor* TargetActor, float BaseDamage, float MinimumDamage, const FVector& Origin, float DamageInnerRadius, float DamageOuterRadius, 
	float DamageFalloff, AActor* DamageCauser = NULL, AController* InstigatedByController = NULL, ECollisionChannel DamagePreventionChannel = ECC_Visibility);

这个函数是从内置函数修改而来,只对单个角色进行计算,获取目标的所有碰撞组件,然后计算是否技能和目标之间是否有阻挡物,然后通过调用角色身上的TakeDamage函数获取到最终伤害并返回。

float URPGAbilitySystemLibrary::ApplyRadialDamageWithFalloff(AActor* TargetActor, float BaseDamage, float MinimumDamage, const FVector& Origin, float DamageInnerRadius,
	float DamageOuterRadius, float DamageFalloff, AActor* DamageCauser, AController* InstigatedByController, ECollisionChannel DamagePreventionChannel)
{
	// 判断目标角色是否死亡
	bool bIsDead = true;
	if(TargetActor->Implements<UCombatInterface>())
	{
		bIsDead = ICombatInterface::Execute_IsDead(TargetActor);
	}
	if(bIsDead)
	{
		return 0.f; //如果角色已经死亡,直接返回0
	}

	// 获取目标角色所有组件
	TArray<UActorComponent*> Components;
	TargetActor->GetComponents(Components);

	bool bIsDamageable = false; //判断攻击是能能够查看到目标
	TArray<FHitResult> HitList; //存储目标收到碰撞查询到的碰撞结果
	for (UActorComponent* Comp : Components)
	{
		UPrimitiveComponent* PrimitiveComp = Cast<UPrimitiveComponent>(Comp);
		if (PrimitiveComp && PrimitiveComp->IsCollisionEnabled())
		{
			FHitResult Hit;
			bIsDamageable = ComponentIsDamageableFrom(
				PrimitiveComp, Origin, DamageCauser, {}, DamagePreventionChannel, Hit
			);
			HitList.Add(Hit);
			if(bIsDamageable) break;
		}
	}

	//应用目标的伤害值
	float AppliedDamage = 0.f;

	if (bIsDamageable)
	{
		// 创建伤害事件
		FRadialDamageEvent DmgEvent;
		DmgEvent.DamageTypeClass = TSubclassOf<UDamageType>(UDamageType::StaticClass());
		DmgEvent.Origin = Origin;
		DmgEvent.Params = FRadialDamageParams(BaseDamage, MinimumDamage, DamageInnerRadius, DamageOuterRadius, DamageFalloff);
		DmgEvent.ComponentHits = HitList;
		
		// 应用伤害
		AppliedDamage = TargetActor->TakeDamage(BaseDamage, DmgEvent, InstigatedByController, DamageCauser);
	}

	return AppliedDamage;
}

ComponentIsDamageableFrom函数,是内置库里的函数,我这里直接复制出来,可以方便调用。

/** @RETURN 如果从 Origin 发出的武器射线击中了 VictimComp 组件,则返回 True。 OutHitResult 将包含击中的具体信息。 */
static bool ComponentIsDamageableFrom(UPrimitiveComponent* VictimComp, FVector const& Origin, AActor const* IgnoredActor, const TArray<AActor*>& IgnoreActors, ECollisionChannel TraceChannel, FHitResult& OutHitResult)
{
	// 配置碰撞查询参数,忽略指定的 Actor
	FCollisionQueryParams LineParams(SCENE_QUERY_STAT(ComponentIsVisibleFrom), true, IgnoredActor);
	LineParams.AddIgnoredActors( IgnoreActors );

	// 获取组件所在世界的指针
	UWorld* const World = VictimComp->GetWorld();
	check(World);

	// 使用组件的包围盒中心作为射线终点
	FVector const TraceEnd = VictimComp->Bounds.Origin;
	FVector TraceStart = Origin;
	// 如果起点和终点重合,微调起点以避免提前退出
	if (Origin == TraceEnd)
	{
		// 微调 Z 轴
		TraceStart.Z += 0.01f;
	}

	// 只有当通道合法时才执行射线检测
	if (TraceChannel != ECollisionChannel::ECC_MAX)
	{
		bool const bHadBlockingHit = World->LineTraceSingleByChannel(OutHitResult, TraceStart, TraceEnd, TraceChannel, LineParams);
		//::DrawDebugLine(World, TraceStart, TraceEnd, FLinearColor::Red, true);

		// 如果有阻挡物,检查是否为目标组件
		if (bHadBlockingHit)
		{
			if (OutHitResult.Component == VictimComp)
			{
				// 阻挡物是目标组件,返回 true
				return true;
			}
			else
			{
				// 击中其他阻挡物,记录日志并返回 false
				UE_LOG(LogDamage, Log, TEXT("Radial Damage to %s blocked by %s (%s)"), *GetNameSafe(VictimComp), *OutHitResult.GetHitObjectHandle().GetName(), *GetNameSafe(OutHitResult.Component.Get()));
				return false;
			}
		}
	}
	else
	{
		// 如果通道无效,输出警告
		UE_LOG(LogDamage, Warning, TEXT("ECollisionChannel::ECC_MAX is not valid! No falloff is added to damage"));
	}

	// 未击中任何物体,构造一个伪造的 HitResult 假设击中组件中心
	FVector const FakeHitLoc = VictimComp->GetComponentLocation();
	FVector const FakeHitNorm = (Origin - FakeHitLoc).GetSafeNormal();		// 法线指向伤害源
	OutHitResult = FHitResult(VictimComp->GetOwner(), VictimComp, FakeHitLoc, FakeHitNorm);
	return true;
}

在计算伤害时,我们只需要调用一下函数,传入所需参数,即可返回值,简单方便。
在这里插入图片描述

解决范围指示光环指针问题

我们触发技能后,范围光环默认在地面,如果指针瞄准到角色,会出现突然闪现一段位置,这是因为鼠标拾取到角色身上的位置,然后拾取到地面,水平偏移会突然闪现一段位置,为了解决这个问题,我们需要创建一个新的通道,这个通道将不会拾取场景中的角色
在这里插入图片描述
将角色身上的对此通过忽略,以及一些不必要的碰撞体也需要设置。比如角色的胶囊体,模型,武器等,还有相机上的碰撞。
在这里插入图片描述
在RPG.h文件里,增加对应通道的定义

#define CUSTOM_DEPTH_RED 250
#define ECC_PROJECTILE ECollisionChannel::ECC_GameTraceChannel1 //对投掷物响应的通道
#define ECC_TARGET_CHANNEL ECollisionChannel::ECC_GameTraceChannel2 //技能对攻击目标拾取的通道,只包含场景中的角色
#define ECC_EXCLUDEPLAYERS_CHANNEL ECollisionChannel::ECC_GameTraceChannel3 //技能范围选择时的通道,忽略场景中可动的角色

在PlayerController里,我们在鼠标拾取函数里,通过指示光环是否定义,来修改拾取使用的通道
在这里插入图片描述

展示一下运行效果
在这里插入图片描述

最后贴一下完整的技能蓝图,可以放大查看
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android OpenGL ES详解——裁剪Scissor

目录 一、概念 二、如何使用 1、开启裁剪测试 2、关闭裁剪测试 3、指定裁剪窗口&#xff08;位置和大小&#xff09; 4、裁剪应用举例 三、窗口、视⼝和裁剪区域三者区别 四、源码下载 一、概念 定义1&#xff1a; 裁剪是OpenGL中提⾼渲染的⼀种方式&#xff0c;只刷新…

内存马浅析

之前在jianshu上写了很多博客&#xff0c;但是安全相关的最近很多都被锁了。所以准备陆陆续续转到csdn来。内存马前几年一直是个很热门的漏洞攻击手段&#xff0c;因为相对于落地的木马&#xff0c;无文件攻击的内存马隐蔽性、持久性更强&#xff0c;适用的漏洞场景也更多。 J…

华为配置 之 GVRP协议

目录 简介&#xff1a; 配置GVRP&#xff1a; 总结&#xff1a; 简介&#xff1a; GVRP&#xff08;GARP VLAN Registration Protocol&#xff09;&#xff0c;称为VLAN注册协议&#xff0c;是用来维护交换机中的VLAN动态注册信息&#xff0c;并传播该信息到其他交换机中&…

62 mysql 中 存储引擎MyISAM 中索引的使用

前言 固定数据表 mysql. tables_priv 的表结构创建如下 CREATE TABLE tables_priv (Host char(60) COLLATE utf8_bin NOT NULL DEFAULT ,Db char(64) COLLATE utf8_bin NOT NULL DEFAULT ,User char(32) COLLATE utf8_bin NOT NULL DEFAULT ,Table_name char(64) COLLATE u…

局长们,今晚0点,国考抢考点!

2025国考报名确认已于11月1日0:00开始已经报完名且通过资格审核的小伙伴们一定要及时确认&#xff01; 具体流程是什么&#xff1f;操作时需要注意哪些事项&#xff1f;看完这篇就能全部搞定~ 25国考时间轴线 ✔️报名时间:10月15日8:00至10月24日18:00 ✔️审查时间:10月1…

list ------ 是一个带头双向循环的列表

结构 insert list 没有find&#xff0c;算法库有 #define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<algorithm> #include<list> using namespace std; class Pos {int _row;int _col; public:Pos(int row, int col):_row(row),_col(col){c…

【已解决】【hadoop】如何解决Hive连接MySQL元数据库的依赖问题

在启动 Hive 之前&#xff0c;通常不需要手动连接到 MySQL 数据库。Hive 的配置文件 hive-site.xml 中已经包含了连接到 MySQL 元数据库所需的信息&#xff0c;包括用户名和密码。当你启动 Hive 服务时&#xff0c;Hive 会使用这些配置信息自动连接到 MySQL 数据库。 为什么还要…

react基础之redux快速上手环境准备

文章目录 核心概念配置基础环境提交action传参异步状态操作redux调试-devtools配套工具 Redux 是一个状态管理库&#xff0c;通常与 React 一起使用&#xff0c;帮助开发者管理应用的全局状态。它的核心理念是将应用的状态存储在一个单一的、不可变的状态树中&#xff0c;并通过…

YashanDB安装及使用问题和常用总结

在YashanDB的安装和使用中总会遇到一些问题&#xff0c;有些抓耳挠腮各种查&#xff0c;在此总结下遇到和群友问到的一些问题&#xff0c;和一些常用总结 一、官方文档 先附上官方文档地址&#xff0c;给迷路的小伙伴&#xff0c;官方文档整体还是比较简介易懂的 安装部署 |…

Unreal5从入门到精通之如何解决在VR项目在头显中卡顿的问题

前言 以前我们使用Unity开发VR,Unity提供了非常便利的插件和工具来做VR。但是由于Unity的渲染效果不如Unreal,现在我们改用Unreal来做VR了,所有的VR相关的配置和操作都要重新学习。 今天就来总结一下,我在开发VR过程中碰到的所有问题。 1.编辑器,以VR运行 默认运行方式…

C#与C++交互开发系列(十四):C++中STL容器与C#集合传递的形式

前言 在跨语言开发中&#xff0c;C 的 STL 容器&#xff08;如 std::vector, std::map&#xff09;和 C# 的集合类&#xff08;如 List<T>, Dictionary<TKey, TValue>&#xff09;之间的数据传递是一个常见需求。由于两者的内存布局和实现机制不同&#xff0c;直接…

docker离线安装达梦数据库

文章目录 下载达梦数据库docker镜像上传DM8镜像文件将DM8镜像导入到本地docker镜像仓库中查看本地docker镜像仓库是否存在DM8镜像带参数启动DM8docker启动DM8默认用户名/密码 下载达梦数据库docker镜像 达梦数据库官网 https://www.dameng.com/ 点击下载中心&#xff0c;选择D…

智能合约分享

智能合约练习 一、solidity初学者经典示例代码&#xff1a; 1.存储和检索数据&#xff1a; // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // 声明 Solidity 编译器版本// 定义一个名为 SimpleStorage 的合约 contract SimpleStorage {// 声明一个公共状态变量 d…

Couldn‘t apply path mapping to the remote file.

Couldn’t apply path mapping to the remote file. /s6home2/zjw524/projects/seq2seq/code/deepnmtpycharm/deepNmt/code/deepNmtPycharm/deepNmt/model/Deep_NMT_Model.py can’t be found in project. You can continue debugging, but without the source. To fix that yo…

4.2-6 使用Hadoop WebUI

文章目录 1. 查看HDFS集群状态1.1 端口号说明1.2 用主机名访问1.3 主节点状态1.4 用IP地址访问1.5 查看数据节点 2. 操作HDFS文件系统2.1 查看HDFS文件系统2.2 在HDFS上创建目录2.3 上传文件到HDFS2.4 删除HDFS文件和目录 3. 查看YARN集群状态4. 实战总结 1. 查看HDFS集群状态 …

嵌入式硬件电子电路设计(一)开关电源Buck电路

目录 Buck电路基本结构 1. 开关闭合&#xff08;SW 闭合&#xff09; 2. 开关断开&#xff08;SW 断开&#xff09; 3. 开关控制和占空比 MP1584电路分析 其他Buck芯片的电路参考 Buck电路基本结构 下图是简化之后的BUCK电路主回路。下面分析输出电压的产生K闭合后&…

医院信息化与智能化系统(14)

医院信息化与智能化系统(14) 这里只描述对应过程&#xff0c;和可能遇到的问题及解决办法以及对应的参考链接&#xff0c;并不会直接每一步详细配置 如果你想通过文字描述或代码画流程图&#xff0c;可以试试PlantUML&#xff0c;告诉GPT你的文件结构&#xff0c;让他给你对应…

Unity 使用Netcode实现用户登录和登出

Unity之NetCode for GameObjets 基本使用 说明思路相关API代码实现Tips 说明 最近项目需要联机&#xff0c;项目方案选用Unity提供的NetCode for GameObjets&#xff08;以下简称NGO&#xff09;&#xff0c;踩了不少坑&#xff0c;本文不介绍基础使用&#xff0c;围绕双端&am…

【单机游戏】红色警戒游戏介绍和玩法

平地一声惊雷&#xff0c;金将军居然发射了洲际导弹&#xff0c;虽然我们不能亲自体验&#xff0c;但是我们可以自己在游戏中体验一把&#xff0c;今天就介绍一个很多80 90都玩过的即时战略游戏-红色警戒 https://pan.quark.cn/s/7aca45fa3dd7 红色警戒&#xff08;Red Alert …

【Python各个击破】matplotlib

导入 import matplotlib.pyplot as plt import numpy as np用法 # 根据x,y数组作图 fig, ax plt.subplots() ax.plot([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,…