LyraLog 27 GameplayCue Damage

跳字整理

C++类,

ULyraNumberPopComponent_NiagaraText => ULyraNumberPopComponent => UControllerComponent

蓝图

/Game/Effects/Blueprints/B_NiagaraNumberPopComponent.B_NiagaraNumberPopComponent

/ShooterCore/Experiences/LAS_ShooterGame_StandardComponents.LAS_ShooterGame_StandardComponents

加到客户端的PlayerController上

最基础的一个C++类,一个是跳字的描述架构,和一个 Controller Component

USTRUCT(BlueprintType)
struct FEqNumberPopRequest
{
    GENERATED_BODY()

    // 生成数字弹出效果的世界坐标位置
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Eq|Number Pops")
    FVector WorldLocation;

    // 与数字弹出效果的来源/原因相关的标签(用于确定样式)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Eq|Number Pops")
    FGameplayTagContainer SourceTags;

    // 与数字弹出效果的目标相关的标签(用于确定样式)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Eq|Number Pops")
    FGameplayTagContainer TargetTags;

    // 要显示的数字
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Eq|Number Pops")
    int32 NumberToDisplay = 0;

    // 数字是否为“暴击” (@TODO: move to a tag)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Eq|Number Pops")
    bool bIsCriticalDamage = false;

    FEqNumberPopRequest()
        : WorldLocation(ForceInitToZero)
    {
    }
};

UCLASS(Abstract)
class UEqNumberPopComponent : public UControllerComponent
{
    GENERATED_BODY()

public:

    UEqNumberPopComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

    /** 添加一个数字弹出效果请求 */
    UFUNCTION(BlueprintCallable, Category = Foo)
    virtual void AddNumberPop(const FEqNumberPopRequest& NewRequest) {}
};

他的子类有两个,一个是叫MeshText,另一个是 NiagaraText

MeshText 代码也太多了,而且没用到吧。项目蓝图继承的是NiagaraText这个

UCLASS(Blueprintable)
class UEqNumberPopComponent_NiagaraText : public UEqNumberPopComponent
{
    GENERATED_BODY()

public:

    UEqNumberPopComponent_NiagaraText(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

    //~UEqNumberPopComponent interface
    virtual void AddNumberPop(const FEqNumberPopRequest& NewRequest) override;
    //~End of UEqNumberPopComponent interface

protected:

    TArray<int32> DamageNumberArray;

    /** 尝试应用于新增数字弹窗的样式规则 */
    UPROPERTY(EditDefaultsOnly, Category = "Number Pop|Style")
    TObjectPtr<UEqDamagePopStyleNiagara> Style;

    // Niagara组件用于显示伤害
    UPROPERTY(EditDefaultsOnly, Category = "Number Pop|Style")
    TObjectPtr<UNiagaraComponent> NiagaraComp;
};

他配置了 /Game/Effects/Blueprints/Damage_BasicNiagaraStyle.Damage_BasicNiagaraStyle 这个风格。风格里面配置的特效用的是哪一个

AddNumberPop 里面的请求是跳字的数字,是否暴击,位置这些东西,然后创建出来吧

void UEqNumberPopComponent_NiagaraText::AddNumberPop(const FEqNumberPopRequest& NewRequest)
{
    // NewRequest 中包含了需要显示跳字的数字、位置、以及一些标签信息(来源/目标标签、是否暴击等)。我们可以根据这些信息来决定如何显示跳字。

    int32 LocalDamage = NewRequest.NumberToDisplay;

    // 改变伤害数值为负数以区分暴击和普通伤害
    if (NewRequest.bIsCriticalDamage)
    {
        LocalDamage *= -1;
    }

    // 如果还没有Niagara组件,则添加一个
    if (!NiagaraComp)
    {
        NiagaraComp = NewObject<UNiagaraComponent>(GetOwner());
        if (Style != nullptr)
        {
            NiagaraComp->SetAsset(Style->TextNiagara);
            NiagaraComp->bAutoActivate = false;
        }
        NiagaraComp->SetupAttachment(nullptr);
        check(NiagaraComp);
        NiagaraComp->RegisterComponent();
    }

    NiagaraComp->Activate(false);
    NiagaraComp->SetWorldLocation(NewRequest.WorldLocation);

    UE_LOG(LogEqZero, Log, TEXT("DamageHit location : %s"), *(NewRequest.WorldLocation.ToString()));

    // 将伤害信息添加到当前的Niagara列表中
    // 伤害信息被打包在一个FVector4中,其中XYZ = 位置,W = 伤害值
    TArray<FVector4> DamageList = UNiagaraDataInterfaceArrayFunctionLibrary::GetNiagaraArrayVector4(NiagaraComp, Style->NiagaraArrayName);
    DamageList.Add(FVector4(NewRequest.WorldLocation.X, NewRequest.WorldLocation.Y, NewRequest.WorldLocation.Z, LocalDamage));
    UNiagaraDataInterfaceArrayFunctionLibrary::SetNiagaraArrayVector4(NiagaraComp, Style->NiagaraArrayName, DamageList);
}

哪里调用的

/Game/GameplayCueNotifies/GCNL_Character_DamageTaken.GCNL_Character_DamageTaken

On Execute 里面,最后调用到 AddNumberPop

FEqNumberPopRequest 有若干参数要填

