文章目录
- 创建激光GA
-
- 添加技能标签
- 初步实现激光技能
- 给激光技能配置GE
- 配置动画
- 添加新的技能对应到角色以及资产的配置
- 添加新的TA`TargetActor_Line`
- 将激光TA添加到激光GA上
- 修改一点杂事
- 补充初始化金币的GE
创建激光GA
添加新技能激光技能GA_Laser
(不用管下图的打错,我也是后知后觉)
添加技能标签
cpp
// 激光
CRUNCH_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Ability_Laser_Shoot)
CRUNCH_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Ability_Laser_Cooldown)
cpp
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Ability_Laser_Shoot, "Ability.Laser.Shoot", "激光技能")
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Ability_Laser_Cooldown, "Ability.Laser.Cooldown", "激光技能冷却")
初步实现激光技能
cpp
#pragma once
#include "CoreMinimal.h"
#include "GAS/Core/CGameplayAbility.h"
#include "GA_Laser.generated.h"
/**
* 激光类技能:
* - 持续施法技能,消耗法力值维持激光
* - 对路径上的目标造成持续伤害和击退效果
*/
UCLASS()
class CRUNCH_API UGA_Laser : public UCGameplayAbility
{
GENERATED_BODY()
public:
// 技能激活时调用
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
// 技能结束时调用
virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;
private:
// 目标检测范围(厘米)
UPROPERTY(EditDefaultsOnly, Category = "Targeting")
float TargetRange = 4000;
// 激光检测半径(厘米)
UPROPERTY(EditDefaultsOnly, Category = "Targeting")
float DetectionCylinderRadius = 30.f;
// 目标检测频率(秒)
UPROPERTY(EditDefaultsOnly, Category = "Targeting")
float TargetingInterval = 0.3f;
// 命中时应用的伤害效果
UPROPERTY(EditDefaultsOnly, Category = "Effects")
FGenericDamageEffectDef HitDamageEffect;
// 持续施法时的法力消耗效果
UPROPERTY(EditDefaultsOnly, Category = "Effects")
TSubclassOf<UGameplayEffect> OnGoingConsumptionEffect;
// 持续消耗效果的激活句柄
FActiveGameplayEffectHandle OnGoingConsumptionEffectHandle;
// 激光技能动画蒙太奇
UPROPERTY(EditDefaultsOnly, Category = "Anim")
TObjectPtr<UAnimMontage> LaserMontage;
// 目标Actor附加的骨骼名称
UPROPERTY(EditDefaultsOnly, Category = "Targeting")
FName TargetActorAttachSocketName = "Laser";
// 动画事件触发的射击回调
UFUNCTION()
void ShootLaser(FGameplayEventData Payload);
// 法力值更新回调(用于检测法力不足)
void ManaUpdated(const FOnAttributeChangeData& ChangeData);
};
cpp
#include "GA_Laser.h"
#include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"
#include "Abilities/Tasks/AbilityTask_WaitGameplayEvent.h"
#include "Abilities/Tasks/AbilityTask_WaitCancel.h"
#include "Abilities/Tasks/AbilityTask_WaitTargetData.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
#include "GAS/Core/CAttributeSet.h"
void UGA_Laser::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
if (!K2_CommitAbility() || !LaserMontage)
{
K2_EndAbility();
return;
}
// 仅在服务器或预测有效时执行
if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
{
// 播放激光蒙太奇
UAbilityTask_PlayMontageAndWait* PlayerLaserMontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, NAME_None, LaserMontage);
PlayerLaserMontageTask->OnBlendOut.AddDynamic(this, &UGA_Laser::K2_EndAbility);
PlayerLaserMontageTask->OnCancelled.AddDynamic(this, &UGA_Laser::K2_EndAbility);
PlayerLaserMontageTask->OnInterrupted.AddDynamic(this, &UGA_Laser::K2_EndAbility);
PlayerLaserMontageTask->OnCompleted.AddDynamic(this, &UGA_Laser::K2_EndAbility);
PlayerLaserMontageTask->ReadyForActivation();
// 等待动画事件触发激光发射
UAbilityTask_WaitGameplayEvent* WaitShootEvent = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(this, TGameplayTags::Ability_Laser_Shoot);
WaitShootEvent->EventReceived.AddDynamic(this, &UGA_Laser::ShootLaser);
WaitShootEvent->ReadyForActivation();
// 设置技能取消监听
UAbilityTask_WaitCancel* WaitCancel = UAbilityTask_WaitCancel::WaitCancel(this);
WaitCancel->OnCancel.AddDynamic(this, &UGA_Laser::K2_EndAbility);
WaitCancel->ReadyForActivation();
}
}
void UGA_Laser::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
// 移除持续消耗法力的效果
UAbilitySystemComponent* OwnerAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
if (OwnerAbilitySystemComponent && OnGoingConsumptionEffectHandle.IsValid())
{
OwnerAbilitySystemComponent->RemoveActiveGameplayEffect(OnGoingConsumptionEffectHandle);
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
void UGA_Laser::ShootLaser(FGameplayEventData Payload)
{
// --- 服务器端逻辑 ---
if (K2_HasAuthority())
{
// 1. 应用持续消耗法力的效果
OnGoingConsumptionEffectHandle = BP_ApplyGameplayEffectToOwner(OnGoingConsumptionEffect, GetAbilityLevel(CurrentSpecHandle, CurrentActorInfo));
// 2. 注册法力变化回调(检测法力不足)
UAbilitySystemComponent* OwnerAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
if (OwnerAbilitySystemComponent)
{
OwnerAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UCAttributeSet::GetManaAttribute()).AddUObject(this, &UGA_Laser::ManaUpdated);
}
}
}
void UGA_Laser::ManaUpdated(const FOnAttributeChangeData& ChangeData)
{
// 当法力不足时自动结束技能
UAbilitySystemComponent* OwnerASC = GetAbilitySystemComponentFromActorInfo() ;
if (OwnerASC && !OwnerASC->CanApplyAttributeModifiers(
OnGoingConsumptionEffect.GetDefaultObject(),
GetAbilityLevel(CurrentSpecHandle, CurrentActorInfo),
MakeEffectContext(CurrentSpecHandle, CurrentActorInfo)))
{
K2_EndAbility();
}
}
给激光技能配置GE
创建多个GE
伤害GE
眩晕GE
持续射击的蓝耗GE
冷却GE(使用我之前的mmc的话)
不用的话就使用可浮动点数
首次提交的蓝耗
继承激光GA创建蓝图版本

