GAS 系统深度学习指南UE 5.4 / 5.5

15 常见问题与排错手册

目标:把 GAS 开发中最常踩的坑集中成一份可检索的排错手册。每个问题都给出"现象 → 原因 → 解决",并对旧资料中的错误说法做了修正标注。


A. 标签与状态

A1. GE 移除了,Tag 却还在

  • 原因:GameplayTag 是引用计数的。Granted Tags 只是其中一个来源;同 Tag 可能同时被其他 GE、Ability 的 Activation Owned Tags、Loose Tag 持有,计数未归零。
  • 解决:排查所有来源;用 showdebug abilitysystem 查看 Tag 计数;确认移除的是对应 FActiveGameplayEffectHandle。

A2. Loose GameplayTag 没有同步给其他客户端

  • 修正标注:旧资料称"Loose Tag 会自动同步到客户端",错误。Loose Tag 默认本地、不复制;UE5 ReplicatedLooseGameplayTags 也只复制给拥有者客户端。
  • 解决:需要全员可见的状态用 GE Granted Tags 或 GameplayCue。

A3. Tag 层级匹配不生效

  • 原因:HasTag 是层级前缀匹配;HasTagExact 是精确匹配。用错 API,或用错了"父/子"关系。
  • 解决:确认要"父标签涵盖子标签"用 HasTag;要完全相等用 HasTagExact。子标签必须真的注册在父标签命名空间下(如 State.Stunned.Electric)。

B. 属性

B1. 属性修改"不生效"或"看起来没变"

  • 原因:Instant GE 改 BaseValue,Duration/Infinite 改 CurrentValue,二者观察方式不同;或 Modifier 没绑定正确的 FGameplayAttribute。
  • 解决:确认 GE 时长策略;确认 Modifier 绑定的属性(GetXxxAttribute());用调试视图看 Base/Current 两个值。

B2. 初学者把 BaseValue 当"最大值"用

  • 修正标注:这是旧资料也容易误导的点。BaseValue ≠ 最大值。最大值应建模为独立属性(MaxHealth),用 Clamp 限制上限是错误做法,会让"降上限的 Debuff"等逻辑失效。
  • 解决:设 MaxHealth 独立属性;PreAttributeChange/PostGameplayEffectExecute 中用 MaxHealth 做 Clamp。

B3. Clamp 在某些路径不生效

  • 原因:PreAttributeChange 只对 CurrentValue 变化生效;Instant GE 改 BaseValue 走 PostGameplayEffectExecute。
  • 解决:分别处理——对 CurrentValue 在 PreAttributeChange 限制;对 Instant 结算在 PostGameplayEffectExecute 里做最终 Clamp。

B4. 属性客户端不同步

  • 原因:属性没注册复制(DOREPLIFETIME),或复制模式限制(Mixed 只给拥有者客户端)。
  • 解决:注册 DOREPLIFETIME_CONDITION_NOTIFY + REPNOTIFY_Always;确认 ReplicationMode 符合预期;Meta 属性不要注册复制。

C. 效果(GameplayEffect)

C1. 冷却不生效 / 冷却永远结束不了

  • 原因:冷却 GE 时长策略配成了 Instant(立即执行并移除,永远不会"存在");或 Granted Tags 没配/配错;或共享冷却 Tag 冲突。
  • 解决:冷却 GE 必须 HasDuration;Granted Tags 加 Cooldown.X;技能 ActivationBlockedTags 包含该 Tag;用 GetCooldownTimeRemaining() 验证。

C2. 冷却剩余时间总是 0

  • 原因:GetCooldownTimeRemaining() 在 CDO(NonInstanced / 在类默认对象上调用)上返回 0。
  • 解决:在技能实例上调用(InstancedPerExecution/InstancedPerActor);确认拿到的 GA 对象是实例而非 CDO。

C3. DoT 堆叠行为不对(整层消失 / 永不结束)

  • 原因:堆叠到期策略选错。
  • 解决:想"每层独立计时"用 NeverRefresh + RemoveSingleStackAndRefreshDuration;想"叠加刷新整层"用 RefreshOnSuccessfulApplication + ClearEntireStack。

C4. SetByCaller 断言/报错

  • 原因:读取时该 Tag 没有对应数值(设值/读取的 Tag 不一致,或未设就读)。
  • 解决:确保 SetSetByCallerMagnitude(Tag, Value) 与 GE 内绑定的 Tag 完全一致;读取前先确认已设置。