  • WorldLocation 找 CueParam->EffectContext->HitResult->Location
  • SourceTags。CueParam->Aggregated SourceTags
  • TargetTags。CueParam->Aggregated TargetTags
  • NumberToDisplay。具体数字,CueParam的Raw Magnitude
  • bIsCriticalDamage。从 CueParam->EffectContext->HitResult-> 物理材质中找TAG

这里回顾一下CueParam来自哪里。

/Game/Weapons/Pistol/GE_Damage_Pistol.GE_Damage_Pistol

这个是GE配置的Cue那么他的Param来自哪里来着

TArray<FActiveGameplayEffectHandle> UGameplayAbility::BP_ApplyGameplayEffectToTarget(FGameplayAbilityTargetDataHandle Target, TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel, int32 Stacks)
{
    return ApplyGameplayEffectToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, Target, GameplayEffectClass, GameplayEffectLevel, Stacks);
}
void UGameplayCueManager::InvokeGameplayCueExecuted_FromSpec(UAbilitySystemComponent* OwningComponent, const FGameplayEffectSpec& Spec, FPredictionKey PredictionKey)
{
    // 。。。

    UAbilitySystemGlobals::Get().InitGameplayCueParameters_GESpec(PendingCue.CueParameters, Spec);

    // 。。。
}

其实上面的

AggregatedSourceTags,AggregatedTargetTags,RawMagnitude 都在这里了

void UAbilitySystemGlobals::InitGameplayCueParameters_GESpec(FGameplayCueParameters& CueParameters, const FGameplayEffectSpec& Spec)
{
    CueParameters.AggregatedSourceTags = *Spec.CapturedSourceTags.GetAggregatedTags();
    CueParameters.AggregatedTargetTags = *Spec.CapturedTargetTags.GetAggregatedTags();

    // Look for a modified attribute magnitude to pass to the CueParameters
    for (const FGameplayEffectCue& CueDef : Spec.Def->GameplayCues)
    {   
        bool FoundMatch = false;
        if (CueDef.MagnitudeAttribute.IsValid())
        {
            for (const FGameplayEffectModifiedAttribute& ModifiedAttribute : Spec.ModifiedAttributes)
            {
                if (ModifiedAttribute.Attribute == CueDef.MagnitudeAttribute)
                {
                    CueParameters.RawMagnitude = ModifiedAttribute.TotalMagnitude;
                    FoundMatch = true;
                    break;
                }
            }
            if (FoundMatch)
            {
                break;
            }
        }
    }

    CueParameters.GameplayEffectLevel = Spec.GetLevel();
    CueParameters.AbilityLevel = Spec.GetEffectContext().GetAbilityLevel();

    InitGameplayCueParameters(CueParameters, Spec.GetContext());
}

还有一个是 CueParam->EffectContext->HitResult

这个EffectContext的HitResult我们啥时候写进去的。

USTRUCT(BlueprintType)
struct FGameplayEffectContextHandle
{
    const FHitResult* GetHitResult() const
    {
        if (IsValid())
        {
            return Data->GetHitResult();
        }
        return nullptr;
    }

    TSharedPtr<FGameplayEffectContext> Data;
}

从流程上应该是

// local control
void UEqZeroGameplayAbility_RangedWeapon::StartRangedWeaponTargeting()
{
    // 射线检测
    TArray<FHitResult> FoundHits;
    PerformLocalTargeting(FoundHits);

    // 构建TargetData
    FGameplayAbilityTargetDataHandle TargetData;
    TargetData.UniqueId = WeaponStateComponent ? WeaponStateComponent->GetUnconfirmedServerSideHitMarkerCount() : 0; // 这里合适吗?

    if (FoundHits.Num() > 0)
    {
        const int32 CartridgeID = FMath::Rand();

        for (const FHitResult& FoundHit : FoundHits)
        {
            // 创建 GAS 标准的单点命中数据结构
            FEqZeroGameplayAbilityTargetData_SingleTargetHit* NewTargetData = new FEqZeroGameplayAbilityTargetData_SingleTargetHit();
            NewTargetData->HitResult = FoundHit;
            NewTargetData->CartridgeID = CartridgeID;

            TargetData.Add(NewTargetData);
        }
    }

    // 本地调用
    OnTargetDataReadyCallback(TargetData, FGameplayTag());
}

