95. UE5 GAS RPG 实现创建多段飞弹攻击敌人

从这篇开始,我们将实现一些技能,比如多段火球术,闪电链等等。

在这一篇里,我们先实现多段火球术,技能可以通过配置发射出多个火球术进行攻击。

创建多段火球函数

首先在我们之前创建的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,它将不受重力影响,并且准确向目标攻击。

相关推荐
m0_597345316 分钟前
【Android】安卓四大组件之广播接收器(Broadcast Receiver):从基础到进阶
android·java·boradcast·安卓四大组件
whysqwhw29 分钟前
OkHttp PublicSuffix包的后缀列表处理
android
yeziyfx1 小时前
kotlin中集合的用法
android·开发语言·kotlin
EngZegNgi3 小时前
安卓应用启动崩溃的问题排查记录
android·crash·启动崩溃
火柴就是我3 小时前
每日见闻之Container Decoration
android·flutter
天枢破军3 小时前
【AOSP】解决repo拉取提示无法连接android.googlesource.com
android
whysqwhw3 小时前
OkHttp之AndroidPlatform类分析
android
XiaolongTu4 小时前
Kotlin Flow详述:从一个“卡顿”问题到线程切换的本质
android·面试
Kapaseker4 小时前
全网最详细的Compose Stable讲解,你一定要看
android
solo_994 小时前
使用Android Studio 聊微信
android