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,它将不受重力影响,并且准确向目标攻击。

相关推荐
axin7c829 分钟前
UnLua访问C++属性、动态生成Actor
ue5
axin7c835 分钟前
UnLua扩展C++函数和蓝图自定义事件
ue5
Reese_Cool1 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
平凡シンプル2 小时前
安卓 uniapp跨端开发
android·uni-app
elina80132 小时前
安卓实现导入Excel文件
android·excel
严文文-Chris2 小时前
【设计模式-享元】
android·java·设计模式
趋势大仙2 小时前
SQLiteDatabase insert or replace数据不生效
android·数据库
DS小龙哥2 小时前
QT For Android开发-打开PPT文件
android·qt·powerpoint
试行3 小时前
Android实现自定义下拉列表绑定数据
android·java
Dingdangr8 小时前
Android中的Intent的作用
android