这里local controller 先过来了,调用 CallServerSetReplicatedTargetData 后,服务器在

OnTargetDataReadyCallbackDelegateHandle = MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).AddUObject(this, &ThisClass::OnTargetDataReadyCallback);

这里注册的 OnTargetDataReadyCallback 会带着TargetData 继续跑

void UEqZeroGameplayAbility_RangedWeapon::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag)
{
    UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
    check(MyAbilityComponent);

    if (const FGameplayAbilitySpec* AbilitySpec = MyAbilityComponent->FindAbilitySpecFromHandle(CurrentSpecHandle))
    {
        // 开启预测窗口,用于网络同步时的平滑处理
        FScopedPredictionWindow ScopedPrediction(MyAbilityComponent);
        FGameplayAbilityTargetDataHandle LocalTargetDataHandle(MoveTemp(const_cast<FGameplayAbilityTargetDataHandle&>(InData)));

        // 如果是客户端,必须调用这个函数 (本地控制,但是又不权威。我们只讨论DS客户端,他这里排除了单机模式和ListenServer的玩家1)
        if (const bool bShouldNotifyServer = CurrentActorInfo->IsLocallyControlled() && !CurrentActorInfo->IsNetAuthority())
        {
            MyAbilityComponent->CallServerSetReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), LocalTargetDataHandle, ApplicationTag, MyAbilityComponent->ScopedPredictionKey);
        }

#if WITH_SERVER_CODE

        // 服务器可以做命中确认,然后做一些逻辑,但是这里没有。。。

#endif //WITH_SERVER_CODE

        // 检查是否还有弹药或满足其他消耗条件
        if (bIsTargetDataValid && CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo))
        {
            // ...

            // 蓝图挂钩:触发伤害、播放特效等
            OnRangedWeaponTargetDataReady(LocalTargetDataHandle);

        }
        else
        {
            // 如果无法提交(例如没子弹了),则打印警告并结束技能
            K2_EndAbility();
        }
    }

    // 标记数据已被处理,防止重复使用
    MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey());
}

OnRangedWeaponTargetDataReady 连着蓝图,把带着HitResult的 LocalTargetDataHandle 传给 Apply Effect。

这个多个HitResult传到Cue里面怎么的 Effect Context 怎么就一个了呢?

TArray<FActiveGameplayEffectHandle> UGameplayAbility::ApplyGameplayEffectSpecToTarget(const FGameplayAbilitySpecHandle AbilityHandle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEffectSpecHandle SpecHandle, const FGameplayAbilityTargetDataHandle& TargetData) const
{
    TArray<FActiveGameplayEffectHandle> EffectHandles;

    if (SpecHandle.IsValid() && HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
    {
        TARGETLIST_SCOPE_LOCK(*ActorInfo->AbilitySystemComponent);
        for (TSharedPtr<FGameplayAbilityTargetData> Data : TargetData.Data)
        {
            if (Data.IsValid())
            {
                EffectHandles.Append(Data->ApplyGameplayEffectSpec(*SpecHandle.Data.Get(), ActorInfo->AbilitySystemComponent->GetPredictionKeyForNewAction()));
            }
            else
            {
                ABILITY_LOG(Warning, TEXT("UGameplayAbility::ApplyGameplayEffectSpecToTarget invalid target data passed in. Ability: %s"), *GetPathName());
            }
        }
    }
    return EffectHandles;
}

核心在这里,他对每一个TargetData.Data都ApplyGameplayEffectSpec了。

====

其他逻辑主要还是给学学niagara

完成跳字

/Game/Effects/Blueprints/Damage_BasicNiagaraStyle.Damage_BasicNiagaraStyle 配置一下DA

/Game/Effects/Blueprints/B_NiagaraNumberPopComponent.B_NiagaraNumberPopComponent 创建蓝图并配置上面的Style

/EqZeroCore/Experiences/LAS_ShooterGame_StandardComponents.LAS_ShooterGame_StandardComponents 里面配置这个组件

给 Eq Player Controller 的 client 加上 刚刚的蓝图

/Game/GameplayCueNotifies/GCNL_Character_DamageTaken.GCNL_Character_DamageTaken

在这个Cue的 On Execute 里面,【这里是主要逻辑】

// 蓝图伪代码
PlayerState = Cast<PlayerState>(CueParam.EffectContext.GetInstigatorActor())
IsLocalPlayer = PlayerState->GetPawn()->IsLocalControlled()

如果是True的时候播放粒子

蓝图函数,是否爆头,给Niagara提供输入参数

// 蓝图伪代码
bool EvaluateWeakSpot(UObject* Object)
{
   return Cast<PhysicalMaterialWithTags>(Object)->Tagss->HasTag(TEXT("Gameplay.Zone.WeakSpot"));
}

这个要继承引擎物理材质,加一个Tag字段。然后在角色物理资产那里去标记。

然后在蓝图准备参数就行,就是连线有点丑,要不改成C++吧。。。

FEqNumberPopRequest 有若干参数要填

  • WorldLocation 找 CueParam->EffectContext->HitResult->Location
  • SourceTags。CueParam->Aggregated SourceTags
  • TargetTags。CueParam->Aggregated TargetTags
  • NumberToDisplay。具体数字,CueParam的Raw Magnitude
  • bIsCriticalDamage。从 CueParam->EffectContext->HitResult-> 物理材质中找TAG

====

被击音效

直接拿击中的 Controller->HealthComp 拿到HP,播放击中和击杀音效。通过 Play Sound 2D

判断击中是否是本地玩家False,那就是自己被打了。要播放另一个音效

GameplayCue的流程

这个是Damage的Effect配置的Cue,还有一个直接ASC接口触发Cue。

// AbilitySystemComponent.h

// 一次性执行:触发 Executed 事件
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag,
const FGameplayCueParameters& GameplayCueParameters);