配置动画
蒙太奇中添加激光的动画通知
添加新的技能对应到角色以及资产的配置
创建一个插槽Laser
用来释放激光的起点
角色类中添加技能
UI数据中添加技能
添加新的TATargetActor_Line
用于设置激光以及检测敌人发送需要造成伤害的敌人
添加一下奶瓜粒子特效
cpp
#pragma once
#include "CoreMinimal.h"
#include "GenericTeamAgentInterface.h"
#include "Abilities/GameplayAbilityTargetActor.h"
#include "Components/SphereComponent.h"
#include "TargetActor_Line.generated.h"
class UNiagaraComponent;
/**
* 线性目标检测器
* 用于技能/能力的线性范围目标检测与反馈
*/
UCLASS()
class CRUNCH_API ATargetActor_Line : public AGameplayAbilityTargetActor, public IGenericTeamAgentInterface
{
GENERATED_BODY()
public:
ATargetActor_Line();
/**
* 配置目标检测参数
* @param NewTargetRange 检测距离
* @param NewDetectionCylinderRadius 检测半径
* @param NewTargetingInterval 检测频率 /s
* @param OwnerTeamId 拥有者队伍ID
* @param bShouldDrawDebug 是否绘制调试信息
*/
void ConfigureTargetSetting(
float NewTargetRange,
float NewDetectionCylinderRadius,
float NewTargetingInterval,
FGenericTeamId OwnerTeamId,
bool bShouldDrawDebug
);
// 设置队伍ID
virtual void SetGenericTeamId(const FGenericTeamId& NewTeamID) override;
// 获取队伍ID
FORCEINLINE virtual FGenericTeamId GetGenericTeamId() const override { return TeamId; }
// 网络属性同步
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 开始目标检测
virtual void StartTargeting(UGameplayAbility* Ability) override;
// 每帧调用,处理目标检测和特效更新
virtual void Tick(float DeltaTime) override;
// 销毁时回调
virtual void BeginDestroy() override;
private:
// 检测射程
UPROPERTY(Replicated)
float TargetRange;
// 检测圆柱体半径
UPROPERTY(Replicated)
float DetectionCylinderRadius;
// 检测间隔(秒)
UPROPERTY()
float TargetingInterval;
// 所属队伍ID
UPROPERTY(Replicated)
FGenericTeamId TeamId;
// 是否绘制调试信息
UPROPERTY()
bool bDrawDebug;
// 技能拥有者Actor
UPROPERTY(Replicated)
const AActor* AvatarActor;
// 激光特效长度参数名
UPROPERTY(EditDefaultsOnly, Category = "VFX")
FName LaserFXLengthParamName = "Length";
// 根组件
UPROPERTY(VisibleDefaultsOnly, Category = "Component")
TObjectPtr<USceneComponent> RootComp;
// 激光特效组件
UPROPERTY(VisibleDefaultsOnly, Category = "Component")
TObjectPtr<UNiagaraComponent> LaserVFX;
// 目标检测球体组件
UPROPERTY(VisibleDefaultsOnly, Category = "Component")
TObjectPtr<USphereComponent> TargetEndDetectionSphere;
// 定时检测句柄
FTimerHandle PeriodicalTargetingTimerHandle;
// 执行目标检测并报告结果
void DoTargetCheckAndReport();
// 更新激光特效和检测逻辑
void UpdateTargetTrace();
// 判断Actor是否应被报告为目标
bool ShouldReportActorAsTarget(const AActor* ActorToCheck) const;
};
cpp
#include "TargetActor_Line.h"
#include "NiagaraComponent.h"
#include "Abilities/GameplayAbility.h"
#include "Crunch/Crunch.h"
#include "Kismet/KismetMathLibrary.h"
#include "Net/UnrealNetwork.h"
// Sets default values
ATargetActor_Line::ATargetActor_Line()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
bReplicates = true;
ShouldProduceTargetDataOnServer = true;
AvatarActor = nullptr;
// 创建根组件
RootComp = CreateDefaultSubobject<USceneComponent>("Root Comp");
SetRootComponent(RootComp);
// 创建目标检测球体组件(用于检测终点附近的目标)
TargetEndDetectionSphere = CreateDefaultSubobject<USphereComponent>("Target End Detection Sphere");
TargetEndDetectionSphere->SetupAttachment(GetRootComponent());
TargetEndDetectionSphere->SetCollisionResponseToChannel(ECC_SPRING_ARM, ECR_Ignore);
// 创建激光特效组件
LaserVFX = CreateDefaultSubobject<UNiagaraComponent>("LaserVFX");
LaserVFX->SetupAttachment(GetRootComponent());
}
void ATargetActor_Line::ConfigureTargetSetting(float NewTargetRange, float NewDetectionCylinderRadius,
float NewTargetingInterval, FGenericTeamId OwnerTeamId, bool bShouldDrawDebug)
{
TargetRange = NewTargetRange;
DetectionCylinderRadius = NewDetectionCylinderRadius;
TargetingInterval = NewTargetingInterval;
SetGenericTeamId(OwnerTeamId);
bDrawDebug = bShouldDrawDebug;
}
void ATargetActor_Line::SetGenericTeamId(const FGenericTeamId& NewTeamID)
{
TeamId = NewTeamID;
}
void ATargetActor_Line::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ATargetActor_Line, TeamId);
DOREPLIFETIME(ATargetActor_Line, TargetRange);
DOREPLIFETIME(ATargetActor_Line, DetectionCylinderRadius);
DOREPLIFETIME(ATargetActor_Line, AvatarActor);
}
void ATargetActor_Line::StartTargeting(UGameplayAbility* Ability)
{
Super::StartTargeting(Ability);
if (!OwningAbility)
return;
AvatarActor = OwningAbility->GetAvatarActorFromActorInfo();
// 在服务器上开启定时器周期检测目标
if (HasAuthority())
{
GetWorldTimerManager().SetTimer(PeriodicalTargetingTimerHandle, this, &ATargetActor_Line::DoTargetCheckAndReport, TargetingInterval, true);
}
}
void ATargetActor_Line::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 更新激光
UpdateTargetTrace();
}
void ATargetActor_Line::BeginDestroy()
{
// 在去除Actor的时候停止定时器
if (GetWorld() && PeriodicalTargetingTimerHandle.IsValid())
{
GetWorldTimerManager().ClearTimer(PeriodicalTargetingTimerHandle);
}
Super::BeginDestroy();
}
void ATargetActor_Line::DoTargetCheckAndReport()
{
// 服务器中执行
if (!HasAuthority()) return;
// 获取重叠的Actor
TArray<AActor*> OverlappingActorSet;
TargetEndDetectionSphere->GetOverlappingActors(OverlappingActorSet);
// 筛选有效目标
TArray<TWeakObjectPtr<AActor>> OverlappingActors;
for (AActor* Actor : OverlappingActorSet)
{
if (ShouldReportActorAsTarget(Actor))
{
OverlappingActors.Add(Actor);
}
}
// 构建目标数据
FGameplayAbilityTargetDataHandle TargetDataHandle;
FGameplayAbilityTargetData_ActorArray* ActorArray = new FGameplayAbilityTargetData_ActorArray;
ActorArray->SetActors(OverlappingActors);
TargetDataHandle.Add(ActorArray);
// 广播目标数据就绪事件
TargetDataReadyDelegate.Broadcast(TargetDataHandle);
}
void ATargetActor_Line::UpdateTargetTrace()
{
// 设置激光起始点
FVector ViewLocation = GetActorLocation();
FRotator ViewRotation = GetActorRotation();
if (AvatarActor)
{
// 获取摄像机位置和瞄准的旋转
AvatarActor->GetActorEyesViewPoint(ViewLocation, ViewRotation);
}
// 计算激光方向和终点
FVector LookEndPoint = ViewLocation + ViewRotation.Vector() * 100000;
// 根据角色位置以及相机方向计算出激光方向
FRotator LookRotation = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), LookEndPoint);
SetActorRotation(LookRotation);
// 获取激光终点 : 由激光发射位置,由角色朝向,和激光长度设置终点位置
FVector SweepEndLocation = GetActorLocation() + LookRotation.Vector() * TargetRange;
TArray<FHitResult> HitResults;
// 配置碰撞参数,忽略自身和拥有者
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(AvatarActor);
QueryParams.AddIgnoredActor(this);
FCollisionResponseParams CollisionResponseParams(ECR_Overlap);
// 球形检测,查找路径上的目标
GetWorld()->SweepMultiByChannel(HitResults, GetActorLocation(), SweepEndLocation, FQuat::Identity, ECC_WorldDynamic, FCollisionShape::MakeSphere(DetectionCylinderRadius), QueryParams, CollisionResponseParams);
FVector LineEndLocation = SweepEndLocation;
float LineLength = TargetRange;
// 命中第一个非友方目标则截断激光
for (FHitResult& HitResult : HitResults)
{
if (HitResult.GetActor())
{
if (GetTeamAttitudeTowards(*HitResult.GetActor()) != ETeamAttitude::Friendly)
{
LineEndLocation = HitResult.ImpactPoint;
LineLength = FVector::Distance(GetActorLocation() , LineEndLocation);
break;
}
}
}
// 更新终点检测球体和激光长度
TargetEndDetectionSphere->SetWorldLocation(LineEndLocation);
if (LaserVFX)
{
LaserVFX->SetVariableFloat(LaserFXLengthParamName, LineLength/100.f);
}
}
bool ATargetActor_Line::ShouldReportActorAsTarget(const AActor* ActorToCheck) const
{
// 基本无效检查
if (!ActorToCheck) return false;
if (ActorToCheck == AvatarActor) return false; // 忽略自身
if (ActorToCheck == this) return false; // 忽略自身
// 团队关系检查(只报告敌对目标)
if (GetTeamAttitudeTowards(*ActorToCheck) != ETeamAttitude::Hostile)
return false;
return true;
}

