这一篇文章我们再实现一个技能火焰爆炸,由于我们之前已经实现了三个玩家技能,这一个技能有一些总结的味道,对于创建技能相同的部分,长话短说,我们过一遍。
准备工作
我们需要一个技能类,继承于伤害技能基类
主要在里面覆写一下技能描述
接下来主要是实现
FString URPGFireBlast::GetDescriptionAtLevel(const int32 Level, const FString& Title)
{
const int32 Damage = GetDamageByDamageType(Level, FRPGGameplayTags::Get().Damage_Fire);
const float ManaCost = GetManaCost(Level);
const float Cooldown = GetCooldown(Level);
return FString::Printf(
TEXT(
// 标题
"<Title>%s</>\n"
// 细节
"<Small>等级:</> <Level>%i</>\n"
"<Small>技能冷却:</> <Cooldown>%.1f</>\n"
"<Small>蓝量消耗:</> <ManaCost>%.1f</>\n\n"//%.1f会四舍五入到小数点后一位
// 技能描述
"<Default>向四面八方发射 %i 颗火球,每颗火球会在返回时发生爆炸,并造成</> <Damage>%i</> <Default>点径向火焰伤害,并有一定几率触发燃烧。</>"),
// 动态修改值
*Title,
Level,
Cooldown,
-ManaCost,
NumFireBalls,
Damage
);
}
然后,创建一个蓝图类,用于实现技能逻辑,技能逻辑在下面再实现,我们将装配,伤害,等相关的实现。
技能需要创建两个标签,一个是技能的标识标签,通过标签可以获取到此技能,另一个就是技能对应的冷却标签,我们直接通过标签管理器创建即可。
技能的标识标签需要在代码中获取,我们需要在代码中创建
有了技能蓝图和标签,我们再技能数据资产将数据添加,后续可以通过此来获取数据,显示到ui,等。
在技能面板的技能按钮,将需要显示此技能的按钮的技能tag设置为对应技能tag
接下来,就是实现技能的伤害,冷却和消耗,对于伤害和消耗,我们需要在对应的图表CT_Damage和CT_SpellCost里增加对应的表格内容。
在技能蓝图里,设置对应的技能伤害GE,这个我们是通用的,然后引用伤害
并将标签设置
接着创建两个GE,实现冷却和消耗
冷却还是之前思路,添加标签实现
消耗去修改属性,这种也不是必须的,我们在技能里调用CommitAbility之前可以设置,或者实现一种自动计算的方式都可以。
然后设置给技能
接下来就是运行测试效果是否符合预期
为了方便后续测试,我们将技能直接放置到技能栏,首先设置技能的输入标签
然后在初始技能数组里添加对应的技能
添加火球
接下来,我们实现技能的表现效果,技能会平均角度,向角色的四面八方移动,然后移动到一定距离,自动返回并发生爆炸。实现这个效果,我们无法通过使用内置的方法,需要使用时间轴来实现效果。
基于之前的火球术的实现,我们创建一个对应的火球的子类
命名一下
在类里,我们覆写beginPlay函数,并添加一个蓝图实现
/**
* 火焰爆发使用的火球类
*/
UCLASS()
class RPG_API ARPGFireBall : public AProjectile
{
GENERATED_BODY()
public:
//执行蓝图时间轴事件,需要在蓝图中实现此事件
UFUNCTION(BlueprintImplementableEvent)
void StartOutgoingTimeline();
UPROPERTY(BlueprintReadOnly) //当前火球返回的目标角色,默认是技能的释放者,在创建火球是创建
TObjectPtr<AActor> ReturnToActor;
protected:
virtual void BeginPlay() override;
在游戏开始事件里调用时间轴事件
void ARPGFireBall::BeginPlay()
{
Super::BeginPlay();
StartOutgoingTimeline(); //调用开始时间线修改
}
编译打开UE创建一个对应的蓝图类
添加一个Niagara组件,用于显示粒子效果
设置爆炸效果和音效配置
因为我们需要使用时间轴实现,内置的自动移动的相关可以关闭
关闭帧更新
我们也不需要自动激活
在火球蓝图里,我们实现时间轴事件,首先获取到初始位置和火球移动的最远目标位置
在后面,我们首先实现火球从初始位置移动到终点位置,通过时间轴实现
时间轴里面设置了一个值,这个值采用现快后慢的方式,这样火球有一种减速的效果。值是0-1,我们可以通过插值的方式实现位置从起点到终点。
在上一个时间轴执行完成之后,我们再运行一个时间轴,这个事件轴是实现角色返回到角色的当前位置,所以,我们修改为了从终点位置向玩家角色位置插值,这里将当前玩家的位置进行了保存,方便后续使用
这个时间轴的线是先慢后快
最后,我们在返回更新时,如果火球和角色的距离小于某个阈值,那么将自我销毁,防止火球攻击自身
对于这种相乘的我们可以转换类型
这是转换成浮点类型的效果
修改技能类实现表现效果
接着,我们修改技能实现效果,我们在技能里设置一个参数,用于设置火球类
private:
UPROPERTY(EditDefaultsOnly, Category="FireBlast") //生成火球使用的类
TSubclassOf<ARPGFireBall> FireBallClass;
编译,在技能蓝图类里设置我们创建的火球蓝图类
接着,我们在技能类里创建一个生成火球的函数,这个函数里,可以生成技能所需的火球
/**
* 生成技能所需的火球
* @return NumFireBalls个数火球的数组
*/
UFUNCTION(BlueprintCallable)
TArray<ARPGFireBall*> SpawnFireBall();
在代码实现里,我们获取到角色的位置和朝向,通过之前创建的函数,获取到每个火球的旋转值,并通过旋转值创建火球,生成所需的参数。
TArray<ARPGFireBall*> URPGFireBlast::SpawnFireBall()
{
//获取到角色朝向和位置
const FVector Forward = GetAvatarActorFromActorInfo()->GetActorForwardVector();
const FVector Location = GetAvatarActorFromActorInfo()->GetActorLocation();
//通过函数获取到每个需要生成的火球的旋转
TArray<FRotator> Rotators = URPGAbilitySystemLibrary::EvenlySpacedRotators(Forward, FVector::UpVector, 360.f, NumFireBalls);
TArray<ARPGFireBall*> FireBalls; //生成所需的火球数组
for(const FRotator& Rotator : Rotators)
{
//创建变换
FTransform SpawnTransform;
SpawnTransform.SetLocation(Location);
SpawnTransform.SetRotation(Rotator.Quaternion());
//创建火球 使用 SpawnActorDeferred 来生成对象时,UE 会延迟实际的对象生成过程,这样你有机会在完全初始化对象之前进行自定义配置。
ARPGFireBall* FireBall = GetWorld()->SpawnActorDeferred<ARPGFireBall>(
FireBallClass,
SpawnTransform,
GetOwningActorFromActorInfo(),
CurrentActorInfo->PlayerController->GetPawn(),
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
//设置火球的伤害配置
FireBall->DamageEffectParams = MakeDamageEffectParamsFromClassDefaults();
FireBall->ReturnToActor = GetAvatarActorFromActorInfo();
FireBalls.Add(FireBall);
//在配置完成火球配置后,调用FinishSpawning将火球正式添加到场景中
FireBall->FinishSpawning(SpawnTransform);
}
return FireBalls;
}
然后在技能蓝图里调用函数
接下来就是运行查看效果
实现伤害
接下来,我们实现技能的伤害,伤害分为两种,一种是火球在行进当中与敌人触碰造成的伤害,另一种是在火球靠近释放者一定范围后,销毁时产生的爆炸。
我们首先实现行进中与敌人产生碰撞造成的伤害。
首先,我们配置技能的蓝图的配置,设置对应的伤害以及伤害类型。
接着,我们要在c++中,覆写OnSphereOverlap函数,实现在移动过程中造成的伤害。这里,我们基于ARPGFireBlot重写,去掉了与敌人产生重叠后的销毁火球的事件,而是应用一个伤害GE,由于火球没有伤害击退效果,这里我们去掉了相关代码。
void ARPGFireBall::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if(GetInstigator() == OtherActor) return; //判断和碰撞体接触的目标是否为自身
//如果不是敌人,将不执行后续
if(!URPGAbilitySystemLibrary::IsNotFriend(GetInstigator(), OtherActor)) return;
//目标未继承战斗接口,返回
if(!OtherActor->Implements<UCombatInterface>()) return;
//播放击中特效 当前服务器端未调用销毁时,直接播放
if(!bHit) PlayImpact();
//在重叠后,销毁自身
if(HasAuthority()) //是否为服务器,如果服务器端,将执行应用GE,并通过GE的自动同步到客户端
{
//为目标应用GE
if(UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
{
//死亡冲击的力度和方向
DamageEffectParams.DeathImpulse = GetActorForwardVector() * DamageEffectParams.DeathImpulseMagnitude;
//通过配置项应用给目标ASC
DamageEffectParams.TargetAbilitySystemComponent = TargetASC;
URPGAbilitySystemLibrary::ApplyDamageEffect(DamageEffectParams);
}
}
}
接下来则是实现火球移动结束后的爆炸效果,这个效果实现,我们需要修改伤害配置项,将其修改为范围伤害,这里采用的是增加一些修改对应配置的函数,然后在蓝图调用实现配置项的修改,然后应用给目标。
这里,我们在函数库里增加了四个函数,用于修改伤害配置项的范围伤害参数,击退,死亡时的击退以及攻击敌人的ASC的函数
/**
* 修改伤害配置项,将其设置为具有范围伤害的配置项
* @param DamageEffectParams 需要修改的配置项
* @param bIsRadial 设置是否为范围伤害
* @param InnerRadius 内半径
* @param OutRadius 外半径
* @param Origin 伤害中心
*/
UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
static void SetIsRadialDamageEffectParams(UPARAM(ref) FDamageEffectParams& DamageEffectParams, bool bIsRadial, float InnerRadius, float OutRadius, FVector Origin);
/**
* 修改伤害的冲击力的方向
* @param DamageEffectParams 需要修改的伤害配置项
* @param KnockbackDirection 攻击时触发击退的方向
*/
UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
static void SetKnockbackDirection(UPARAM(ref) FDamageEffectParams& DamageEffectParams, FVector KnockbackDirection, float Magnitude = 0.f);
/**
* 修改伤害的冲击力的方向
* @param DamageEffectParams 需要修改的伤害配置项
* @param ImpulseDirection 死亡时触发击退的方向
*/
UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
static void SetDeathImpulseDirection(UPARAM(ref) FDamageEffectParams& DamageEffectParams, FVector ImpulseDirection, float Magnitude = 0.f);
/**
* 设置伤害配置应用目标ASC
* @param DamageEffectParams 需要修改的伤害配置
* @param InASC 应用目标ASC
*/
UFUNCTION(BlueprintCallable, Category="RPGAbilitySystemLibrary|GameplayEffects")
static void SetEffectParamsTargetASC(UPARAM(ref) FDamageEffectParams& DamageEffectParams, UAbilitySystemComponent* InASC);
接着,我们实现对配置项的修改
void URPGAbilitySystemLibrary::SetIsRadialDamageEffectParams(FDamageEffectParams& DamageEffectParams, bool bIsRadial, float InnerRadius, float OutRadius, FVector Origin)
{
DamageEffectParams.bIsRadialDamage = bIsRadial;
DamageEffectParams.RadialDamageInnerRadius = InnerRadius;
DamageEffectParams.RadialDamageOuterRadius = OutRadius;
DamageEffectParams.RadialDamageOrigin = Origin;
}
void URPGAbilitySystemLibrary::SetKnockbackDirection(FDamageEffectParams& DamageEffectParams, FVector KnockbackDirection, float Magnitude)
{
KnockbackDirection.Normalize();
if(Magnitude == 0.f)
{
DamageEffectParams.KnockbackForce = KnockbackDirection * DamageEffectParams.KnockbackForceMagnitude;
}
else
{
DamageEffectParams.KnockbackForce = KnockbackDirection * Magnitude;
}
}
void URPGAbilitySystemLibrary::SetDeathImpulseDirection(FDamageEffectParams& DamageEffectParams, FVector ImpulseDirection, float Magnitude)
{
ImpulseDirection.Normalize();
if(Magnitude == 0.f)
{
DamageEffectParams.DeathImpulse = ImpulseDirection * DamageEffectParams.DeathImpulseMagnitude;
}
else
{
DamageEffectParams.DeathImpulse = ImpulseDirection * Magnitude;
}
}
void URPGAbilitySystemLibrary::SetEffectParamsTargetASC(FDamageEffectParams& DamageEffectParams, UAbilitySystemComponent* InASC)
{
DamageEffectParams.TargetAbilitySystemComponent = InASC;
}
接着,我们编译代码,打开火球蓝图,在触发火球爆炸后,实现对应逻辑
我们将后面的逻辑封装为了一个函数,这个函数内主要逻辑是设置技能造成的范围伤害相关参数,以及获取到需要应用伤害的目标,然后调用函数应用,销毁自身。
应用伤害的函数看似很长,其实主要就是遍历伤害数组,然后修改应用目标的ASC,以及击退相关参数,这里对每个目标的值都不一样。最后调用函数库函数应用即可。
修改爆炸使用GameplayCue实现
我们实现了伤害,这里再修改爆炸时的爆炸效果,我们采用通过c++调用,然后使用GameplayCue实现。
我们在c++里增加一个新的标签,这个火焰爆炸的GameplayCue可以通过此标签调用
FGameplayTag GameplayCue_FireBlast; //火焰爆炸火球爆炸时的表现效果
GameplayTags.GameplayCue_FireBlast = UGameplayTagsManager::Get()
.AddNativeGameplayTag(
FName("GameplayCue.FireBlast"),
FString("火焰爆炸表现效果标签")
);
我们需要覆写RPGFireBall的PlayImpact函数
virtual void PlayImpact() override;
实现这里,我们将通过UGameplayCueManager去调用GameplayCue,而不是之前的方式。
void ARPGFireBall::PlayImpact()
{
if(GetOwner())
{
//设置GameplayCue播放位置
FGameplayCueParameters Parameters;
Parameters.Location = GetActorLocation();
UGameplayCueManager::ExecuteGameplayCue_NonReplicated(GetOwner(), FRPGGameplayTags::Get().GameplayCue_FireBlast, Parameters);
}
//将音乐停止后会自动销毁
if(LoopingSoundComponent)
{
//循环组件暂停并销毁
LoopingSoundComponent->Stop();
LoopingSoundComponent->DestroyComponent();
}
bHit = true;
}
接着编译打开蓝图,创建一个对应的GameplayCue
设置应用标签
然后设置粒子系统和爆炸音效。