// 添加持续 Cue:触发 OnActive + WhileActive 事件
void AddGameplayCue(const FGameplayTag GameplayCueTag,
const FGameplayCueParameters& GameplayCueParameters);

// 移除持续 Cue:触发 Removed 事件
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);

// 通用触发接口:可手动指定事件类型
void InvokeGameplayCueEvent(const FGameplayTag GameplayCueTag,
EGameplayCueEvent::Type EventType,
const FGameplayCueParameters& GameplayCueParameters);

有Loop的Cue,用错会怎么样。有些函数是空实现,流程不对。某些流程没跑,OnRemove 没回收等等。反正就是有问题。

我们看 ExecuteGameplayCue

void UAbilitySystemComponent::ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext)
{
    // Send to the wrapper on the cue manager
    UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueExecuted(this, GameplayCueTag, ScopedPredictionKey, EffectContext);
}

最后都会到这

void UGameplayCueManager::AddPendingCueExecuteInternal(FGameplayCuePendingExecute& PendingCue)
{
    if (ProcessPendingCueExecute(PendingCue))
    {
        PendingExecuteCues.Add(PendingCue);
    }

    if (GameplayCueSendContextCount == 0)
    {
        // Not in a context, flush now
        FlushPendingCues();
    }
}

加到了一个列表里面 UGameplayCueManager::HandleGameplayCue

然后这里消费

