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

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

在这一篇里,我们将实现技能播放时,对目标敌人应用技能伤害。

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

添加范围伤害属性

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

在RPGAbilityTypes.h中,伤害技能生成的配置项里,添加对应的参数

cpp 复制代码
	//当前伤害类型是否为范围伤害
	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实例对象进行获取和设置

cpp 复制代码
	/**
	 * 获取当前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);
cpp 复制代码
	/**
	 * 设置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文件里实现

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;
}
cpp 复制代码
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,用于设置技能的相关配置

cpp 复制代码
	//当前伤害类型是否为范围伤害
	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;

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

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;
	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实例上,这个我们是在函数库的函数实现的,我们增加对范围伤害属性的支持

cpp 复制代码
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,实现委托的广播。

我们在战斗接口增加一个新的委托类型,用于广播受到的伤害

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

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

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

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

cpp 复制代码
FOnDamageSignature OnDamageDelegate; //传入伤害后得到结果后的委托

覆写获取委托函数

cpp 复制代码
virtual FOnDamageSignature& GetOnDamageDelegate() override;

在cpp里实现函数

cpp 复制代码
FOnDamageSignature& ARPGCharacterBase::GetOnDamageDelegate()
{
	return OnDamageDelegate;
}

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

cpp 复制代码
	/**
	 * 覆写 应用伤害给自身
	 * @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;

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

cpp 复制代码
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,

cpp 复制代码
		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);
		}

到这里,我们实现范围伤害的应用,在计算伤害这里有点逻辑复杂,现绑定委托,然后调用函数触发委托,修改伤害值,这种相当于绕了一圈又回来了。

我比较推荐直来直去的逻辑,可以减少后期维护成本,希望有能力的同学可以实现对应的函数,直接返回值即可,没必要通过委托绕一圈。

在蓝图实现伤害的应用

我们在伤害数据资产里增加奥术爆发的伤害设置

然后应用给技能

这里,我们将不使用应用负面效果,但技能带有击飞效果,并将范围相关配置设置

设置完成,我们设置调试节点,来查看每次调用是否能够正确的显示内圈和外圈。

然后运行查看打印效果。

接着我们处理在应用伤害时的中心位置,在创建配置时,我们增加一个新的参数,用于可以设置目标位置

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

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

cpp 复制代码
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循环遍历所有的目标,创建伤害配置,并应用给目标。

运行查看效果

修改计算伤害方式

之前,我们通过委托回调的方式修改,那种方式有些反人类,这里,我们可以将所需的计算封装为一个函数,并直接返回计算后的伤害。

这里,我在函数库里增加了一个新的函数,专门用于计算范围伤害,并且保留了距离减伤和障碍物阻挡功能。

cpp 复制代码
	/** 此函数为计算范围性伤害,可以根据距离和障碍物进行精准控制最终造成的伤害
	 * @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函数获取到最终伤害并返回。

cpp 复制代码
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函数,是内置库里的函数,我这里直接复制出来,可以方便调用。

cpp 复制代码
/** @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文件里,增加对应通道的定义

cpp 复制代码
#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里,我们在鼠标拾取函数里,通过指示光环是否定义,来修改拾取使用的通道

展示一下运行效果

最后贴一下完整的技能蓝图,可以放大查看

相关推荐
小魏冬琅6 分钟前
探索面向对象的高级特性与设计模式(2/5)
java·开发语言
TT哇21 分钟前
【Java】数组的定义与使用
java·开发语言·笔记
look_outs38 分钟前
JavaSE笔记2】面向对象
java·开发语言
武子康42 分钟前
大数据-191 Elasticsearch - ES 集群模式 配置启动 规划调优
java·大数据·elk·elasticsearch·搜索引擎·全文检索
A_aspectJ1 小时前
‌Spring MVC的主要组件有哪些?
java·spring·mvc
大耳猫1 小时前
Android gradle和maven国内镜像地址
android·gradle·maven
塔塔开!.1 小时前
Maven的依赖
java·maven
liuyang-neu1 小时前
力扣第420周赛 中等 3324. 出现在屏幕上的字符串序列
java·算法·leetcode
划]破1 小时前
Maven的安装及配置
java·maven
讓丄帝愛伱1 小时前
dependencyManagement保持maven的多模块依赖版本一致
java·maven