ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

UE5 GAS中GameplayEffect的Tag堆叠与监听机制深度解析

UE5 GAS中GameplayEffect的Tag堆叠与监听机制深度解析 1. 项目概述当GAS的Tag机制遇上你的游戏逻辑如果你正在用UE5.3的Gameplay Ability System (GAS)做项目并且已经不止一次被GameplayEffect的Tag堆叠逻辑搞得晕头转向或者为如何可靠地监听一个效果的状态变化而抓耳挠腮那么这篇东西就是为你准备的。这不是一篇从零开始的GAS入门教程而是聚焦于两个在实际开发中高频出现、文档又语焉不详的“深坑”GameplayEffect的Tag叠加移除策略以及如何稳健地监听GameplayEffect的生效与失效。很多团队在实现诸如“灼烧”Debuff叠加层数、或者需要精确知道一个增益效果何时被驱散时都会在这里栽跟头。我自己在几个项目的战斗系统、状态系统重构中反复蹚过这些浑水积累了一些必须分享出来的经验和避坑指南。简单来说我们会深入两个核心痛点第一当你设计一个可以叠加的GameplayEffect比如一个可叠加5层的“中毒”效果时其携带的GameplayTag是如何随着层数增加和移除而变化的一个常见的误解是“移除一层效果就会移除对应的Tag”但事实往往更微妙。第二我们如何知道一个GameplayEffect被成功应用、被移除、或者其堆叠数发生了变化UE提供了委托Delegate但直接使用OnGameplayEffectApplied这类全局委托会面临监听对象生命周期管理的难题而FActiveGameplayEffect上的委托又需要一点技巧才能用好。本文将结合UE5.3的源码逻辑和实际项目案例把这两件事彻底讲透并提供可直接复制使用的代码方案。2. 核心机制深度解析Tag与堆叠的纠缠关系要避开坑首先得明白坑是怎么形成的。GameplayEffectGE、GameplayTagGT和堆叠Stacking三者之间的交互是GAS中最容易产生预期外行为的区域之一。2.1 GameplayEffect的Tag容器与授予策略一个GameplayEffect拥有多个GameplayTag容器属性其中与我们话题最相关的是GrantedTags授予标签当这个GE处于激活状态时这些标签会被添加到目标Target的AbilitySystemComponentASC上。OngoingTagRequirements持续标签需求GE激活期间目标ASC必须持续满足这些标签要求拥有RequireTags不拥有IgnoreTags否则GE会被暂时禁用。RemovalTagRequirements移除标签需求如果目标ASC获得了这些标签GE会被移除。关键理解GrantedTags的“授予”行为与GE的堆叠周期和堆叠过期策略紧密绑定。它不是简单的一对一映射。2.2 堆叠类型与Tag授予的真相在GE的细节面板中Stacking部分决定了多个相同GE实例如何共存。Stacking Type通常有三种None不堆叠。新应用的同种GE会刷新持续时间但不会叠加层数。GrantedTags只存在“有”或“无”的状态。Aggregate by Source按来源聚合堆叠。来自同一Source的GE会堆叠层数。Aggregate by Target按目标聚合堆叠。无论来源是谁应用到同一目标上的同种GE都会堆叠层数。这里是最核心的陷阱对于可堆叠的GE类型2或3GrantedTags的授予不是每层一个Tag。默认情况下只要堆叠数大于等于1GrantedTags就会被授予一次。当堆叠数降为0时这些标签才会被移除。这意味着一个5层的中毒效果和一个1层的中毒效果在目标的ASC上拥有的GrantedTags例如State.Poisoned在数量上是没有区别的——都只有一个。避坑提示1层数判断不能依赖Tag数量你无法通过查询HasTag(State.Poisoned)来判断中毒层数。HasTag只会返回布尔值。要获取层数必须通过GetGameplayEffectCount或查询FActiveGameplayEffect的StackCount。那么Stack Duration Policy和Stack Expiration Policy又如何影响Tag呢Stack Duration Policy堆叠周期策略Refresh on successful application新应用GE会刷新整个堆叠的持续时间。GrantedTags的持续时间也随之刷新。Never Refresh每层独立计时。当最早的一层到期时堆叠数减1。只有当堆叠数从1减为0时GrantedTags才会被移除。这是最容易出错的地方假设一个5层、每层持续10秒的“破甲”效果Stack Expiration Policy设为Remove Single Stack and Refresh Duration在第50秒时第一层到期。此时堆叠数变为4但GrantedTags如State.ArmorBroken依然存在于目标身上因为层数不为0。你的“对破甲状态敌人增伤”的逻辑依然有效尽管层数变了。Stack Expiration Policy堆叠过期策略Clear Entire Stack到期时移除整个堆叠。GrantedTags自然被移除。Remove Single Stack and Refresh Duration到期时移除一层并刷新剩余堆叠的持续时间。如上例所述Tag的移除时机取决于堆叠数是否归零。2.3 源码视角下的Tag移除结合网络搜索中提到的“Reddit - Please wait for verification”片段虽然内容未显示但标题很有启发性“How to remove gameplay tags with a gameplay effect in UE5.3 - You can Add and Remove tags in one Gameplay Effect, with a caveat. This way will remove/cancel an ENTIRE gameplay effect (stacks and all).”这指向了GE的另一个强大功能GameplayEffect可以移除其他GameplayEffect。这是通过Remove Gameplay Effects with Tags或Remove Gameplay Effects with Granted Tags模块实现的。但这里有一个巨大的“坑”caveat当使用这种方式移除一个可堆叠的GE时会移除其整个堆叠stacks and all而不是仅仅移除一层。这与我们上面提到的每层独立过期移除的逻辑完全不同。例如你设计了一个“净化术”效果它试图移除目标身上的所有“魔法效果”Tag为Debuff.Magic。如果目标身上有一个10层叠加的魔法中毒效果一个净化术会将其完全清除而不是净化掉一层。这在设计技能时至关重要需要明确区分“驱散一层Debuff”和“驱散整个Debuff效果”是两种完全不同的需求。3. 可靠监听委托的使用策略与生命周期管理知道了Tag和堆叠的机制我们还需要知道“什么时候”发生了这些变化。这就是监听监听器的用武之地。GAS提供了几种委托Delegate来响应GE的变化。3.1 全局委托AbilitySystemComponent上的监听在UAbilitySystemComponent上有一系列OnGameplayEffectX委托例如OnActiveGameplayEffectAddedDelegateToSelf当任何GE被添加到这个ASC时触发。OnAnyGameplayEffectRemovedDelegate当任何GE从这个ASC移除时触发。OnGameplayEffectStackChangeDelegate当某个GE的堆叠数发生变化时触发。OnGameplayEffectTagCountChanged当ASC拥有的GameplayTag数量变化时触发这与GE的GrantedTags变化相关。使用方法示例// 在持有ASC的Actor如Character初始化时绑定 if (UAbilitySystemComponent* MyASC GetAbilitySystemComponent()) { MyASC-OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, AMyCharacter::OnGameplayEffectAdded); MyASC-OnAnyGameplayEffectRemovedDelegate().AddUObject(this, AMyCharacter::OnGameplayEffectRemoved); MyASC-OnGameplayEffectStackChangeDelegate.AddUObject(this, AMyCharacter::OnGameplayEffectStackChanged); } void AMyCharacter::OnGameplayEffectAdded(UAbilitySystemComponent* TargetASC, const FGameplayEffectSpec SpecApplied, FActiveGameplayEffectHandle ActiveHandle) { // 通过SpecApplied.Definition 或 ActiveHandle 获取具体的GE信息 UGameplayEffect* GE SpecApplied.Def; if (GE GE-HasGrantedTags(MagicDebuffTagContainer)) { // 处理魔法Debuff添加的逻辑 } } void AMyCharacter::OnGameplayEffectStackChanged(FActiveGameplayEffectHandle ActiveHandle, int32 NewStackCount, int32 OldStackCount) { // 处理层数变化 }陷阱与解决方案生命周期问题这些委托绑定在ASC上。如果你的监听对象比如一个UI组件、一个技能计算模块比ASC先销毁需要在析构时手动移除委托绑定否则会引发访问野指针的崩溃。对于UObject监听者使用AddUObject是安全的因为UE内部会处理弱引用但显式移除仍是好习惯。信息过滤全局委托会收到所有GE的事件。你必须在回调函数里写一堆if来判断是不是你关心的那个GE通过Tag、Asset等。当GE种类很多时这会变得臃肿。性能考虑频繁的GE应用如每秒多次的Dot伤害会频繁触发这些委托。确保你的回调函数是轻量级的。3.2 局部委托监听特定FActiveGameplayEffectHandle更精确的监听方式是针对特定的GE实例。当你应用一个GE时会得到一个FActiveGameplayEffectHandle。通过这个Handle你可以获取到FActiveGameplayEffect并监听其生命周期事件。这是更推荐的方式尤其用于监听特定重要效果的移除// 应用一个GE并准备监听其移除 FGameplayEffectSpecHandle SpecHandle MakeOutgoingSpec(DamageOverTimeEffectClass, 1.0f, ContextHandle); if (SpecHandle.IsValid()) { FActiveGameplayEffectHandle ActiveGEHandle ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get()); if (ActiveGEHandle.IsValid()) { // 获取ASC UAbilitySystemComponent* ASC GetAbilitySystemComponent(); if (ASC) { // 通过Handle找到ActiveGE FActiveGameplayEffect* ActiveGE ASC-GetActiveGameplayEffect(ActiveGEHandle); if (ActiveGE) { // 绑定到该特定ActiveGE的移除委托 ActiveGE-EventSet.OnEffectRemoved.AddUObject(this, AMyCharacter::OnSpecificDamageOverTimeRemoved, ActiveGEHandle); // 注意这里传递了Handle作为参数方便在回调中识别 } } } } void AMyCharacter::OnSpecificDamageOverTimeRemoved(const FGameplayEffectRemovalInfo RemovalInfo, FActiveGameplayEffectHandle Handle) { // RemovalInfo包含移除原因自然结束、手动清除、标签不满足等 // Handle可以用来确认是哪个效果 if (RemovalInfo.bStackExpired) { // 是因为堆叠到期而移除一层 } else if (RemovalInfo.bPrematureRemoval) { // 是被提前移除的如被驱散 } // 执行效果移除后的逻辑如播放消失特效、触发后续技能 }优势精准只监听你关心的那个效果实例。自动清理当FActiveGameplayEffect被销毁时其绑定的委托也会自动清理减少了生命周期管理的负担。信息丰富FGameplayEffectRemovalInfo提供了详细的移除原因对于区分“自然结束”和“被驱散”至关重要。注意事项你需要保存FActiveGameplayEffectHandle并在合适的时机如效果预期结束时检查其有效性。无效的Handle意味着效果已不存在。对于堆叠效果OnEffectRemoved只在整个效果的所有堆叠都被移除时才会触发。监听单层堆叠的变化仍需使用ASC的OnGameplayEffectStackChangeDelegate。3.3 基于Tag的查询与轮询有时委托可能不是唯一答案。对于某些不频繁变化或对实时性要求不高的状态在每帧或定时器里查询ASC的Tag状态也是一种可行方案。// 在Tick或定时器中检查 void AMyCharacter::CheckCriticalState(float DeltaTime) { if (UAbilitySystemComponent* ASC GetAbilitySystemComponent()) { // 检查是否拥有某个Tag对应GE是否激活 bool bIsBurning ASC-HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag(TEXT(State.Burning))); // 如果需要知道层数必须查询具体的GE int32 BurnStacks 0; TArrayFActiveGameplayEffectHandle ActiveEffects ASC-GetActiveEffects(FGameplayEffectQuery()); for (const auto Handle : ActiveEffects) { if (FActiveGameplayEffect* ActiveGE ASC-GetActiveGameplayEffect(Handle)) { if (ActiveGE-Spec.Def ActiveGE-Spec.Def-HasGrantedTags(BurnTagContainer)) { BurnStacks ActiveGE-StackCount; break; } } } } }适用场景UI上显示的状态图标、周期性触发的被动技能检查。不适用场景需要立即响应的游戏逻辑如效果被驱散时触发爆炸。4. 实战案例设计一个可堆叠、可被部分驱散的Debuff系统假设我们要设计一个战斗系统其中包含“虚弱”Debuff可叠加5层每层降低目标5%攻击力。每层独立持续10秒。可以被“净化术”驱散但净化术每次只驱散一层。“净化术”技能移除目标身上一层“魔法”类Debuff。UI系统实时显示玩家身上所有Debuff的图标和层数。4.1 “虚弱”Debuff的GameplayEffect配置AssetGE_Debuff_WeaknessGrantedTags添加Debuff.Magic和Debuff.Weakness。Modifiers添加一个Attribute Based的Modifier作用于AttackPower属性Modifier Op为AdditiveScalable Float Magnitude设为-0.05 * StackCount注意这里用到了StackCount变量。Stacking:Stacking Type:Aggregate by Target来自任何源的虚弱都可叠加。Stack Limit Count:5。Stack Duration Policy:Never Refresh每层独立计时。Stack Expiration Policy:Remove Single Stack and Refresh Duration一层到期只移除该层并刷新剩余层持续时间。Duration Policy:Has Duration(10秒)。这样配置后一个3层的虚弱Debuff其GrantedTagsDebuff.Magic和Debuff.Weakness在ASC上只存在一份。攻击力降低值为-0.05 * 3 -15%。当第一层10秒到期时堆叠数变为2攻击力降低值更新为-10%但Debuff.Magic和Debuff.Weakness标签依然存在。4.2 “净化术”的技能逻辑实现净化术的目标是移除一层魔法Debuff而不是整个效果。我们不能使用GE的Remove Gameplay Effects with Tags功能因为那会清除整个堆叠。我们需要在技能GameplayAbility的激活事件中编写自定义逻辑void UGA_Purify::ExecutePurifyLogic(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) { // 假设这个Ability是在施法者身上目标是另一个Actor AActor* TargetActor GetTargetActorFromEventData(); // 从技能目标数据获取 if (!TargetActor) return; UAbilitySystemComponent* TargetASC UAbilitySystemComponent::GetAbilitySystemComponentFromActor(TargetActor); if (!TargetASC) return; // 1. 构建查询查找所有授予了Debuff.Magic标签的ActiveGE FGameplayEffectQuery Query; Query.OwningTagQuery FGameplayTagQuery::MakeQuery_MatchAnyTags(FGameplayTagContainer::CreateFromSingleTag(DebuffMagicTag)); TArrayFActiveGameplayEffectHandle MagicDebuffHandles TargetASC-GetActiveEffects(Query); if (MagicDebuffHandles.Num() 0) { // 2. 选择其中一个效果进行驱散例如第一个或随机一个或剩余时间最短的一个 // 这里简单选择第一个 FActiveGameplayEffectHandle HandleToRemoveOneStack MagicDebuffHandles[0]; // 3. 关键步骤减少该效果的一层堆叠而不是移除整个效果 FActiveGameplayEffect* ActiveGE TargetASC-GetActiveGameplayEffect(HandleToRemoveOneStack); if (ActiveGE ActiveGE-StackCount 0) { // 使用InternalDecrementStackCount函数。注意这是一个受保护函数通常需要通过继承ASC或使用友元/公共接口。 // 更实际的做法是在自定义的ASC子类中暴露一个公共方法 // bool UMyAbilitySystemComponent::DecrementGameplayEffectStack(FActiveGameplayEffectHandle Handle, int32 StacksToRemove 1); // 其内部调用 InternalDecrementStackCount。 // 假设我们有一个这样的公共方法 UMyAbilitySystemComponent* MyTargetASC CastUMyAbilitySystemComponent(TargetASC); if (MyTargetASC) { bool bSuccess MyTargetASC-DecrementGameplayEffectStack(HandleToRemoveOneStack, 1); if (bSuccess) { // 驱散成功可以触发视觉、音效反馈 // 当堆叠数减到0时GE会被自动标记为待移除其GrantedTags也会被清除 } } } } // ... 技能结束逻辑 }在自定义ASC子类中的实现// MyAbilitySystemComponent.h public: bool DecrementGameplayEffectStack(FActiveGameplayEffectHandle Handle, int32 StacksToRemove 1); // MyAbilitySystemComponent.cpp bool UMyAbilitySystemComponent::DecrementGameplayEffectStack(FActiveGameplayEffectHandle Handle, int32 StacksToRemove) { FActiveGameplayEffect* ActiveGE GetActiveGameplayEffect(Handle); if (ActiveGE ActiveGE-Spec.Def ActiveGE-Spec.Def-StackingType ! EGameplayEffectStackingType::None) { // 调用内部函数减少堆叠。第二个参数是StackCount的修改器。 InternalUpdateActiveGameplayEffectStackCount(Handle, -StacksToRemove); return true; } return false; }4.3 UI层的监听与更新UI需要显示Debuff列表和层数。我们使用OnGameplayEffectStackChangeDelegate和OnAnyGameplayEffectRemovedDelegate来更新UI。// 在UI控件的初始化函数中绑定到玩家角色的ASC void UDebuffStatusWidget::BindToASC(UAbilitySystemComponent* PlayerASC) { if (PlayerASC !BoundASC.IsValid()) { BoundASC PlayerASC; PlayerASC-OnGameplayEffectStackChangeDelegate.AddUObject(this, UDebuffStatusWidget::OnGameplayEffectStackChanged); PlayerASC-OnAnyGameplayEffectRemovedDelegate().AddUObject(this, UDebuffStatusWidget::OnGameplayEffectRemoved); // 初始时也需要获取一次所有ActiveGE来构建UI RefreshAllDebuffs(); } } void UDebuffStatusWidget::OnGameplayEffectStackChanged(FActiveGameplayEffectHandle ActiveHandle, int32 NewStackCount, int32 OldStackCount) { // 找到对应的UI条目更新层数显示 FDebuffUIData* UIData FindUIDataByHandle(ActiveHandle); if (UIData) { UIData-CurrentStacks NewStackCount; UpdateDebuffSlot(*UIData); } else { // 如果没找到可能是一个新的Debuff需要添加条目 AddNewDebuffSlot(ActiveHandle); } } void UDebuffStatusWidget::OnGameplayEffectRemoved(const FActiveGameplayEffect EffectRemoved) { // 移除对应的UI条目 RemoveDebuffSlot(EffectRemoved.Handle); } void UDebuffStatusWidget::RefreshAllDebuffs() { if (BoundASC.IsValid()) { TArrayFActiveGameplayEffectHandle AllEffects BoundASC-GetActiveEffects(FGameplayEffectQuery()); for (const auto Handle : AllEffects) { if (FActiveGameplayEffect* ActiveGE BoundASC-GetActiveGameplayEffect(Handle)) { // 检查是否是Debuff通过Tag或Asset判断 if (IsDebuffEffect(ActiveGE-Spec.Def)) { AddNewDebuffSlot(Handle); } } } } }生命周期管理切记在UI控件BeginDestroy时解绑这些委托防止ASC回调时访问已销毁的UI对象。5. 常见问题排查与调试技巧在实际开发中即使理解了原理还是会遇到各种诡异的问题。下面是一些常见问题的排查清单和调试方法。5.1 Tag相关的问题问题1我认为GE应该授予Tag但目标ASC上查询不到。检查1确认GE是否被成功应用。在应用GE后立即在控制台打印或断点查看ActiveGameplayEffectHandle是否有效。检查2确认GE的Duration Policy不是Instant。瞬时效果在应用后立即执行Modifier然后就被清除了其GrantedTags不会被持久化添加。检查3检查GE的OngoingTagRequirements。如果目标ASC不满足RequireTags或拥有IgnoreTagsGE会被禁用其GrantedTags也会被移除。检查4对于堆叠GE确认堆叠数是否大于0。堆叠数为0时GrantedTags会被移除。调试工具在游戏中打开控制台~输入ShowDebug AbilitySystem可以查看角色当前的ASC状态包括所有ActiveGE和拥有的Tag。问题2一个可堆叠的GE被移除了但它的Tag还留在ASC上。原因很可能有其他GE也授予了相同的Tag。GAS的Tag是引用计数的。只有当所有授予该Tag的GE都被移除后Tag才会从ASC上消失。排查使用GetOwnedGameplayTags获取Tag容器然后使用GetTagCount查看该Tag的引用计数。或者遍历所有ActiveGE检查是哪个效果授予的。5.2 委托监听相关的问题问题1绑定的委托没有被触发。检查1确认委托绑定成功。在绑定代码后添加日志输出。检查2确认事件确实发生了。例如如果你监听OnGameplayEffectStackChange但GE的堆叠是通过InternalUpdateActiveGameplayEffectStackCount直接修改的而不是通过标准的ApplyGameplayEffectSpec这个委托可能不会触发。需要确认源码中该修改路径是否广播了委托。检查3对于OnEffectRemoved局部委托确认你绑定的是正确的、有效的FActiveGameplayEffect对象。如果效果在绑定前就已经失效委托自然无效。问题2游戏崩溃报错指向委托回调函数。几乎肯定是生命周期问题。监听者如UI、Actor被销毁了但没有解绑委托。当ASC广播事件时回调了一个已销毁对象的函数。解决方案在监听者的析构函数或EndPlay中解绑委托。使用AddUObject绑定它内部使用弱引用但为了安全仍建议显式解绑。对于全局委托使用RemoveAll或保存FDelegateHandle以便精确移除。// 在头文件中声明委托句柄 FDelegateHandle OnEffectAddedDelegateHandle; // 绑定委托时保存句柄 OnEffectAddedDelegateHandle MyASC-OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(...); // 在销毁时移除 void AMyListener::BeginDestroy() { if (MyASC.IsValid() OnEffectAddedDelegateHandle.IsValid()) { MyASC-OnActiveGameplayEffectAddedDelegateToSelf.Remove(OnEffectAddedDelegateHandle); } Super::BeginDestroy(); }5.3 堆叠逻辑相关的问题问题我设计了一个每层独立计时的Dot期望每层到期时触发一次事件但事件只在整个效果被移除时触发了一次。分析你很可能监听的是OnEffectRemoved整个GE移除或ASC的OnAnyGameplayEffectRemovedDelegate。对于“每层到期”你应该监听OnGameplayEffectStackChangeDelegate。当一层到期导致堆叠数从N变为N-1时这个委托会被触发参数中包含了新旧堆叠数。正确做法在OnGameplayEffectStackChangeDelegate的回调中判断NewStackCount OldStackCount且不是因为新应用导致的减少这通常需要结合上下文判断比如检查是否有对应的OnActiveGameplayEffectAdded事件刚刚发生则可以认为是层数到期减少触发你的“单层到期”逻辑。调试堆叠在ShowDebug AbilitySystem的显示信息中可以找到ActiveGE的列表其中会显示每个效果的Stack Count和Duration。对于每层独立计时的效果其Duration显示的是最早生效的那一层的剩余时间。6. 高级技巧与最佳实践在掌握了基础机制和排查方法后一些高级技巧能让你的GAS架构更健壮、更易维护。6.1 使用自定义的GameplayEffect计算类GameplayEffectExecutionCalculation对于复杂的、尤其是层数影响计算公式的效果强烈建议使用GameplayEffectExecutionCalculation简称Execution。你可以在Execution的Execute函数中直接访问FGameplayEffectSpec从而获取到当前的StackCount并进行复杂的计算。void UMyDamageExecution::Execute_Implementation(const FGameplayEffectCustomExecutionParameters ExecutionParams, FGameplayEffectCustomExecutionOutput OutExecutionOutput) const { // ... 获取Source和Target的ASC ... const FGameplayEffectSpec Spec ExecutionParams.GetOwningSpec(); int32 EffectStackCount Spec.GetStackCount(); float BaseDamage 10.0f; float TotalDamage BaseDamage * EffectStackCount; // 基于层数的计算 // ... 设置输出属性修改 ... }这样可以将层数逻辑完全封装在效果定义和计算类中外部系统只需关心应用效果即可。6.2 封装一个健壮的Debuff监听管理器为了避免在每个需要监听Debuff的类如角色、UI、技能中重复编写委托绑定和解绑逻辑可以创建一个单例或组件化的DebuffListenerManager。这个管理器的职责持有对玩家ASC的引用。集中绑定所有与Debuff相关的全局委托OnAdded,OnStackChanged,OnRemoved。提供注册/注销接口让其他系统如UI、技能订阅特定Tag的Debuff事件。在收到ASC的全局委托回调后根据Tag过滤并转发给对应的订阅者。妥善管理所有订阅者的生命周期使用弱引用。这实现了关注点分离业务代码只需关心“当我有虚弱Debuff时该做什么”而不需要处理GAS委托的细节。6.3 关于网络同步的考虑GAS是一个为网络游戏设计的框架。GameplayTag和ActiveGameplayEffect的堆叠数都是在服务器和客户端之间同步的。这意味着你大部分基于Tag和层数的逻辑在客户端也是可见且可预测的。但是委托的触发时机在客户端和服务器可能略有不同。通常效果的应用和移除由服务器权威决定然后同步给客户端。客户端的委托会在收到同步数据后触发。因此在编写监听逻辑时要确保它既能运行在服务器上处理游戏逻辑也能运行在客户端上更新UI、播放本地特效并且要处理好可能的延迟或预测Prediction带来的状态暂时不一致。一个常见的模式是在服务器的GE监听逻辑里执行核心游戏逻辑如计算伤害、触发连锁技能在客户端的GE监听逻辑里执行表现相关的操作如播放音效、粒子、更新UI。可以通过GetOwnerRole()来判断当前是运行在服务器还是客户端。
返回列表