void UGameplayCueManager::HandleGameplayCue(AActor* TargetActor,
                                             FGameplayTag GameplayCueTag,
                                             EGameplayCueEvent::Type EventType,
                                             const FGameplayCueParameters& Parameters,
                                             EGameplayCueExecutionOptions Options)
{
    // Step 1: 抑制检查
    if (ShouldSuppressGameplayCues(TargetActor))
        return;  // DS 默认不执行、DisableGameplayCues cvar、null 目标

    // Step 2: Tag 翻译(运行时重映射)
    // 允许将标签就地翻译为其他标签。详见 FGameplayCueTranslatorManager。这是一个扩展可以自定义规则,也可以不用。
    TranslateGameplayCue(GameplayCueTag, TargetActor, Parameters);

    // Step 3: 路由
    RouteGameplayCue(TargetActor, GameplayCueTag, EventType, Parameters);
}
void UGameplayCueManager::RouteGameplayCue(AActor* TargetActor,
    FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType,
    const FGameplayCueParameters& Parameters, EGameplayCueExecutionOptions Options)
{
    // 接口引用:如果目标 Actor 实现了 IGameplayCueInterface,取出接口指针
    IGameplayCueInterface* GameplayCueInterface =
        !(Options & EGameplayCueExecutionOptions::IgnoreInterfaces)
            ? Cast<IGameplayCueInterface>(TargetActor) : nullptr;

    // 接口可拒绝接收某些 Cue
    bool bAcceptsCue = true;
    if (GameplayCueInterface)
        bAcceptsCue = GameplayCueInterface->ShouldAcceptGameplayCue(TargetActor, GameplayCueTag, EventType, Parameters);

    // ========== PATH 1: Notify 路由 ==========
    // 通过 CueSet 查找并执行已注册的 Notify 蓝图/类
    if (bAcceptsCue && !(Options & EGameplayCueExecutionOptions::IgnoreNotifies))
    {
        RuntimeGameplayCueObjectLibrary.CueSet->HandleGameplayCue(
            TargetActor, GameplayCueTag, EventType, Parameters);
    }

    // ========== PATH 2: 接口分发 ==========
    // 在目标 Actor 类上查找 Tag 同名 UFunction 并调用(蓝图自定义处理器)
    if (GameplayCueInterface && bAcceptsCue)
    {
        GameplayCueInterface->HandleGameplayCue(
            TargetActor, GameplayCueTag, EventType, Parameters);
    }

    TargetActor->ForceNetUpdate();
}
bool UGameplayCueSet::HandleGameplayCue(AActor* TargetActor,
    FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType,
    const FGameplayCueParameters& Parameters)
{
    // 关键:在 Map 中查找 Tag → DataIdx(O(1) 查找)
    int32* Ptr = GameplayCueDataMap.Find(GameplayCueTag);
    if (Ptr && *Ptr != INDEX_NONE)
    {
        int32 DataIdx = *Ptr;
        FGameplayCueParameters writableParameters = Parameters;
        // 找到了,进入内部处理
        return HandleGameplayCueNotify_Internal(TargetActor, DataIdx, EventType, writableParameters);
    }
    return false;  // 该 Tag 没有对应的 Notify 资产
}
bool UGameplayCueSet::HandleGameplayCueNotify_Internal(AActor* TargetActor,
    int32 DataIdx, EGameplayCueEvent::Type EventType, FGameplayCueParameters& Parameters)
{
    FGameplayCueNotifyData& CueData = GameplayCueData[DataIdx];
    Parameters.MatchedTagName = CueData.GameplayCueTag;

    // 惰性加载:如果类还没加载,尝试 ResolveObject
    if (CueData.LoadedGameplayCueClass == nullptr)
    {
        CueData.LoadedGameplayCueClass =
            Cast<UClass>(CueData.GameplayCueNotifyObj.ResolveObject());
        if (CueData.LoadedGameplayCueClass == nullptr)
        {
            // 未找到 → HandleMissingGameplayCue(可能触发同步加载或报错)
            CueManager->HandleMissingGameplayCue(this, CueData, TargetActor, EventType, Parameters);
            return false;
        }
    }

    // ===== PATH A: Static(非实例化)Notify =====
    if (auto* StaticCue = Cast<UGameplayCueNotify_Static>(CueData.LoadedGameplayCueClass->GetDefaultObject()))
    {
        if (StaticCue->HandlesEvent(EventType))
        {
            StaticCue->HandleGameplayCue(TargetActor, EventType, Parameters);  // CDO 上直接调用
            if (!StaticCue->IsOverride)
                HandleGameplayCueNotify_Internal(TargetActor, CueData.ParentDataIdx, EventType, Parameters);
                //                                             ^^^^^^^^^^^^^^^^^^
                // 递归检查父 Tag(如 GameplayCue.Hero.Damage → GameplayCue.Hero → GameplayCue)
        }
        else
        {
            // 该事件类型不被此 Notify 处理,直接递归到父 Tag
            HandleGameplayCueNotify_Internal(TargetActor, CueData.ParentDataIdx, EventType, Parameters);
        }
    }
    // ===== PATH B: Actor(实例化)Notify =====
    else if (auto* ActorCDO = Cast<AGameplayCueNotify_Actor>(CueData.LoadedGameplayCueClass->GetDefaultObject()))
    {
        if (ActorCDO->HandlesEvent(EventType)) // 默认true,重写就可以不跑这个 HandleGameplayCue 的事件,有什么用?
        {
            // 获取或创建 Actor 实例(可能从对象池复用)
            AGameplayCueNotify_Actor* Instance =
                CueManager->GetInstancedCueActor(TargetActor, ActorCDO->GetClass(), Parameters);
            Instance->HandleGameplayCue(TargetActor, EventType, Parameters);
            if (!Instance->IsOverride)
                HandleGameplayCueNotify_Internal(TargetActor, CueData.ParentDataIdx, EventType, Parameters);

            // 特殊情况:Executed 事件 + 无活跃 GE + bAutoDestroyOnRemove
            // → 立即补发一次 Removed 事件,确保实例被清理
        }
        else
        {
            HandleGameplayCueNotify_Internal(TargetActor, CueData.ParentDataIdx, EventType, Parameters);
        }
    }

    return bReturnVal;
}

分为Actor和Static,以及他们的子类

class AGameplayCueNotify_BurstLatent : public AGameplayCueNotify_Actor

class AGameplayCueNotify_Looping : public AGameplayCueNotify_Actor

class UGameplayCueNotify_Burst : public UGameplayCueNotify_Static

======

UGameplayCueNotify_Static

很明显

Static不实例化,一次性的 UGameplayCueNotify_Burst 在Lyra中是开火的Cue /EqZeroCore/Weapons/Pistol/GCN_Weapon_Pistol_Fire.GCN_Weapon_Pistol_Fire