C5. Instant GE 的 Granted Tags 不生效

  • 原因:Instant GE 不施加 Granted Tags(这是设计行为,Instant 不进入 Active 容器)。
  • 解决:需要"效果期间有 Tag"用 HasDuration/Infinite。

D. 能力(GameplayAbility)

D1. 技能激活后"卡住"、无法再次使用

  • 原因:最常见——忘了 EndAbility();或 CommitAbility 失败没走到结束分支。
  • 解决:所有代码路径(完成/打断/取消/出错)最终都到 EndAbility();给技能加调试日志确认生命周期。

D2. 使用 AbilityTask 但技能是 NonInstanced

  • 原因:NonInstanced 不支持 Task。
  • 解决:改用 InstancedPerExecution 或 InstancedPerActor。

D3. 客户端点技能没反应

  • 原因:InputID 没对上;GA 未授予;Tag 拦截(如带 State.Dead);NetExecutionPolicy 配置不当(LocalPredicted 却服务端才激活)。
  • 解决:showdebug abilitysystem 看技能列表与 Tag;确认 InputID 与授予时一致;确认策略。

D4. 客户端看不到技能效果

  • 原因:本地预测没启用(NetExecutionPolicy 非 LocalPredicted);或表现走的是模拟代理路径(本来就不该在模拟代理上跑 Ability)。
  • 解决:用预测键正确组织(第 11 章);表现层用 Cue/动画复制。

D5. GetCooldownTimeRemaining 与实例化策略

  • 修正标注:见 C2。这是官方论坛确认的经典问题:InstancedPerExecution 下在 CDO 调用返回 0。

E. 网络

E1. Listen Server 主机 Cue 触发两次

  • 原因:主机既是本地又是服务端,多播回环 + 本地执行导致重复。
  • 解决:Cue 表现处理要幂等;重复播放不叠加效果(粒子覆盖即可)。

E2. 客户端预测的扣蓝没有生效(服务端覆盖)

  • 原因:预测 GE 没带正确预测键 / 没走 ScopedPredictionWindow / 服务端结果不同。
  • 解决:ApplyGameplayEffectSpecToSelf_Predictive 等预测 API;确认键在链路中传递;以服务端值为准是正常行为。

E3. Minimal 模式下客户端看不到 GE 变化

  • 修正标注:这是正确行为——Minimal 模式 GE 不复制给任何客户端(旧资料误写为"同步给 Owner Client")。
  • 解决:需要客户端感知的数值用属性复制(但 Minimal 也不复制属性);需要表现用 GameplayCue。AI 用 Minimal 是标准做法。

E4. Mixed 模式属性没同步到其他玩家

  • 原因:正确行为——Mixed 下属性只复制给拥有者客户端。其他玩家通过 Cue/UI 事件感知。
  • 解决:设计上不要依赖"读别人属性";用 GameplayCue 传递数值(如飘字)。

E5. 复制模式 Mixed 但效果不复制

  • 原因:Mixed 要求 OwnerActor 的 Owner 是 Controller。若 ASC 在 Character 上且 Owner 不是 Controller,Mixed 的拥有者复制路径不成立。
  • 解决:用 PlayerState 承载 ASC(天然满足),或确保 Character 的 Owner 是 Controller。

F. 全局与初始化

F1. TargetData 报错/崩溃(UE 4.24~5.2)

  • 原因:未调用 UAbilitySystemGlobals::Get().InitGlobalData()。
  • 解决:在 UAssetManager::StartInitialLoading() 中调用。UE 5.3+ 引擎自动调用,无需手动(旧资料"必须手动 + 配置 bAutoInitGlobalData"说法不准确)。

F2. 想调"预测深度"找不到配置

  • 修正标注:旧资料所谓 MaxPredictionDepth=4 配置不存在。
  • 解决:预测深度靠合理组织预测边界与依赖键,而不是调配置。

F3. 调试命令敲了没反应

  • 修正标注:AbilitySystem.Debug.Category.Prediction、net.PredictionKey.MaxDelay 等不存在。
  • 解决:用 showdebug abilitysystem、AbilitySystem.Debug.NextCategory/NextTarget、Log AbilitySystem Verbose。

G. 迁移

G1. UE4 项目迁到 UE5,InitGlobalData 相关代码报错/失效

  • 解决:UE5.3+ 可移除手动调用;检查 GameplayTags 的 Fast Replication 配置;Enhanced Input 迁移(旧 Input 系统节点在蓝图转 EIA 节点)。

