跳字整理
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马上就要回收了。其他看具体逻辑。