里面是一些音效,还有开火的spawn一些actor做开火效果。枪口火,烟雾,

UGameplayCueNotify_Burst 这里函数很少,

bool UGameplayCueNotify_Burst::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const
{
    UWorld* World = (Target ? Target->GetWorld() : GetWorld());

    FGameplayCueNotify_SpawnContext SpawnContext(World, Target, Parameters);
    SpawnContext.SetDefaultSpawnCondition(&DefaultSpawnCondition);
    SpawnContext.SetDefaultPlacementInfo(&DefaultPlacementInfo);

    if (DefaultSpawnCondition.ShouldSpawn(SpawnContext))
    {
        FGameplayCueNotify_SpawnResult SpawnResult;

        // 这里面是 CameraShake, Feedback手柄反馈,就是Cue旁边配置的那些东西
        BurstEffects.ExecuteEffects(SpawnContext, SpawnResult);

        // 留给蓝图的函数,Lyra的开火就在这里面播放音效,拿到武器spawn actor,创建特效了
        OnBurst(Target, Parameters, SpawnResult);
    }

    return false;
}

接下来问题是,HandleGameplayCue 怎么跑到 OnExecute

void UGameplayCueNotify_Static::HandleGameplayCue(AActor* MyTarget, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& Parameters)
{
    SCOPE_CYCLE_COUNTER(STAT_HandleGameplayCueNotifyStatic);

    if (IsValid(MyTarget))
    {
        K2_HandleGameplayCue(MyTarget, EventType, Parameters);

        switch (EventType)
        {
        case EGameplayCueEvent::OnActive:
            OnActive(MyTarget, Parameters);
            break;

        case EGameplayCueEvent::WhileActive:
            WhileActive(MyTarget, Parameters);
            break;

        case EGameplayCueEvent::Executed:
            OnExecute(MyTarget, Parameters);
            break;

        case EGameplayCueEvent::Removed:
            OnRemove(MyTarget, Parameters);
            break;
        };
    }
    else
    {
        ABILITY_LOG(Warning, TEXT("Null Target"));
    }
}

AGameplayCueNotify_Actor

if (ActorCDO->HandlesEvent(EventType)) // 默认true,重写就可以不跑这个 HandleGameplayCue 的事件,有什么用?
{
    // 获取或创建 Actor 实例(可能从对象池复用)
    AGameplayCueNotify_Actor* Instance =
        CueManager->GetInstancedCueActor(TargetActor, ActorCDO->GetClass(), Parameters);
    Instance->HandleGameplayCue(TargetActor, EventType, Parameters);
    if (!Instance->IsOverride)
        HandleGameplayCueNotify_Internal(TargetActor, CueData.ParentDataIdx, EventType, Parameters);

    // 特殊情况:Executed 事件 + 无活跃 GE + bAutoDestroyOnRemove
    // → 立即补发一次 Removed 事件,确保实例被清理
}

会实例化的情况 GetInstancedCueActor 里面

这里面有角色上有Cue就复用,没有才SpawnActor

HandleGameplayCue 前面看过了,根据 EventType 决定不同的触发函数。

bool bShouldDestroy = false;
if (EventType == EGameplayCueEvent::Executed && !Parameters.bGameplayEffectActive && InstancedCue->bAutoDestroyOnRemove)
{
    bShouldDestroy = true;
}

if (bShouldDestroy)
{
    SpawnedInstancedCue->HandleGameplayCue(TargetActor, EGameplayCueEvent::Removed, Parameters);
}

比如 EGameplayCueEvent::Executed 马上就要回收了。

EventType在你调用函数的时候就决定好了

比如GE就有这一段

FActiveGameplayEffectsContainer::AddActiveGameplayEffectGrantedTagsAndModifiers

    if (!Owner->bSuppressGameplayCues)
    {
        for (const FGameplayEffectCue& Cue : Effect.Spec.Def->GameplayCues)
        {
            // If we use Minimal/Mixed Replication, then this path will AddCue and broadcast the RPC that calls EGameplayCueEvent::OnActive (and WhileActive)
            // Note: This is going to ignore bInvokeGameplayCueEvents and invoke them anyway (with a bunch of caveats e.g. you're the server and ignoring them)
            if (ShouldUseMinimalReplication()) // This can only be true on Authority
            {
                for (const FGameplayTag& CueTag : Cue.GameplayCueTags)
                {
                    // We are now replicating the EffectContext in minimally replicated cues. It may be worth allowing this be determined on a per cue basis one day.
                    // (not sending the EffectContext can make things wrong. E.g, the EffectCauser becomes the target of the GE rather than the source)
                    Owner->AddGameplayCue_MinimalReplication(CueTag, Effect.Spec.GetEffectContext());
                }
            }
            else // ActiveGameplayEffects are replicating to everyone (this path can also execute on client)
            {
                // Do a pseudo-AddGameplayCue (but don't add to ActiveGameplayCues so it doesn't replicate in addition to the AGE we're replicating).
                Owner->UpdateTagMap(Cue.GameplayCueTags, 1);

                if (bInvokeGameplayCueEvents)
                {
                    Owner->InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::OnActive);
                    Owner->InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::WhileActive);
                }
            }
        }
    }