G2. 蓝图技能节点找不到(C++ 没暴露)

  • 解决:给函数/属性加 BlueprintCallable / BlueprintReadOnly / BlueprintPure;Task 工厂用 BlueprintInternalUseOnly + HidePin 规范生成蓝图节点。

H. 运行时报错与编译链接

H1. "Can't activate LocalOnly or LocalPredicted ability when not local!"

  • 原因:客户端的 ASC 未正确初始化(只在服务端 PossessedBy 里调了 InitAbilityActorInfo,客户端没调)。
  • 解决:客户端也要初始化。服务端在 PossessedBy、客户端在 OnRep_PlayerState 都调用 InitAbilityActorInfo(第 03 章的双路径初始化)。
void AMyCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);
    if (AbilitySystemComponent)
        AbilitySystemComponent->InitAbilityActorInfo(this, this);   // 服务端
}

void AMyCharacter::OnRep_PlayerState()
{
    Super::OnRep_PlayerState();
    if (AbilitySystemComponent)
        AbilitySystemComponent->InitAbilityActorInfo(this, this);   // 客户端
}

H2. ScriptStructCache 错误导致客户端断开

  • 原因:未调用 UAbilitySystemGlobals::Get().InitGlobalData()(UE 4.24 ~ 5.2 之间必须手动)。
  • 解决:在 UAssetManager::StartInitialLoading() 中调用。UE 5.3+ 引擎自动调用,无需手动(第 13 章 1.2)。

H3. 动画蒙太奇不复制到客户端

  • 原因:在 GA 里用了 PlayMontage 蓝图节点 / AnimInstance->Montage_Play,这些不经过 ASC 复制。
  • 解决:用 UAbilityTask_PlayMontageAndWait(经 ASC 复制蒙太奇):
// ❌ 错误:PlayMontage 不复制
// ✓ 正确:PlayMontageAndWait 会通过 ASC 复制
UAbilityTask_PlayMontageAndWait* MontageTask =
    UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, NAME_None, Montage);
MontageTask->OnCompleted.AddDynamic(this, &UMyAbility::OnMontageCompleted);
MontageTask->ReadyForActivation();

H4. 复制蓝图 Actor 后 AttributeSet 指针为 nullptr(UE-81109)

  • 原因:UE 已知 Bug(UE-81109),复制蓝图 Actor 时会把头文件中声明的 AttributeSet 指针设为 nullptr。
  • 解决:不在头文件声明 AttributeSet 指针,而在 PostInitializeComponents() 中 AddSet<T>() 添加:
void AMyPlayerState::PostInitializeComponents()
{
    Super::PostInitializeComponents();
    if (AbilitySystemComponent)
        AbilitySystemComponent->AddSet<UMyAttributeSet>();
}
// 读取属性时通过 ASC:
float AMyPlayerState::GetHealth() const
{
    return AbilitySystemComponent
        ? AbilitySystemComponent->GetNumericAttribute(UMyAttributeSet::GetHealthAttribute())
        : 0.f;
}

H5. MarkPropertyDirty 链接错误(LNK2019)

  • 现象:
error LNK2019: unresolved external symbol
"__declspec(dllimport) void __cdecl UEPushModelPrivate::MarkPropertyDirty(int,int)"
  • 原因:WITH_PUSH_MODEL 在不同编译单元中定义不一致。
  • 解决:在 Build.cs 中添加 NetCore 模块:
PublicDependencyModuleNames.AddRange(new string[] {
    "Core", "CoreUObject", "Engine", "NetCore"   // 添加 NetCore
});

H6. 枚举名路径警告(C4996)

  • 警告:
warning C4996: 'FGameplayAbilityInputBinds': Enum names are now represented by path names.
  • 原因:FGameplayAbilityInputBinds 用 FString 表示枚举名已被弃用。
  • 解决:改用 FTopLevelAssetPath(UE 5.1+):
FTopLevelAssetPath AbilityEnumAssetPath =
    FTopLevelAssetPath(FName("/Script/MyGame"), FName("EMyAbilityInputID"));
AbilitySystemComponent->BindAbilityActivationToInputComponent(
    InputComponent,
    FGameplayAbilityInputBinds(FString("ConfirmTarget"),
        FString("CancelTarget"),
        AbilityEnumAssetPath,
        static_cast<int32>(EMyAbilityInputID::Confirm),
        static_cast<int32>(EMyAbilityInputID::Cancel)));

下一章:附录A 版本演进与迁移