从这篇开始,我们将实现一些技能,比如多段火球术,闪电链等等。
在这一篇里,我们先实现多段火球术,技能可以通过配置发射出多个火球术进行攻击。
创建多段火球函数
首先在我们之前创建的RPGFireBolt.h类里面增加一个生成多段火球的函数,使用之前的配置。
然后可以设置最大火球数量以及最大攻击角度
cpp
UFUNCTION(BlueprintCallable, Category="Projectile")
void SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, const FName SocketName, const bool bOverridePitch = false, const float PitchOverride = 0.f, AActor* HomingTarget = nullptr);
protected:
UPROPERTY(EditDefaultsOnly, Category="FireBolt")
float ProjectileSpread = 90.f; //攻击角度
UPROPERTY(EditDefaultsOnly, Category="FireBolt")
int32 MaxNumProjectiles = 5; //最大生成火球数量
然后在实现里,我们通过等级和最大火球数量取最小值,如果是1级,就只能发射一个火球。那么,还是按之前默认的发射单个技能的函数去实现。
如果数量大于1,那么,我们需要计算多段,然后在这一段角度里,获取到中间角度,生成一段火球。
具体逻辑,就是获取到每一段的角度,然后,获取到角色最左侧的角度,根据最左侧开始递归,生成每一个火球。
cpp
void URPGFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, const FName SocketName, const bool bOverridePitch, const float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority(); //判断此函数是否在服务器运行
if (!bIsServer) return;
if (GetAvatarActorFromActorInfo()->Implements<UCombatInterface>())
{
//限制产生火球的最大数量
NumProjectiles = FMath::Min(MaxNumProjectiles, GetAbilityLevel());
//根据可生成数量进行逻辑判断
if(NumProjectiles > 1)
{
//获取释放位置
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocationByTag(GetAvatarActorFromActorInfo(), SocketTag, SocketName);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation(); //将方向转为旋转
if(bOverridePitch) Rotation.Pitch = PitchOverride; //覆写发射角度
const float DeltaSpread = ProjectileSpread / NumProjectiles; //技能分的段数
const FVector LeftOfSpread = Rotation.Vector().RotateAngleAxis(-ProjectileSpread / 2.f, FVector::UpVector); //获取到最左侧的角度
for(int32 i = 0; i<NumProjectiles; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * (i + 0.5f), FVector::UpVector); //获取当前分段的角度
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
SpawnTransform.SetRotation(Direction.Rotation().Quaternion());
//SpawnActorDeferred将异步创建实例,在实例创建完成时,相应的数据已经应用到了实例身上
AProjectile* Projectile = GetWorld()->SpawnActorDeferred<AProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetAvatarActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
//确保变换设置被正确应用
Projectile->FinishSpawning(SpawnTransform);
UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), SocketLocation, SocketLocation + Direction * 100.f, 5, FLinearColor::Green, 120, 5);
}
}
else
{
SpawnProjectile(ProjectileTargetLocation, SocketTag, SocketName, bOverridePitch, PitchOverride);
}
}
}
编译打开蓝图,在蓝图里,我们使用新创建的函数来实现火球术的生成。
将技能生成5级,查看效果
然后我们修改角度,查看不一样的效果。
实现分段函数
由于在一定角度范围内,平均角度,获取多个角度的函数通用性比较高,所以我们将在函数库里增加两个函数,用于生成多段角度和多段向量。
所以,我们创建两个函数,用于获取相应内容,这里我将函数库里所有的函数都添加了对应的注释,方便查看,如果有需要的理解的朋友可以在文章底部加群里了解更多。
cpp
/**
* 这个函数根据传入的值计算均匀分布的多段角度,
*
* @param Forward 正前方向
* @param Axis 基于旋转的轴
* @param Spread 角度范围
* @param NumRotators 分段数
*
* @return TArray<FRotator&> 返回每段角度的中间角度的数组
*
* @note 这个函数用于在技能生成投掷物的函数逻辑中。
*/
UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayMechanics")
static TArray<FRotator> EvenlySpacedRotators(const FVector& Forward, const FVector & Axis, float Spread, int32 NumRotators);
/**
* 这个函数根据传入的值计算均匀分布的多段朝向
*
* @param Forward 正前方向
* @param Axis 基于旋转的轴
* @param Spread 角度范围
* @param NumVectors 分段数
*
* @return TArray<FVector&> 返回每段角度的中间角度的朝向数组
*
* @note 这个函数用于在技能生成投掷物的函数逻辑中。
*/
UFUNCTION(BlueprintPure, Category="RPGAbilitySystemLibrary|GameplayMechanics")
static TArray<FVector> EvenlyRotatedVectors(const FVector& Forward, const FVector & Axis, float Spread, int32 NumVectors);
实现这里,也没什么好说的,就是将一部分逻辑抽离出来,这两个函数区别就是一个返回的是旋转角度,另一个是返回的朝向向量。
cpp
TArray<FRotator> URPGAbilitySystemLibrary::EvenlySpacedRotators(const FVector& Forward, const FVector& Axis, float Spread, int32 NumRotators)
{
TArray<FRotator> Rotators;
const FVector LeftOfSpread = Forward.RotateAngleAxis(-Spread / 2.f, Axis); //获取到最左侧的角度
if(NumRotators > 1)
{
const float DeltaSpread = Spread / NumRotators; //技能分的段数
for(int32 i=0; i<NumRotators; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * (i + 0.5f), Axis); //获取当前分段的角度
Rotators.Add(Direction.Rotation());
}
}
else
{
//如果只需要一个,则将朝向放入即可
Rotators.Add(Forward.Rotation());
}
return Rotators;
}
TArray<FVector> URPGAbilitySystemLibrary::EvenlyRotatedVectors(const FVector& Forward, const FVector& Axis, float Spread, int32 NumVectors)
{
TArray<FVector> Vectors;
const FVector LeftOfSpread = Forward.RotateAngleAxis(-Spread / 2.f, Axis); //获取到最左侧的角度
if(NumVectors > 1)
{
const float DeltaSpread = Spread / NumVectors; //技能分的段数
for(int32 i=0; i<NumVectors; i++)
{
const FVector Direction = LeftOfSpread.RotateAngleAxis(DeltaSpread * (i + 0.5f), Axis); //获取当前分段的角度
Vectors.Add(Direction);
}
}
else
{
//如果只需要一个,则将朝向放入即可
Vectors.Add(Forward);
}
return Vectors;
}
实现了对应的函数后,我们修改生成多段火球术的代码,将生成内容修改为通过调用函数库的方法获取多段角度,并生成火球。
cpp
void URPGFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, const FName SocketName, const bool bOverridePitch, const float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority(); //判断此函数是否在服务器运行
if (!bIsServer) return;
if (GetAvatarActorFromActorInfo()->Implements<UCombatInterface>())
{
//限制产生火球的最大数量
NumProjectiles = FMath::Min(MaxNumProjectiles, GetAbilityLevel());
//获取释放位置
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocationByTag(GetAvatarActorFromActorInfo(), SocketTag, SocketName);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation(); //将方向转为旋转
if(bOverridePitch) Rotation.Pitch = PitchOverride; //覆写发射角度
const FVector Forward = Rotation.Vector(); //获取朝向向量
//根据函数获取到所有生成的转向
TArray<FRotator> Rotations = URPGAbilitySystemLibrary::EvenlySpacedRotators(Forward, FVector::UpVector, ProjectileSpread, NumProjectiles);
//遍历所有朝向,并生成火球术
for(FRotator& Rot : Rotations)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
SpawnTransform.SetRotation(Rot.Quaternion());
//SpawnActorDeferred将异步创建实例,在实例创建完成时,相应的数据已经应用到了实例身上
AProjectile* Projectile = GetWorld()->SpawnActorDeferred<AProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetAvatarActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
//确保变换设置被正确应用
Projectile->FinishSpawning(SpawnTransform);
//Debug
//UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), SocketLocation, SocketLocation + Rot.Vector() * 100.f, 5, FLinearColor::Green, 120, 5);
}
}
}
实现飞弹跟随目标
在上面,我们实现了释放技能可以一次性生成多个火球去攻击敌人,但是现在有一个问题,就是生成的火球术是一种扩散的方式向外射出,无法准确的攻击到敌人,所以,我们需要实现给生成的飞弹设置攻击目标,并且飞弹可以朝向目标飞行。
实现这个效果,我们需要使用到ProjectileMovement->HomingTargetComponent组件,可以给飞弹的发射组件设置攻击目标。接下来,我们将实现这个功能。
首先,我们在火球术技能类里增加三个参数,用于设置朝向目标移动时的最大速度和最小速度,火球术将在最大值和最小值中随机一个值来设置,并添加一个技能是否需要朝向目标移动的布尔,这些值都可以在技能蓝图中配置
cpp
UPROPERTY(EditDefaultsOnly, Category="FireBolt")
float HomingAccelerationMin = 1600.f; //移动朝向目标的最小加速度
UPROPERTY(EditDefaultsOnly, Category="FireBolt")
float HomingAccelerationMax = 3200.f; //移动朝向目标的最大加速度
UPROPERTY(EditDefaultsOnly, Category="FireBolt")
bool bLaunchHomingProjectiles = true; //设置生成的飞弹是否需要朝向目标飞行
接下来,我们在飞弹类里增加一个场景组件,这个组件在无法找到攻击目标时,我们也能够实现它能够朝向目标位置飞行,并且这个值在飞弹被销毁时,也能够被正确的垃圾回收(ProjectileMovement->HomingTargetComponent是弱引用,ProjectileMovement销毁时,不会去销毁HomingTargetComponent)。
cpp
UPROPERTY() //一个场景组件,用于确定当前投掷物的攻击目标(在没有默认目标时,有默认目标直接设置目标的根组件)
TObjectPtr<USceneComponent> HomingTargetSceneComponent;
接下来,我们在生成多重飞弹的函数里,增加对攻击目标的设置,如果目标继承战斗接口,我们直接获取它的根组件设置给HomingTargetComponent ,如果没有,我们就创建一个,并将目标位置应用。
然后设置朝向目标的加速度,和开启朝向目标移动变量。
cpp
//根据目标类型设置HomingTargetComponent,此内容必须在飞弹被生成后设置
if(HomingTarget && HomingTarget->Implements<UCombatInterface>())
{
//设置攻击的位置为攻击对象的根位置
Projectile->ProjectileMovement->HomingTargetComponent = HomingTarget->GetRootComponent();
}
else
{
//如果没有获取到攻击目标,则创建一个可销毁的并应用
Projectile->HomingTargetSceneComponent = NewObject<USceneComponent>(USceneComponent::StaticClass());
Projectile->HomingTargetSceneComponent->SetWorldLocation(ProjectileTargetLocation); //设置组件位置
Projectile->ProjectileMovement->HomingTargetComponent = Projectile->HomingTargetSceneComponent;
}
//设置飞弹朝向目标时的加速度
Projectile->ProjectileMovement->HomingAccelerationMagnitude = FMath::FRandRange(HomingAccelerationMin, HomingAccelerationMax);
Projectile->ProjectileMovement->bIsHomingProjectile = bLaunchHomingProjectiles; //设置为true,飞弹将加速飞向攻击目标
完整代码如下
cpp
void URPGFireBolt::SpawnProjectiles(const FVector& ProjectileTargetLocation, const FGameplayTag& SocketTag, const FName SocketName, const bool bOverridePitch, const float PitchOverride, AActor* HomingTarget)
{
const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority(); //判断此函数是否在服务器运行
if (!bIsServer) return;
if (GetAvatarActorFromActorInfo()->Implements<UCombatInterface>())
{
//限制产生火球的最大数量
NumProjectiles = FMath::Min(MaxNumProjectiles, GetAbilityLevel());
//获取释放位置
const FVector SocketLocation = ICombatInterface::Execute_GetCombatSocketLocationByTag(GetAvatarActorFromActorInfo(), SocketTag, SocketName);
FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation(); //将方向转为旋转
if(bOverridePitch) Rotation.Pitch = PitchOverride; //覆写发射角度
const FVector Forward = Rotation.Vector(); //获取朝向向量
//根据函数获取到所有生成的转向
TArray<FRotator> Rotations = URPGAbilitySystemLibrary::EvenlySpacedRotators(Forward, FVector::UpVector, ProjectileSpread, NumProjectiles);
//遍历所有朝向,并生成火球术
for(FRotator& Rot : Rotations)
{
FTransform SpawnTransform;
SpawnTransform.SetLocation(SocketLocation);
SpawnTransform.SetRotation(Rot.Quaternion());
//SpawnActorDeferred将异步创建实例,在实例创建完成时,相应的数据已经应用到了实例身上
AProjectile* Projectile = GetWorld()->SpawnActorDeferred<AProjectile>(
ProjectileClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
Cast<APawn>(GetAvatarActorFromActorInfo()),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
Projectile->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
//根据目标类型设置HomingTargetComponent,此内容必须在飞弹被生成后设置
if(HomingTarget && HomingTarget->Implements<UCombatInterface>())
{
//设置攻击的位置为攻击对象的根位置
Projectile->ProjectileMovement->HomingTargetComponent = HomingTarget->GetRootComponent();
}
else
{
//如果没有获取到攻击目标,则创建一个可销毁的并应用
Projectile->HomingTargetSceneComponent = NewObject<USceneComponent>(USceneComponent::StaticClass());
Projectile->HomingTargetSceneComponent->SetWorldLocation(ProjectileTargetLocation); //设置组件位置
Projectile->ProjectileMovement->HomingTargetComponent = Projectile->HomingTargetSceneComponent;
}
//设置飞弹朝向目标时的加速度
Projectile->ProjectileMovement->HomingAccelerationMagnitude = FMath::FRandRange(HomingAccelerationMin, HomingAccelerationMax);
Projectile->ProjectileMovement->bIsHomingProjectile = bLaunchHomingProjectiles; //设置为true,飞弹将加速飞向攻击目标
//确保变换设置被正确应用
Projectile->FinishSpawning(SpawnTransform);
//Debug
//UKismetSystemLibrary::DrawDebugArrow(GetAvatarActorFromActorInfo(), SocketLocation, SocketLocation + Rot.Vector() * 100.f, 5, FLinearColor::Green, 120, 5);
}
}
}
完成以后,我们还需要去修改技能蓝图的逻辑,设置飞弹移动的目标。
我们修改蓝图,将鼠标拾取到的目标Actor保存为变量,在生成飞弹时,将变量传入。
我们现在可以覆写发射垂直角度,让飞弹先朝某个角度飞行,然后再朝向目标飞行
在飞弹蓝图里,我们可以修改它的初始速度和最大速度,是否受重力影响来实现不同的效果。
如果你想让飞弹能够准确的朝向目标飞行,那么将发射物的重力范围设置为0,它将不受重力影响,并且准确向目标攻击。