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 标签的定义与管理
方式一:编辑器(推荐)
- Edit → Project Settings → GameplayTags
- 点击 Import Tags from Config 或直接在界面中添加
- 支持:创建层级标签、重命名(自动创建重定向,旧资产仍可用)、搜索引用(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 标签的网络复制(本资料重点修正)
关于"标签会不会同步给别人",官方文档与源码给出以下准确结论:
-
通过 GE 的 Granted Tags 添加的标签:随该 GE 的复制范围走。 - Full 模式 → 复制给所有客户端; - Mixed 模式 → 复制给拥有者客户端(并非"所有客户端",旧资料笼统说"自动同步到所有客户端"不准确); - Minimal 模式 → GE 不复制给任何人,标签同样不复制。
-
Loose GameplayTags(
AddLooseGameplayTag手动添加):默认不复制,必须在本地手动管理移除。 - UE5 的 ASC 提供ReplicatedLooseGameplayTags容器,但它只复制给拥有者客户端(复制条件COND_ReplayOrOwner),不会广播给所有客户端。 - 结论:想让"所有客户端"都能看到的标签,不要用 Loose Tag,应改用 GE 的 Granted Tags,或通过 GameplayCue 让客户端自行感知。 -
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 并理解前提。