OnActive -> WhileActive

ASC::ExecuteGameplayCue的调用 EventType就是 Executed,见 UGameplayCueManager::FlushPendingCues

然后是Actor的HandleGameplayCue

void AGameplayCueNotify_Actor::HandleGameplayCue(AActor* MyTarget,
                                                  EGameplayCueEvent::Type EventType,
                                                  const FGameplayCueParameters& Parameters)
{
    // 1. 事件门控检查(防止重复触发)
    if (EventType == EGameplayCueEvent::OnActive &&
        !bAllowMultipleOnActiveEvents && bHasHandledOnActiveEvent)
        return;

    if (EventType == EGameplayCueEvent::WhileActive &&
        !bAllowMultipleWhileActiveEvents && bHasHandledWhileActiveEvent)
        return;

    // 2. 堆叠检查(Remove 时检查目标是否仍有匹配 Tag)
    if (GameplayCueNotifyTagCheckOnRemove > 0 &&
        EventType == EGameplayCueEvent::Removed)
    {
        if (TagInterface->HasMatchingGameplayTag(Parameters.MatchedTagName))
            return;  // 目标仍有此 Tag,不执行 Remove
    }

    // 3. 通用 Blueprint 事件(所有事件类型都会调用)
    K2_HandleGameplayCue(MyTarget, EventType, Parameters);

    // 4. 清除之前的自动销毁计时
    SetLifeSpan(0.f);

    // 5. 根据事件类型分发
    switch (EventType)
    {
        case EGameplayCueEvent::OnActive:
            OnActive(MyTarget, Parameters);         // → Blueprint "Event On Burst"
            bHasHandledOnActiveEvent = true;
            break;

        case EGameplayCueEvent::WhileActive:
            WhileActive(MyTarget, Parameters);      // → Blueprint "Event On Become Relevant"
            bHasHandledWhileActiveEvent = true;
            break;

        case EGameplayCueEvent::Executed:
            OnExecute(MyTarget, Parameters);        // → Blueprint "Event On Execute"
            break;

        case EGameplayCueEvent::Removed:
            bHasHandledOnRemoveEvent = true;
            OnRemove(MyTarget, Parameters);         // → Blueprint "Event On Cease Relevant"

            // 自动销毁逻辑
            if (bAutoDestroyOnRemove)
            {
                if (AutoDestroyDelay > 0.f)
                    SetTimer(FinishTimerHandle, AutoDestroyDelay);
                else
                    GameplayCueFinishedCallback();  // 立即回收/销毁
            }
            break;
    }
}

对于TakeDamage 他是GE的Cue,Lyra这个跑了 OnActive 和 Executed 是怎么个事情

开一枪 Executed 跑了

void UAbilitySystemComponent::NetMulticast_InvokeGameplayCueExecuted_FromSpec_Implementation(const FGameplayEffectSpecForRPC Spec, FPredictionKey PredictionKey)
{
    if (IsOwnerActorAuthoritative() || PredictionKey.IsLocalClientKey() == false)
    {
        InvokeGameplayCueEvent(Spec, EGameplayCueEvent::Executed);
    }
}

Lyra这个蓝图的 OnBurst

bool AGameplayCueNotify_BurstLatent::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters)
{
    UWorld* World = GetWorld();

    FGameplayCueNotify_SpawnContext SpawnContext(World, Target, Parameters);
    SpawnContext.SetDefaultSpawnCondition(&DefaultSpawnCondition);
    SpawnContext.SetDefaultPlacementInfo(&DefaultPlacementInfo);

    if (DefaultSpawnCondition.ShouldSpawn(SpawnContext))
    {
        BurstEffects.ExecuteEffects(SpawnContext, BurstSpawnResults);

        OnBurst(Target, Parameters, BurstSpawnResults);
    }

    return false;
}

是在这里跑的

那他这个重写 Execute和蓝图 OnBurst 有什么区别?

多了一个 BurstSpawnResults

USTRUCT(BlueprintType)
struct FGameplayCueNotify_SpawnResult final
{
    GENERATED_BODY()

    GAMEPLAYABILITIES_API FGameplayCueNotify_SpawnResult();

    GAMEPLAYABILITIES_API void Reset();

