在上一篇文章里,我们在技能蓝图里实现了通过技能实现技能指示,再次触发按键后,将通过定时器触发技能效果表现,最多支持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里,我们在鼠标拾取函数里,通过指示光环是否定义,来修改拾取使用的通道
展示一下运行效果
最后贴一下完整的技能蓝图,可以放大查看