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

02 GameplayTags 标签系统

目标:掌握 GAS 的"语言"——标签的层级机制、容器/查询 API、引用计数原理、事件监听、Native GameplayTags 的最佳实践,以及(最重要的)标签在网络中到底怎么复制。


2.1 标签是什么

GameplayTags 是 UE 的层级化标签系统,用点分隔的字符串标识游戏中的概念、状态和类别。它是 GAS 的基石——GAS 几乎所有的判断逻辑(能不能放技能、目标是否免疫、当前什么状态)都基于标签。

State.Debuff.Stun              // 状态:眩晕(子标签归类)
State.Debuff.Silence           // 状态:沉默
State.Buff.Haste               // 状态:加速
Ability.Skill.Fireball         // 能力:火球术
Ability.Passive.ArmorStack     // 被动:护甲叠层
Damage.Type.Fire               // 伤害类型:火焰
Cooldown.Ability.Fireball      // 冷却:火球术
GameplayCue.Damage.Fire        // 表现:火焰伤害
Event.MeleeHit                 // 事件:近战命中
Input.Attack                   // 输入:攻击

所有标签必须在项目设置 → Gameplay Tags(或 DefaultGameplayTags.ini)中预先注册后才能使用,不能在运行时随意创建。

通俗理解:把 GameplayTag 想象成文件系统路径——State.Debuff.Stun 就像 C:/Users/Documents/。这种设计让你可以查询"所有 Debuff"或"所有 State",就像查询一个文件夹下的所有文件。

为什么用标签替代 bool/enum

传统方式的问题 标签的解法
布尔值爆炸:状态越多变量越多 一个容器容纳任意多个标签
枚举互斥:不能同时 Stunned 和 Sprinting 标签天然可共存
扩展困难:新增状态要改枚举和所有引用 新增标签零侵入
网络同步低效:每个布尔独立复制 标签统一容器、可优化复制

2.2 标签的定义与管理

方式一:编辑器(推荐)

  1. Edit → Project Settings → GameplayTags
  2. 点击 Import Tags from Config 或直接在界面中添加
  3. 支持:创建层级标签、重命名(自动创建重定向,旧资产仍可用)、搜索引用(Reference Viewer)、删除

最佳实践:尽量在编辑器中管理标签。需要重命名时,建议"新建标签 → 手动更新所有引用 → 删除旧标签",避免留下重定向。

方式二:DefaultGameplayTags.ini

[/Script/GameplayTags.GameplayTagsSettings]
+GameplayTagList=(Tag="State.Debuff.Stun",DevComment="角色被眩晕")
+GameplayTagList=(Tag="State.Dead",DevComment="角色已死亡")
+GameplayTagList=(Tag="Ability.Skill.FireBall",DevComment="火球术")
+GameplayTagList=(Tag="GameplayCue.FireBall.Impact",DevComment="火球命中特效")

方式三:插件 .ini(第三方插件场景)

// 插件 StartupModule 中把插件 Tags 目录加入搜索路径
UGameplayTagsManager::Get().AddTagIniSearchPath(
    IPluginManager::Get().FindPlugin(TEXT("MyPlugin"))->GetBaseDir()
    / TEXT("Config") / TEXT("Tags"));

2.3 核心类型

类型 说明
FGameplayTag 单个标签的轻量表示,本质是封装后的 FName,比较高效
FGameplayTagContainer 标签集合容器,内部用哈希结构,比 TArray<FGameplayTag> 高效得多
FGameplayTagCountContainer 引用计数容器,ASC 内部维护标签计数的核心
FGameplayTagQuery 复杂查询表达式,支持 AND/OR/NOT 组合
// 获取标签(FName 查询,命中全局 TagManager 缓存)
FGameplayTag StunTag = FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun"));

// 容器操作
FGameplayTagContainer Tags;
Tags.AddTag(StunTag);
Tags.AddTag(FGameplayTag::RequestGameplayTag(FName("State.Buff.Haste")));

// 查询
Tags.HasTag(StunTag);                    // 含(层级匹配)
Tags.HasTagExact(StunTag);               // 精确匹配
Tags.HasAny(OtherContainer);             // 有任一
Tags.HasAll(OtherContainer);             // 有全部
Tags.GetGameplayTagArray(OutArray);      // 导出数组
Tags.RemoveTag(StunTag);                 // 移除

常见陷阱:不要用 TArray<FGameplayTag> 替代 FGameplayTagContainer。容器内部使用高效哈希结构,在频繁查询场景性能差异明显。

2.3.1 匹配规则(重点)

函数 语义
TagA.Matches(TagB) TagA 是 TagB 的子标签或相同时 true(层级前缀匹配)
TagA.MatchesExact(TagB) 完全相同时 true
Container.HasTag(Tag) 容器内有 Tag 或其子标签时 true
Container.HasTagExact(Tag) 容器内有完全相等的 Tag 时 true