将激光TA添加到激光GA上
到CGameplayAbility中添加一个函数用来应用伤害
cpp
// 将伤害应用到TargetDataHandle中的所有目标
void ApplyDamageToTargetDataHandle(const FGameplayAbilityTargetDataHandle& TargetDataHandle, const FGenericDamageEffectDef& Damage, int Level = 1);
cpp
void UCGameplayAbility::ApplyDamageToTargetDataHandle(const FGameplayAbilityTargetDataHandle& TargetDataHandle,
const FGenericDamageEffectDef& Damage, int Level)
{
const UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
AActor* AvatarActor = GetAvatarActorFromActorInfo();
// 创建效果上下文, 设置能力 、源对象 和 施加者
FGameplayEffectContextHandle ContextHandle = ASC->MakeEffectContext();
ContextHandle.SetAbility(this);
ContextHandle.AddSourceObject(AvatarActor);
ContextHandle.AddInstigator(AvatarActor, AvatarActor);
for (const auto& TypePair : Damage.DamageTypeDefinitions)
{
// 创建效果Spec句柄,指定效果类、能力等级和上下文
FGameplayEffectSpecHandle EffectSpecHandle = ASC->MakeOutgoingSpec(Damage.DamageEffect, Level, ContextHandle);
float TotalModifier = TypePair.Value.BaseDamage.GetValueAtLevel(Level);
for (const auto& Modifier : TypePair.Value.AttributeDamageModifiers)
{
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(EffectSpecHandle, Modifier.Key, Modifier.Value);
}
UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(EffectSpecHandle, TypePair.Key, TotalModifier);
// 在目标上应用游戏效果规范
K2_ApplyGameplayEffectSpecToTarget(EffectSpecHandle,TargetDataHandle);
}
}
发射激光技能中添加这个TA
cpp
// 激光目标Actor类(直线检测)
UPROPERTY(EditDefaultsOnly, Category = "Targeting")
TSubclassOf<class ATargetActor_Line> LaserTargetActorClass;
// 目标数据接收回调(处理命中目标)
UFUNCTION()
void TargetReceived(const FGameplayAbilityTargetDataHandle& TargetDataHandle);
cpp
void UGA_Laser::ShootLaser(FGameplayEventData Payload)
{
// --- 服务器端逻辑 ---
if (K2_HasAuthority())
{
// 应用持续消耗法力的效果
OnGoingConsumptionEffectHandle = BP_ApplyGameplayEffectToOwner(OnGoingConsumptionEffect, GetAbilityLevel(CurrentSpecHandle, CurrentActorInfo));
// 注册法力变化回调(检测法力不足)
UAbilitySystemComponent* OwnerAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
if (OwnerAbilitySystemComponent)
{
OwnerAbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(UCAttributeSet::GetManaAttribute()).AddUObject(this, &UGA_Laser::ManaUpdated);
}
}
// 创建目标检测任务
UAbilityTask_WaitTargetData* WaitDamageTargetTask = UAbilityTask_WaitTargetData::WaitTargetData(
this,
NAME_None,
EGameplayTargetingConfirmation::CustomMulti, // 持续检测模式
LaserTargetActorClass
);
// 绑定委托
WaitDamageTargetTask->ValidData.AddDynamic(this, &UGA_Laser::TargetReceived);
// 执行任务
WaitDamageTargetTask->ReadyForActivation();
// 配置并生成目标检测Actor
AGameplayAbilityTargetActor* TargetActor;
WaitDamageTargetTask->BeginSpawningActor(this, LaserTargetActorClass, TargetActor);
if (ATargetActor_Line* LineTargetActor = Cast<ATargetActor_Line>(TargetActor))
{
// 设置检测参数:范围/半径/频率/队伍/调试显示
LineTargetActor->ConfigureTargetSetting(
TargetRange,
DetectionCylinderRadius,
TargetingInterval,
GetOwnerTeamId(),
ShouldDrawDebug()
);
}
WaitDamageTargetTask->FinishSpawningActor(this, TargetActor);
// 将Actor附加到骨骼中
if (ATargetActor_Line* LineTargetActor = Cast<ATargetActor_Line>(TargetActor))
{
LineTargetActor->AttachToComponent(
GetOwningComponentFromActorInfo(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
TargetActorAttachSocketName
);
}
}
void UGA_Laser::TargetReceived(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
// --- 服务器端逻辑 ---
if (K2_HasAuthority())
{
// 1. 对目标应用伤害效果
// BP_ApplyGameplayEffectToTarget(
// TargetDataHandle,
// HitDamageEffect,
// GetAbilityLevel(CurrentSpecHandle, CurrentActorInfo)
// );
// K2_ApplyGameplayEffectSpecToTarget(,TargetDataHandle);
ApplyDamageToTargetDataHandle(TargetDataHandle, HitDamageEffect, GetAbilityLevel(CurrentSpecHandle, CurrentActorInfo));
}
// 2. 对目标施加击退力
PushTargets(
TargetDataHandle,
GetAvatarActorFromActorInfo()->GetActorForwardVector() * HitDamageEffect.PushVelocity // 基于发射者方向的击退
);
}
编译后在蓝图中把这个TA塞进来
再把瞄准偏移移动一下
修改一点杂事
把风暴核心的碰撞修改一下
顺便把风暴核心的胶囊体弄大点,可能太小不好爬楼梯
把地图的两个目标的碰撞类型改为Pawn
补充初始化金币的GE

到英雄固定资产中添加