一、GameplayTag 不是什么黑科技
就是一个带层级结构的字符串,被引擎预注册、哈希化,比较时是整数比较(不是字符串比较):
GameplayTag: Ability.Sofia.RevealTwo
层级结构: Ability └── Sofia └── RevealTwo引擎启动时,所有在 DefaultGameplayTags.ini 或 DataTable 中声明的 Tag 被加载到一个全局字典里,每个 Tag 获得一个唯一的整数 ID。运行时任何 TagA == TagB 的比较都是 O(1) 的整数比较。
二、为什么不用 Bool / Enum
布尔值的死亡螺旋
// 一个角色身上可能有这些状态bool bIsStunned;bool bIsDead;bool bIsInvincible;bool bIsInAir;bool bIsUsingSkill;bool bIsInteracting;bool bIsInCutscene;// ... 12 个 bool 后,没人知道 bIsStunned && !bIsDead && bIsInAir 是什么语义布尔值的问题:
- 互斥关系靠
if维护——哪天忘了加if (!bIsDead),死了还能被眩晕 - 组合查询困难——“角色是否有任何一个控制阻断的 debuff”需要
bIsStunned \|\| bIsFrozen \|\| bIsFeared \|\| ...
GameplayTag 替代
// 不用 bool——用 TagASC->AddLooseGameplayTag(Tag_Status_Stunned); // 眩晕ASC->AddLooseGameplayTag(Tag_Status_Dead); // 死亡
// 查询:有任何一个 "控制阻断" 的 Tag 吗?FGameplayTagContainer BlockingTags;BlockingTags.AddTag(Tag_Status_Stunned);BlockingTags.AddTag(Tag_Status_Frozen);BlockingTags.AddTag(Tag_Status_Feared);bool bBlocked = ASC->HasAnyMatchingGameplayTags(BlockingTags);
// 查询:有 Status.Dead 的所有子 Tag 吗?(包括 Status.Dead.Player, Status.Dead.NPC)bool bDead = ASC->HasMatchingGameplayTag(FGameplayTag::RequestGameplayTag("Status.Dead"));Tags 天然支持层级查询——HasTag(Status.Dead) 自动匹配 Status.Dead.Player、Status.Dead.NPC。
三、Tag 的匹配模式
请求 Tag: Status.Dead ← "有没有死亡的 Tag"匹配: Status.Dead ✓ 精确匹配 Status.Dead.Player ✓ 子 Tag Status.Alive ✗ 不同分支 Status ✗ 父 Tag(只向下匹配)Tag 只在层级树上向下匹配,不向上。
TagQuery
复杂的组合条件可以用 FGameplayTagQuery 表达:
// "角色没有死亡、且具有至少一个主动技能、且没有被沉默"FGameplayTagQuery Query = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .AllTagsMatch() .AddTag(Tag_Status_Alive) .AddAnyTag(Tag_Ability_Active) .AddTag(FGameplayTagQueryExpression() .NoTagsMatch() .AddTag(Tag_Status_Silenced) ));// 一个表达式搞定——不用 3 个 if这个 Query 可以被序列化——存在 DataTable 里、存在 GE 配置里。策划可以在编辑器里配复杂的条件组合,不需要程序员写 if。
四、Tags 作为系统间的通信协议
Tags 最大的价值不是替代 Bool——是让不同的子系统通过 Tags 通信而互相不知道对方的存在。
GAS 赋予 Tag → 动画系统监听到 Tag → 切动画GAS 赋予 Tag → UI 监听到 Tag → 显示 Buff 图标GAS 赋予 Tag → 输入系统监听到 Tag → 禁用移动输入GAS 赋予 Tag → AI 监听到 Tag → 切换到被动行为GAS 只知道”我给了这个角色一个 Status.Stunned 的 Tag”。收到 Tag 的系统各自决定自己的反应——动画系统说”播放眩晕动画”、输入系统说”禁用输入”、AI 说”停止当前行为”。各方对 GAS 无依赖,只依赖 Tags。
五、UE 中 Tags 的使用场景
| 子系统 | 用 Tags 做什么 |
|---|---|
| GAS | Ability Tags、GE GrantedTags、ActivationBlockedTags、Attribute 查询 |
| 动画 | AnimBlueprint 中 HasTag 节点——“角色被眩晕了吗 → 切眩晕动画” |
| 输入 | InputAction 绑定到 Tag——按 Q 键激活 Tag 为 Ability.Sofia.Reveal 的技能 |
| AI | Behavior Tree Decorator Blackboard Based Tag Query |
| UI | GameplayTag 驱动的 UI 显示/隐藏——BuffIcon 在 Tag 存在时显示 |
| 物理 | 碰撞通道的 Tag 过滤(Physical Material 上的 Tag) |
| DataTable | 行级别的 Tag 过滤——“这个道具只在 Tags 中包含 Season.Halloween 时加载” |
Tags 已经渗透了 UE 几乎所有子系统——你现在写 UE 代码,不碰 Tags 几乎不可能。
六、Tags 的管理
层级命名规范
Status.DeadStatus.StunnedStatus.RootedStatus.Silenced
Ability.Offensive.FireballAbility.Offensive.LightningAbility.Defensive.ShieldAbility.Passive.Swordsmanship
Event.Combat.HitEvent.Combat.KillEvent.Game.RoundStartEvent.Game.RoundEnd
Input.Skill.QInput.Skill.EInput.Move.Jump规范层级比技术实现更重要。 建议在项目初期就定好 Tag 命名规范——Category.SubCategory.SpecificName。Tag 多了后重构是很痛苦的(需要改所有引用的资产)。
DataTable 管理
UE 推荐把所有 Tag 声明在 DataTable 里(DT_GameplayTags):
| Tag | Comment ||------------------------------|------------------|| Status.Stunned | 眩晕 || Status.Frozen | 冰冻 || Ability.Mage.Fireball | 法师火球技能 || GamePhase.Exploration | 探索阶段 || GamePhase.Combat | 战斗阶段 |引擎启动时加载这个 DataTable → 注册所有 Tag 到 Global Dictionary。没有在 DataTable 里声明的 Tag 运行时也能用,但编辑器里不会有自动补全提示。
七、总结
| 概念 | 一句话 |
|---|---|
| Tag 本质 | 预注册的层级字符串 → 运行时整数 ID → O(1) 比较 |
| 层级匹配 | 查 Status.Dead 自动匹配 Status.Dead.Player(向下查) |
| TagQuery | 复杂的 Tag 组合条件——可序列化,策划可配 |
| 系统间解耦 | GAS 写 Tag,动画/UI/输入/AI 各自监听——互不依赖 |
| DataTable 管理 | 统一声明 Tag 列表,编辑器有自动补全,后续重构可控 |
GameplayTag 是 UE Gameplay 架构的”结缔组织”——它把各个子系统缝合在一起,同时保持它们之间的松耦合。如果你在 UE 项目里写了超过 5 个 bool 来表达角色状态,停下来,用 Tags 代替。