上一篇,我们解决了火球术于物体碰撞的问题,现在火球术能够正确的和攻击目标产生碰撞。接下来,我们要实现火球术的伤害功能,在火球术击中目标后,给目标造成伤害。
实现伤害功能的思路是给技能一个GameplayEffect,在击中时,给目标应用GE。首先,我们在GameplayAbility(GA)身上增加一个设置GE的类,在创建火球时,在火球身上创建一个GE的实例,在火球击中目标后,使用GE的实例通过目标的ASC应用GE并造成伤害。
当然,这篇内容主要是为了测试,实际项目中药比这复杂,比如GE造成伤害时需要应用多少伤害。敌人生成时血量,防御,攻击力等等
创建GE
首先,我们创建一个新的GameplayEffect,在GE里面,我们用于测试,先不要实现复杂的伤害计算,直接固定减少20血量。
首先,我们要在火球的类里面增加一个变量,用于承载技能里面创建的GE
这里我们创建了一个GE实例的句柄,我们通过句柄可以获取到GE的实例等一些信息。
我们并将其设置为了蓝图可读写,并且在创建时,可以设置它在创建时可以设置其属性
UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn = true)) //蓝图可读写,创建时需要将接口暴露出来方便设置FGameplayEffectSpecHandle DamageEffectHandle;
然后在我们之前做的技能发射器类里面,我们有一项TUDO项,就是给火球添加GE,接下来我们在技能里面实现GE实例的创建。
在ProjectileSpell文件中,我们先增加一个可以设置GE的参数,只能在蓝图中设置,可以在蓝图中读写。
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)TSubclassOf<UGameplayEffect> DamageEffectClass;
然后在生成火球这里,获取到技能的ASC,通过ASC创建它的SpecHandle,并设置给我们生成的Projectile
//创建一个GE的实例,并设置给投射物
const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());
const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), SourceASC->MakeEffectContext());
Projectile->DamageEffectHandle = SpecHandle;
然后我们回到Projectile类里面,在它的碰撞体触发重叠事件时,如果当前客户端对Projectile类有绝对控制权,将获取到目标身上的ASC,然后应用我们设置的DamageEffect。ApplyGameplayEffectSpecToSelf需要的是GE的实际引用,而DamageEffectHandle是对GE的句柄,它的Data是对GE实例的弱指针,我们通过Get()函数获取到GE实例的实际指针,然后在前面加上*代表获取引用。
void AProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{//播放击中特效PlayImpact();//在重叠后,销毁自身if(HasAuthority()){//为目标应用GEif(UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor)){TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectHandle.Data.Get());}Destroy();}else{//如果对actor没有权威性,将bHit设置为true,证明当前已经播放了击中特效bHit = true;}
}
测试效果
实际代码我们已经书写完成,接下来,我们要测试这样使用后,有没有效果。
我们打开AttributeSet属性集,在属性值变动后,使用UE_LOG打印对应的数据,可以查看到底谁掉血了
void UAttributeSetBase::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{Super::PostGameplayEffectExecute(Data);FEffectProperties Props;SetEffectProperties(Data, Props);if(Data.EvaluatedData.Attribute == GetHealthAttribute()){SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));UE_LOG(LogTemp, Warning, TEXT("%s 的生命值发生了修改,当前生命值:%f"), *Props.TargetAvatarActor->GetName(), GetHealth());}if(Data.EvaluatedData.Attribute == GetManaAttribute()){SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));}
}
接下来就是实现在敌人身上设置对应的属性值,方便我们测试,现在我们还没制作敌人的属性,正常游戏项目里面会使用数据驱动的方式,方便策划制作,不会像英雄身上的属性这样复杂,敌人的属性尤其是小怪的属性都是固定的。当时为了方便测试,我们这是使用角色的设置GE来实现敌人身上属性值的设置。
角色身上的属性值的设置是通过三个GE的设置
- 主要属性,为设置角色基础属性
- 次级属性,基于主要或其他次级属性设置自身属性
- 至关重要的属性,主要是血量和蓝量,在角色属性设置完成,出生时将血量和蓝量填满。
这初始化角色属性的函数现在是书写在角色基类里的,我们只需要在敌人的基类里面调用这个InitializeDefaultAttributes函数,然后在蓝图里面设置上对应的GE,就可以实现测试属性的添加。
我们在敌人初始化ASC后,进行函数调用
void AEnemyBase::InitAbilityActorInfo()
{AbilitySystemComponent->InitAbilityActorInfo(this, this);Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->AbilityActorInfoSet();//通过GE初始角色的属性InitializeDefaultAttributes();
}
现在准备工作完成了,接下来编译UE,我们首先在火球术的技能上,设置创建的GE
然后在敌人的蓝图上面设置初始化属性的GE,我们创建了敌人的蓝图基类,可以在基类上面直接设置,那么,它的子类都会将此作为默认值。这里也犯懒了,懒得再创建新的了,直接使用英雄测试的GE来测试。
这里可以看到,我们一共攻击了BP_Goblin_Slingshot_C_3两次,每次减少20血
下面我将技能和技能创建的火球的源码列下来
ProjectileSpell
// 版权归暮志未晚所有。#pragma once#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/GameplayAbilityBase.h"
#include "ProjectileSpell.generated.h"class AProjectile;
/*** */
UCLASS()
class AURA_API UProjectileSpell : public UGameplayAbilityBase
{GENERATED_BODY()protected:virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;UFUNCTION(BlueprintCallable, Category="Projectile")void SpawnProjectile(const FVector& ProjectileTargetLocation);UPROPERTY(EditAnywhere, BlueprintReadOnly)TSubclassOf<AProjectile> ProjectileClass;UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)TSubclassOf<UGameplayEffect> DamageEffectClass;
};
// 版权归暮志未晚所有。#include "AbilitySystem/Abilities/ProjectileSpell.h"#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
#include "Actor/Projectile.h"
#include "Interaction/CombatInterface.h"void UProjectileSpell::ActivateAbility(const FGameplayAbilitySpecHandle Handle,const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,const FGameplayEventData* TriggerEventData)
{Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}void UProjectileSpell::SpawnProjectile(const FVector& ProjectileTargetLocation)
{const bool bIsServer = GetAvatarActorFromActorInfo()->HasAuthority(); //判断此函数是否在服务器运行if (!bIsServer) return;if (ICombatInterface* CombatInterface = Cast<ICombatInterface>(GetAvatarActorFromActorInfo())){const FVector SocketLocation = CombatInterface->GetCombatSocketLocation();FRotator Rotation = (ProjectileTargetLocation - SocketLocation).Rotation(); //将方向转为旋转Rotation.Pitch = 0.f; //设置Pitch为0,转向的朝向将平行于地面FTransform SpawnTransform;SpawnTransform.SetLocation(CombatInterface->GetCombatSocketLocation());SpawnTransform.SetRotation(Rotation.Quaternion());//SpawnActorDeferred将异步创建实例,在实例创建完成时,相应的数据已经应用到了实例身上AProjectile* Projectile = GetWorld()->SpawnActorDeferred<AProjectile>(ProjectileClass,SpawnTransform,GetOwningActorFromActorInfo(),Cast<APawn>(GetOwningActorFromActorInfo()),ESpawnActorCollisionHandlingMethod::AlwaysSpawn);//创建一个GE的实例,并设置给投射物const UAbilitySystemComponent* SourceASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(GetAvatarActorFromActorInfo());const FGameplayEffectSpecHandle SpecHandle = SourceASC->MakeOutgoingSpec(DamageEffectClass, GetAbilityLevel(), SourceASC->MakeEffectContext());Projectile->DamageEffectHandle = SpecHandle;//确保变换设置被正确应用Projectile->FinishSpawning(SpawnTransform);}
}
// 版权归暮志未晚所有。#pragma once#include "CoreMinimal.h"
#include "GameplayEffectTypes.h"
#include "GameFramework/Actor.h"
#include "Projectile.generated.h"class UNiagaraSystem;
class UProjectileMovementComponent;
class USphereComponent;UCLASS()
class AURA_API AProjectile : public AActor
{GENERATED_BODY()public: // Sets default values for this actor's propertiesAProjectile();UPROPERTY(VisibleAnywhere)TObjectPtr<UProjectileMovementComponent> ProjectileMovement;UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn = true)) //蓝图可读写,创建时需要将接口暴露出来方便设置FGameplayEffectSpecHandle DamageEffectHandle;protected:// Called when the game starts or when spawnedvirtual void BeginPlay() override;virtual void Destroyed() override;UFUNCTION()void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
private://此物体的存在时间UPROPERTY(EditDefaultsOnly)float LifeSpan = 15.f;void PlayImpact() const;bool bHit;//碰撞球UPROPERTY(VisibleAnywhere)TObjectPtr<USphereComponent> Sphere;//击中粒子特效UPROPERTY(EditAnywhere)TObjectPtr<UNiagaraSystem> ImpactEffect;//击中音效UPROPERTY(EditAnywhere)TObjectPtr<USoundBase> ImpactSound;//移动循环音效UPROPERTY(EditAnywhere)TObjectPtr<USoundBase> LoopingSound;//储存循环音效的变量,后续用于删除UPROPERTY()TObjectPtr<UAudioComponent> LoopingSoundComponent;
};
// 版权归暮志未晚所有。#include "Actor/Projectile.h"#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
#include "NiagaraFunctionLibrary.h"
#include "Aura/Aura.h"
#include "Components/AudioComponent.h"
#include "Components/SphereComponent.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Kismet/GameplayStatics.h"// Sets default values
AProjectile::AProjectile()
{// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.PrimaryActorTick.bCanEverTick = false;bReplicates = true; //服务器负责计算并更新Actor的状态,然后通过网络将这些更新复制到所有连接的客户端上。//初始化碰撞体Sphere = CreateDefaultSubobject<USphereComponent>("Sphere");SetRootComponent(Sphere); //设置其为根节点,Sphere->SetCollisionObjectType(ECC_PROJECTILE); //设置发射物的碰撞类型Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly); //设置其只用作查询使用Sphere->SetCollisionResponseToChannels(ECR_Ignore); //设置其忽略所有碰撞检测Sphere->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap); //设置其与世界动态物体产生重叠事件Sphere->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Overlap); //设置其与世界静态物体产生重叠事件Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); //设置其与Pawn类型物体产生重叠事件//创建发射组件ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>("ProjectileMovement");ProjectileMovement->InitialSpeed = 550.f; //设置初始速度ProjectileMovement->MaxSpeed = 550.f; //设置最大速度ProjectileMovement->ProjectileGravityScale = 0.f; //设置重力影响因子,0为不受影响
}// Called when the game starts or when spawned
void AProjectile::BeginPlay()
{Super::BeginPlay();//设置此物体的存在时间SetLifeSpan(LifeSpan);Sphere->OnComponentBeginOverlap.AddDynamic(this, &AProjectile::OnSphereOverlap);//添加一个音效,并附加到根组件上面,在技能移动时,声音也会跟随移动LoopingSoundComponent = UGameplayStatics::SpawnSoundAttached(LoopingSound, GetRootComponent());
}void AProjectile::Destroyed()
{//如果没有权威性,并且bHit没有修改为true,证明当前没有触发Overlap事件,在销毁前播放击中特效if(!bHit && !HasAuthority()){//播放击中特效PlayImpact();}Super::Destroyed();
}void AProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{//播放击中特效PlayImpact();//在重叠后,销毁自身if(HasAuthority()){//为目标应用GEif(UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor)){TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectHandle.Data.Get());}Destroy();}else{//如果对actor没有权威性,将bHit设置为true,证明当前已经播放了击中特效bHit = true;}
}void AProjectile::PlayImpact() const
{//播放声效UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);//播放粒子特效UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());//将音乐停止后会自动销毁if(LoopingSoundComponent) LoopingSoundComponent->Stop();
}