42. UE5 RPG 实现火球术伤害

上一篇,我们解决了火球术于物体碰撞的问题,现在火球术能够正确的和攻击目标产生碰撞。接下来,我们要实现火球术的伤害功能,在火球术击中目标后,给目标造成伤害。

实现伤害功能的思路是给技能一个GameplayEffect,在击中时,给目标应用GE。首先,我们在GameplayAbility(GA)身上增加一个设置GE的类,在创建火球时,在火球身上创建一个GE的实例,在火球击中目标后,使用GE的实例通过目标的ASC应用GE并造成伤害。

当然,这篇内容主要是为了测试,实际项目中药比这复杂,比如GE造成伤害时需要应用多少伤害。敌人生成时血量,防御,攻击力等等

创建GE

首先,我们创建一个新的GameplayEffect,在GE里面,我们用于测试,先不要实现复杂的伤害计算,直接固定减少20血量。

首先,我们要在火球的类里面增加一个变量,用于承载技能里面创建的GE

这里我们创建了一个GE实例的句柄,我们通过句柄可以获取到GE的实例等一些信息。

我们并将其设置为了蓝图可读写,并且在创建时,可以设置它在创建时可以设置其属性

cpp 复制代码
	UPROPERTY(BlueprintReadWrite, meta=(ExposeOnSpawn = true)) //蓝图可读写,创建时需要将接口暴露出来方便设置
	FGameplayEffectSpecHandle DamageEffectHandle;

然后在我们之前做的技能发射器类里面,我们有一项TUDO项,就是给火球添加GE,接下来我们在技能里面实现GE实例的创建。

在ProjectileSpell文件中,我们先增加一个可以设置GE的参数,只能在蓝图中设置,可以在蓝图中读写。

cpp 复制代码
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TSubclassOf<UGameplayEffect> DamageEffectClass;

然后在生成火球这里,获取到技能的ASC,通过ASC创建它的SpecHandle,并设置给我们生成的Projectile

cpp 复制代码
//创建一个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实例的实际指针,然后在前面加上*代表获取引用。

cpp 复制代码
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打印对应的数据,可以查看到底谁掉血了

cpp 复制代码
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的设置

  1. 主要属性,为设置角色基础属性
  2. 次级属性,基于主要或其他次级属性设置自身属性
  3. 至关重要的属性,主要是血量和蓝量,在角色属性设置完成,出生时将血量和蓝量填满。

    这初始化角色属性的函数现在是书写在角色基类里的,我们只需要在敌人的基类里面调用这个InitializeDefaultAttributes函数,然后在蓝图里面设置上对应的GE,就可以实现测试属性的添加。
    我们在敌人初始化ASC后,进行函数调用
cpp 复制代码
void AEnemyBase::InitAbilityActorInfo()
{
	AbilitySystemComponent->InitAbilityActorInfo(this, this);
	Cast<UAbilitySystemComponentBase>(AbilitySystemComponent)->AbilityActorInfoSet();

	//通过GE初始角色的属性
	InitializeDefaultAttributes();
}

现在准备工作完成了,接下来编译UE,我们首先在火球术的技能上,设置创建的GE

然后在敌人的蓝图上面设置初始化属性的GE,我们创建了敌人的蓝图基类,可以在基类上面直接设置,那么,它的子类都会将此作为默认值。这里也犯懒了,懒得再创建新的了,直接使用英雄测试的GE来测试。

这里可以看到,我们一共攻击了BP_Goblin_Slingshot_C_3两次,每次减少20血

下面我将技能和技能创建的火球的源码列下来

ProjectileSpell

cpp 复制代码
// 版权归暮志未晚所有。

#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;
};
cpp 复制代码
// 版权归暮志未晚所有。


#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);
	}
}
cpp 复制代码
// 版权归暮志未晚所有。

#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;
};
cpp 复制代码
// 版权归暮志未晚所有。


#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();
}
相关推荐
AA陈超2 天前
虚幻引擎UE5专用服务器游戏开发-20 添加基础能力类与连招能力
c++·游戏·ue5·游戏引擎·虚幻
平行云3 天前
如何实现UE程序大并发多集群的像素流部署
unity·ue5·图形渲染
我的巨剑能轻松搅动潮汐3 天前
UE5 C++ 下载视频到本地。//BindLambda用法解析
ue5
不爱说话的采儿4 天前
UE5详细保姆教程(第四章)
笔记·ue5·游戏引擎·课程设计
二DUAN帝4 天前
UE实现路径回放、自动驾驶功能简记
人工智能·websocket·机器学习·ue5·自动驾驶·ue4·cesiumforue
温玉琳琅4 天前
【UE5】虚幻引擎小百科
ue5·游戏引擎·虚幻
远离UE45 天前
UE 材质 变体 概念
ue5·材质
吴梓穆7 天前
UE5 重新编译插件版本
ue5
HECUgauss7 天前
UE5 使用过程遇到的问题
ue5
小白学过的代码7 天前
ue5.4和webul通信
开发语言·javascript·ue5