上一篇,我们解决了火球术于物体碰撞的问题,现在火球术能够正确的和攻击目标产生碰撞。接下来,我们要实现火球术的伤害功能,在火球术击中目标后,给目标造成伤害。
实现伤害功能的思路是给技能一个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())
{
//为目标应用GE
if(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 properties
AProjectile();
UPROPERTY(VisibleAnywhere)
TObjectPtr<UProjectileMovementComponent> ProjectileMovement;
UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn = true)) //蓝图可读写,创建时需要将接口暴露出来方便设置
FGameplayEffectSpecHandle DamageEffectHandle;
protected:
// Called when the game starts or when spawned
virtual 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())
{
//为目标应用GE
if(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();
}