    // List of FX components spawned.  There may be null pointers here as it matches the defined order.
    UPROPERTY(BlueprintReadOnly, Transient, Category = GameplayCueNotify)
    TArray<TObjectPtr<UFXSystemComponent>> FxSystemComponents;

    // List of audio components spawned.  There may be null pointers here as it matches the defined order.
    UPROPERTY(BlueprintReadOnly, Transient, Category = GameplayCueNotify)
    TArray<TObjectPtr<UAudioComponent>> AudioComponents;

    // List of camera shakes played.  There will be one camera shake per local player controller if shake is played in world.
    UPROPERTY(BlueprintReadOnly, Transient, Category = GameplayCueNotify)
    TArray<TObjectPtr<UCameraShakeBase>> CameraShakes;

    // List of camera len effects spawned.  There will be one camera lens effect per local player controller if the effect is played in world.
    UPROPERTY(BlueprintReadOnly, Transient, Category = GameplayCueNotify)
    TArray<TScriptInterface<ICameraLensEffectInterface>> CameraLensEffects;

    // Force feedback component that was spawned.  This is only valid when force feedback is set to play in world.
    UPROPERTY(BlueprintReadOnly, Transient, Category = GameplayCueNotify)
    TObjectPtr<UForceFeedbackComponent> ForceFeedbackComponent;

    // Player controller used to play the force feedback effect.  Used to stop the effect later.
    UPROPERTY(Transient)
    TObjectPtr<APlayerController> ForceFeedbackTargetPC;

    // Spawned decal component.  This may be null.
    UPROPERTY(BlueprintReadOnly, Transient, Category = GameplayCueNotify)
    TObjectPtr<UDecalComponent> DecalComponent;
};

他会把这么多东西执行后塞到里面去吧。

void FGameplayCueNotify_BurstEffects::ExecuteEffects(const FGameplayCueNotify_SpawnContext& SpawnContext, FGameplayCueNotify_SpawnResult& OutSpawnResult) const
{
    if (!SpawnContext.World)
    {
        UE_LOG(LogGameplayCueNotify, Error, TEXT("GameplayCueNotify: Trying to execute Burst effects with a NULL world."));
        return;
    }

    for (const FGameplayCueNotify_ParticleInfo& ParticleInfo : BurstParticles)
    {
        ParticleInfo.PlayParticleEffect(SpawnContext, OutSpawnResult);
    }

    for (const FGameplayCueNotify_SoundInfo& SoundInfo : BurstSounds)
    {
        SoundInfo.PlaySound(SpawnContext, OutSpawnResult);
    }

    BurstCameraShake.PlayCameraShake(SpawnContext, OutSpawnResult);
    BurstCameraLensEffect.PlayCameraLensEffect(SpawnContext, OutSpawnResult);
    BurstForceFeedback.PlayForceFeedback(SpawnContext, OutSpawnResult);
    BurstDevicePropertyEffect.SetDeviceProperties(SpawnContext, OutSpawnResult);
    BurstDecal.SpawnDecal(SpawnContext, OutSpawnResult);
}

例如 PlaySound 后里面就多一个Audio Component

/Game/Effects/Camera/Damage/NCLE_DamageTaken.NCLE_DamageTaken

比如这个,我受伤了,屏幕有一个受伤效果,需要给特效一个方向参数。

这个 OnBurst 就把Cue蓝图配置的Niagara拿到了,我们在这里Set 特效参数就好了。

奇奇怪怪的疑问

EGameplayCueEvent::WhileActive 会一直触发吗,EGameplayCueEvent::Removed 什么时候会把Actor回收。

===

WhileActive 没有任何 Tick 或定时机制去重复调用。整个引擎中只有 5 处 调用它,全部是一次性触发:

服务器 AddCue AbilitySystemComponent.cpp:1388 首次添加时调用一次
客户端预测 AbilitySystemComponent.cpp:1397 与 OnActive 在同一帧连续调用
属性复制到达 GameplayCueInterface.cpp:296 <font style="color:rgb(215, 186, 125);background-color:rgba(255, 255, 255, 0.1);">PostReplicatedAdd()</font>
中调用一次
Minimal 代理初始化 GameplayCueInterface.cpp:514 SetOwner 时对已有 Tag 调一次
Minimal Delta 新增 GameplayCueInterface.cpp:645 增量反序列化发现新 Tag 时调一次

设计意图:WhileActive 的语义是"我现在处于活跃状态"(状态通知),不是"我每帧都在活跃"。如果你需要持续效果,应该在 WhileActive 里启动循环粒子/声音,而非期望它反复被调用。

EGameplayCueEvent::Removed 什么时候触发这个事件,具体逻辑调用了。比如Exec的Event马上就要回收了。其他看具体逻辑。

上一篇