假设注册了 State.Debuff.Stun、State.Debuff.Stun.Electric:

  • HasTag(State.Debuff.Stun):只要容器里有 Stun 或 Stun.Electric 都为 true;
  • HasTagExact(State.Debuff.Stun):只有精确的 Stun 才算。

这是 GAS 状态判断的核心机制:Ability 的 Activation Blocked Tags 设为 State.Stunned,只要 Actor 拥有 State.Stunned 或任意子标签,技能就无法激活。设计标签层级时"父标签表大类、子标签表细分",子标签天然继承父标签的"拦截/需求"语义。

2.4 FGameplayTagCountContainer 与 TagMapCount(引用计数)

ASC 内部用 FGameplayTagCountContainer 维护每个标签的引用计数(TagMapCount):

TagMap
├─ State.Debuff.Stun   count: 1   ← HasTag() == true
├─ State.Buff.Haste    count: 2
└─ State.Dead          count: 0   ← 标签存在但计数为 0,HasTag() == false

关键行为:

  • TagMapCount > 0 → HasTag() 返回 true;
  • TagMapCount == 0 → 即使标签仍在容器,HasTag() 也返回 false;
  • 计数从 0→1 或 1→0 → 触发 NewOrRemoved 事件;
  • 计数在非零值之间变化 → 只触发 AnyCountChange 事件。

为什么是引用计数:同一个标签可能被多个来源持有(多个眩晕效果、多个 GE 的 Granted Tags),只有所有来源都释放后计数才归零、标签才真正消失。这就是"GE 移除了 Tag 却还在"的根源。

// 手动操作 TagMapCount(仅用于 LooseGameplayTags,且小心使用)
AbilitySystemComponent->SetTagMapCount(DeadTag, 0);   // 强制重置
int32 Count = AbilitySystemComponent->GetTagCount(StunTag);

2.5 标签变化事件监听(重要能力)

ASC 提供委托监听标签变化,是"状态变化 → 触发游戏逻辑"的标准方式:

// 仅在标签新增或移除时触发
AbilitySystemComponent->RegisterGameplayTagEvent(
    FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")),
    EGameplayTagEventType::NewOrRemoved
).AddUObject(this, &AMyPlayerState::OnStunTagChanged);

// 或监听任何计数变化
AbilitySystemComponent->RegisterGameplayTagEvent(
    FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")),
    EGameplayTagEventType::AnyCountChange
).AddUObject(this, &AMyPlayerState::OnStunTagAnyChange);

回调签名:

void AMyPlayerState::OnStunTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
    if (NewCount > 0)
    {
        // 被眩晕:取消除"眩晕不可打断"外的所有技能
        FGameplayTagContainer ToCancel;
        ToCancel.AddTag(FGameplayTag::RequestGameplayTag(FName("Ability")));
        FGameplayTagContainer ToIgnore;
        ToIgnore.AddTag(FGameplayTag::RequestGameplayTag(FName("Ability.NotCanceledByStun")));
        AbilitySystemComponent->CancelAbilities(&ToCancel, &ToIgnore);
    }
    else
    {
        // 眩晕结束
    }
}

为什么用事件监听:事件驱动比轮询高效,且不会遗漏状态变化。典型的应用:眩晕时取消技能、死亡时禁用输入、Buff 变化时更新 UI。

2.6 标签的网络复制(本资料重点修正)

关于"标签会不会同步给别人",官方文档与源码给出以下准确结论:

  1. 通过 GE 的 Granted Tags 添加的标签:随该 GE 的复制范围走。 - Full 模式 → 复制给所有客户端; - Mixed 模式 → 复制给拥有者客户端(并非"所有客户端",旧资料笼统说"自动同步到所有客户端"不准确); - Minimal 模式 → GE 不复制给任何人,标签同样不复制。

  2. Loose GameplayTags(AddLooseGameplayTag 手动添加):默认不复制,必须在本地手动管理移除。 - UE5 的 ASC 提供 ReplicatedLooseGameplayTags 容器,但它只复制给拥有者客户端(复制条件 COND_ReplayOrOwner),不会广播给所有客户端。 - 结论:想让"所有客户端"都能看到的标签,不要用 Loose Tag,应改用 GE 的 Granted Tags,或通过 GameplayCue 让客户端自行感知。

  3. Loose Tag 的典型用途:客户端立即响应状态(如 HP 归零立刻进入死亡表现),不想等待 GE 复制延迟。示例项目用 State.Dead 作 Loose Tag 让拥有者客户端即时反应。

旧资料中"Loose Tag 会自动同步到客户端/所有客户端"的说法是错的,请以本节为准。

Fast Replication(高频标签优化)

  • 开启:Edit → Project Settings → GameplayTags → 勾选 Enable Fast Replication;
  • 原理:网络传输用压缩索引而非完整字符串,大幅减少带宽;
  • 前提:服务器与客户端标签列表一致(默认满足);
  • 最佳实践:始终启用;把高频标签(State.Dead、State.Debuff.Stun 等)加入"常用复制标签"列表进一步优化。

2.7 LooseGameplayTags vs GE 赋予的标签(决策表)

维度 GE Granted Tags LooseGameplayTags
生命周期 GE 管理(Duration 到期/Infinite 移除) 手动管理
复制 随 GE 复制(按复制模式) 不复制(或仅拥有者)
触发事件 随 GE 施加/移除 Add/RemoveLooseGameplayTag 触发
适用 需要全员/拥有者感知的状态 本地即时状态、临时逻辑

常见陷阱:不要混用 AddLooseGameplayTag() 和手动 SetTagMapCount()。优先用 Add/RemoveLooseGameplayTag()(内部正确触发事件),只在特殊场景(如重生强制重置)才直接操作 TagMapCount。

2.8 C++ 中获取标签引用(三种方式)

方式一:运行时查找

FGameplayTag StunTag = FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun"));

方式二:Native GameplayTags(UE 5.x 推荐,编译时零开销)

// .h
#include "NativeGameplayTags.h"
UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Debuff_Stun);
UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Dead);

// .cpp
UE_DEFINE_GAMEPLAY_TAG(State_Debuff_Stun, "State.Debuff.Stun");
UE_DEFINE_GAMEPLAY_TAG(State_Dead, "State.Dead");

// 使用:编译时解析,零运行时开销
FGameplayTag Tag = State_Debuff_Stun;

方式三:缓存成员引用

// .h
UPROPERTY() FGameplayTag DeadTag;
UPROPERTY() FGameplayTag StunTag;

// .cpp 构造函数
DeadTag = FGameplayTag::RequestGameplayTag(FName("State.Dead"));

最佳实践:频繁使用的标签务必缓存(成员变量或 Native Tag)。每次 RequestGameplayTag() 虽命中内部缓存,但重复调用仍是坏习惯。Native GameplayTags 是最推荐方式。

2.9 GameplayTagManager 高级操作

#include "GameplayTagManager.h"

// 获取所有子标签
TArray<FGameplayTag> ChildTags;
UGameplayTagManager::Get().RequestGameplayTagChildren(
    FGameplayTag::RequestGameplayTag(FName("State.Debuff")), ChildTags);
// → State.Debuff.Stun, State.Debuff.Silence, ...

// 获取父标签
FGameplayTag Parent = UGameplayTagManager::Get().RequestGameplayTagParent(StunTag);
// → State.Debuff

// 简单名称
FString SimpleName = StunTag.GetSimpleTagName().ToString();   // "Stun"

TagManager 内部把标签存为关系节点(parent/child),比字符串操作高效。

2.10 标签过滤(蓝图/编辑器 UX)

// 只显示 GameplayCue 开头的标签
UPROPERTY(EditAnywhere, Meta = (Categories = "GameplayCue"))
FGameplayTag CueTag;

// 函数参数过滤:蓝图中只能选 Ability.* 标签
UFUNCTION(BlueprintCallable)
void ActivateByTag(FGameplayTag AbilityTag);   // 配合 Meta 使用

2.11 标签命名规范(强烈建议项目级统一)

Ability.     # 能力标识        Ability.Skill.Fireball
State.       # 状态标识        State.Stunned / State.Debuff.Stun / State.Combat
Cooldown.    # 冷却标识        Cooldown.Ability.Fireball
Damage.      # 伤害类型        Damage.Type.Fire
Effect.      # 效果类别        Effect.Buff / Effect.Debuff
Event.       # 事件标识        Event.MeleeHit / Event.Death
Data.        # SetByCaller 数值标识  Data.Damage / Data.Cooldown
Input.       # 输入标识        Input.Attack / Input.Fireball
GameplayCue. # 表现层标识      GameplayCue.Damage.Fire

一个实用惯例:区分"类型前缀"和"实例路径"。Cooldown.Ability.Fireball 表明"火球术的冷却",多个技能引用同一个冷却前缀即可实现共享冷却。

2.12 本章验证

  • [ ] 在项目里注册一套完整 Tag(Ability./State./Cooldown./Data./Event./GameplayCue. 前缀);
  • [ ] 用 FGameplayTagContainer 完成一次 HasTag / HasAny / HasAll 判断并理解层级匹配;
  • [ ] 用 RegisterGameplayTagEvent 实现"眩晕时取消技能";
  • [ ] 用 Native GameplayTags 宏替换一处运行时查找;
  • [ ] 能说出"为什么 GE 移除后 Tag 可能还在"(引用计数);
  • [ ] 能准确说出 Loose Tag 与 GE Granted Tag 的网络复制差异;
  • [ ] 开启 Fast Replication 并理解前提。

下一章:03 AbilitySystemComponent 与初始化