Inspector 控件
隐藏字段、注册自定义 UI Toolkit 控件与校验规则,复用 GCS 的字段控件和下拉菜单
Workbench 会根据序列化字段和 Runtime 属性选择控件,通过 Editor 注册表可以隐藏字段、替换单个控件或添加校验规则,其余字段继续由 Workbench 绘制
自定义控件与校验代码属于 Editor 程序集,派生内容类及其序列化数据仍放在Inspector 扩展定义的运行时程序集中
GCS-Samples.zip 中的 Samples~/InspectorExtensions/ 包含 Runtime 和 Editor 扩展示例,在 Unity 项目外解压,再把需要的目录复制到项目自有位置
引用 Editor 程序集
创建一个 Editor-only Assembly Definition,引用项目内容程序集、GCS Editor 与 Shared Editor
{
"name": "YourGame.GCS.Content.Editor",
"references": [
"YourGame.GCS.Content",
"TinyGiants.GCS.Runtime",
"TinyGiants.GCS.Editor",
"TinyGiants.Shared.Editor"
],
"includePlatforms": [
"Editor"
]
}
该程序集不会进入 Player Build,运行时代码不能引用 GCSInspectorVisibilityRegistry、GCSInspectorControlRegistry、GCSInspectorValidationRegistry、TGDropdown 或 UI Toolkit Editor 控件
隐藏继承字段
使用 GCSInspectorVisibilityRegistry 为指定类型隐藏字段,字段声明和已保存数据保持不变,在 Editor 程序集中注册字段名,每次 Domain Reload 后重新注册
[InitializeOnLoad]
public static class SampleInspectorRegistration
{
static SampleInspectorRegistration()
{
GCSInspectorVisibilityRegistry.Hide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
}
}
SamplePlayer 的三个字段会从 Workbench 中隐藏,整组字段被隐藏时也不会留下空折叠区,字段仍可保存和读取,GamePlayerUnit 与其他玩家类型不受影响
为基础内容类型注册的隐藏规则也会作用于其派生类型,Editor 集成被停用或替换时,可以显式移除注册
GCSInspectorVisibilityRegistry.Unhide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
注册一个字段控件
GCSInspectorControlRegistry 根据内容类型与序列化字段名查找控件,应使用 [InitializeOnLoad] 或 [InitializeOnLoadMethod] 在每次 Domain Reload 后重新注册
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using UnityEditor;
using UnityEngine.UIElements;
[InitializeOnLoad]
public static class SampleInspectorRegistration
{
static SampleInspectorRegistration()
{
GCSInspectorVisibilityRegistry.Hide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
GCSInspectorControlRegistry.Register<SampleCard>(
nameof(SampleCard.SpellLoadout),
CreateSpellLoadoutControl);
GCSInspectorValidationRegistry.Register<SampleCard>(
"sample.card.progression",
ValidateProgression);
}
private static VisualElement CreateSpellLoadoutControl(
GCSInspectorFieldContext context)
{
context.UseFullWidth = true;
return new SampleSpellLoadoutField(context);
}
private static IEnumerable<GCSInspectorValidationResult> ValidateProgression(
SampleCard card)
{
if (card.RequiredLevel < 1)
{
yield return new GCSInspectorValidationResult(
GCSInspectorValidationSeverity.Error,
"Required Level must be at least 1.",
fieldLabel: "Required Level",
category: "Sample Progression",
propertyPath: nameof(SampleCard.RequiredLevel));
}
}
}
SampleCard.SpellLoadout 由 SampleSpellLoadoutField 绘制,RequiredLevel 小于 1 时会报告校验错误,这些注册作用于指定类型及其派生类型,重复注册同一类型的同一字段会替换原有控件工厂,基类和派生类都注册了该字段时,优先使用最接近当前类型的注册
使用序列化字段上下文
通过 GCSInspectorFieldContext 读取和修改字段,自定义控件也能使用 Workbench 的序列化、Undo 和刷新功能
| 成员 | 用途 |
|---|---|
Target | 当前具体 ScriptableObject |
SerializedObject | 当前目标的序列化包装 |
PropertyPath | 被替换字段的准确路径 |
Field | 反射得到的字段元数据 |
Label | 经过 GCSLabel、InspectorName 或自动美化后的最终显示标签 |
GetProperty() | 更新 SerializedObject 并解析当前属性 |
Modify(undoLabel, mutation) | 应用序列化修改、记录 Undo、标记目标 Dirty 并刷新 Workbench |
RegisterRefresh(refresh) | 在 Undo/Redo 或外部序列化变化后刷新控件 |
UseFullWidth | 让返回元素占用完整 Section 宽度 |
数组编辑、Undo 或 Inspector 重建后,旧的 SerializedProperty 可能失效,每次读取字段时用 GetProperty() 获取当前属性,通过 RegisterRefresh 更新界面值时则用 SetValueWithoutNotify,避免再次触发编辑回调,需要整行显示的复合控件可设置 UseFullWidth
private static VisualElement BuildTimelineControl(
GCSInspectorFieldContext context)
{
context.UseFullWidth = true;
var timeline = new VisualElement();
timeline.AddToClassList("sample-timeline");
return timeline;
}
控件工厂应返回 UI Toolkit 的 VisualElement,使用 IMGUI 时需要放进 IMGUIContainer 并自行处理焦点和序列化变化,工厂返回 null 或抛出异常时,Workbench 会改用字段属性指定的控件或普通序列化控件,并将异常写入 Console
直接复用 GCS 字段控件
控件工厂也可以直接创建以下 GCS 内置控件
| 控件 | 用途 |
|---|---|
GCSSelectField | 纯选择字符串字段 |
GCSEditableSelectField | 可编辑字符串与选项下拉框 |
GCSTagField | 逗号分隔值与多选下拉框 |
GCSChipMultiSelectField | 有序、可移除胶囊项与多选下拉框 |
GCSEnumDropdown<T> | 使用 GCS 下拉表现的枚举 Picker |
GCSBuffDebuffPicker | 带自定义标签的双段 Boolean 控件 |
GCSDescriptionField | 支持 Token 与语义引用的 GCS Description Editor |
GCSSubAssetField | 从已注册 GCS 内容中选择紧凑行或卡片式引用 |
GCSSubAssetListField | GCS 内容引用的序列化 List Editor |
GCSFlowGraphField | FlowGraph 按钮与入口或敌方意图静态摘要 |
GCSDeckEntriesField | GameDeck 卡牌数量 Picker 与摘要 |
字段属性能满足需求时,直接使用 Runtime 属性,需要组合控件、条件交互或项目命令时,再注册控件工厂
添加项目校验
通过 GCSInspectorValidationRegistry 注册校验函数,Workbench 会与内置校验一起执行,每项注册使用内容类型和 ID 标识,重复注册会替换原项,注册到基类的规则也会检查派生对象
GCSInspectorValidationRegistry.Register<SampleEncounter>(
"sample.encounter.rewards",
encounter => ValidateRewards(encounter));
Validator 返回零个或多个 GCSInspectorValidationResult
| 值 | Workbench 结果 |
|---|---|
Warning | 向当前数据库 Issue Badge 加入 Warning |
Error | 向当前数据库 Issue Badge 加入 Error |
Category | 显示项目定义的问题分类 |
Message | 显示在内容对象名称之后 |
PropertyPath | 精确的 SerializedProperty.propertyPath,Workbench 会优先用它定位并闪烁一个 Inspector 字段行 |
FieldLabel | PropertyPath 为空或无法解析时使用的显示标签回退值 |
字段校验使用五参数构造函数 (severity, message, fieldLabel, category, propertyPath),通过 propertyPath 指定位置,顶层字段使用 nameof(SampleCard.RequiredLevel),嵌套字段使用 Stats.BaseDamage,数组和 List<T> 使用包含 Array.data[0] 的完整 Unity 属性路径
Workbench 优先使用 PropertyPath 定位字段,路径为空或无法解析时使用 FieldLabel,该标签来自 [GCSLabel]、[InspectorName] 或 Unity 格式化后的字段名,可能与 C# 名称不同
四参数构造函数 (severity, message, fieldLabel, category) 仍可使用,通过 fieldLabel 定位字段,问题属于整个资产时,将 fieldLabel 和适用的 propertyPath 留空
校验只报告问题,不修改数据或阻止保存,某个校验函数抛出异常时,Workbench 会记录异常并继续执行其他校验
运行时约束需要由 Runtime 代码检查,派生类型覆盖 OnEnable 或 OnValidate 时,应调用基类实现,保留 GCS Identity 和基类数据处理
为 TGDropdown 建立选项
TGDropdown 是 GCS 内置选择控件共用的 Editor Popup,每个 Entry 都会提供选项标识、显示文本、页面路径与回调数据
using System.Collections.Generic;
using TinyGiants.Shared.Editor;
private static List<TGDropdownEntry> BuildAbilityEntries()
{
return new List<TGDropdownEntry>
{
new TGDropdownEntry
{
Key = "fireball",
Label = "Fireball",
GroupPath = new List<string> { "Combat", "Magic", "Fire" },
Payload = "fireball",
Tooltip = "Deals fire damage",
Enabled = true
},
new TGDropdownEntry
{
Key = "guard-break",
Label = "Guard Break",
GroupPath = new List<string> { "Combat", "Physical" },
Payload = "guard-break",
Tooltip = "Reduces armor"
}
};
}
| Entry 成员 | 含义 |
|---|---|
Key | 多选状态使用的唯一稳定标识 |
Label | 可见行文本 |
GroupPath | 具有任意层级的显式页面路径 |
Payload | 传给回调的对象,相等 Payload 在 Multiple 模式共享同一选择状态 |
Tooltip | 可选的行 Tooltip |
Enabled | 该项能否被选择 |
GroupPath 的每个元素对应一层页面,元素中的 / 按普通字符显示,Group 仍支持 Combat/Magic/Fire 这样的旧式路径,GroupPath 为 null 时使用 Group,空列表表示根页面
搜索同时匹配 Label 和完整路径,GroupOrder 指定每层优先显示的子页面,其余页面按名称排序
在 Multiple 模式下,为每行设置唯一且稳定的 Key,缺失或重复的 Key 会被替换为仅在本次 Popup 中有效的内部标识,选中或清除某项时,具有相同 Payload 的选项会同步选择状态,回调只执行一次
打开单选下拉框
Single 模式选中条目后,将 Payload 传给 OnPicked 并关闭 Popup,启用 ShowNoneOption 后,选择 None 会传入 null
using System;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine.UIElements;
private static void OpenSingle(
Button anchor,
Action<string> commit)
{
TGDropdown.Open(new TGDropdownConfig
{
Title = "Ability",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Single,
ShowNoneOption = true,
NoneLabel = "No Ability",
OnPicked = payload => commit(payload as string),
GroupOrder = new[] { "Combat", "Utility" },
MinWidth = 260f
});
}
在 OnPicked 中调用 context.Modify 保存 Inspector 字段修改
打开多选下拉框
Multiple 模式通过 IsSelected 读取初始状态,再通过 OnSelectionChanged 报告每次切换,示例中的 selected 列表会按选择顺序保存这些变化,并在 Popup 关闭时提交
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine.UIElements;
private static void OpenMultiple(
Button anchor,
IEnumerable<string> initial,
Action<List<string>> commit)
{
var selected = new List<string>();
if (initial != null)
{
foreach (string value in initial)
{
if (!string.IsNullOrEmpty(value) && !selected.Contains(value))
selected.Add(value);
}
}
TGDropdown.Open(new TGDropdownConfig
{
Title = "Granted Abilities",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Multiple,
ShowNoneOption = false,
IsSelected = payload =>
payload is string id && selected.Contains(id),
OnSelectionChanged = (payload, isSelected) =>
{
if (!(payload is string id)) return;
if (isSelected && !selected.Contains(id)) selected.Add(id);
if (!isSelected) selected.Remove(id);
},
OnClose = () => commit(new List<string>(selected)),
SelectedItemsFirst = true,
ClearSearchOnSelection = true,
MaxHeight = 360f
});
}
需要立即保存时,在 OnSelectionChanged 中调用 context.Modify,需要将整次操作合并为一个 Undo 步骤时,先保存到局部变量,再在 OnClose 中调用一次 context.Modify
打开计数下拉框
Count 模式为每个 Entry 显示非负计数,左键报告 +1,右键报告 -1
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine;
using UnityEngine.UIElements;
private static void OpenCounts(
Button anchor,
IDictionary<string, int> initial,
Action<Dictionary<string, int>> commit)
{
var counts = initial != null
? new Dictionary<string, int>(initial, StringComparer.Ordinal)
: new Dictionary<string, int>(StringComparer.Ordinal);
TGDropdown.Open(new TGDropdownConfig
{
Title = "Ability Copies",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Count,
ShowNoneOption = false,
GetCount = payload =>
{
string id = payload as string;
return id != null && counts.TryGetValue(id, out int count)
? count
: 0;
},
OnCountChanged = (payload, count) =>
{
if (!(payload is string id)) return 0;
if (count == 0) counts.Remove(id);
else counts[id] = count;
return count;
},
OnClose = () =>
commit(new Dictionary<string, int>(counts, StringComparer.Ordinal)),
CountHint = "Left click +1 · Right click −1",
MaxHeight = 520f
});
}
Count 模式修改数量后保持 Popup 打开,OnCountChanged 接收请求的非负数量,返回值决定显示的数量,可在回调中限制最大值并保存结果
统一配置 Popup 行为
通过 TGDropdownConfig 设置三种模式的标题、尺寸与关闭回调
| 设置 | 作用 |
|---|---|
Title、AnchorRect、Entries | 定义 Popup 标题、屏幕锚点与数据 |
SelectionMode | 选择 Single、Multiple 或 Count 交互 |
ShowNoneOption、NoneLabel | 配置 Single 模式的可选空值行 |
GroupOrder | 在每一层路径应用优先子页面顺序 |
Width、MinWidth、MaxHeight | 覆盖或限制 Popup 尺寸 |
SelectedItemsFirst | 在每一页把 Multiple 模式已选项放到未选项之前 |
ClearSearchOnSelection | Multiple 模式切换后清空搜索 |
CountHint | 替换 Count 模式说明文本 |
OnClose | 在所有 Popup 关闭路径中执行一次 |
Popup 不会自动保存项目数据,通过 context.Modify 写入序列化字段,并通过 RegisterRefresh 注册控件刷新逻辑
遵守扩展边界
- 每次 Domain Reload 后,从 Editor 程序集注册控件与校验
- 把序列化字段、Option Provider 与强类型运行时访问保留在 Runtime 程序集
- 优先使用声明式属性,再为确实需要的字段注册 Factory
- 所有持久化自定义控件修改都使用
context.Modify,Undo/Redo 刷新使用RegisterRefresh - Validator 保持无副作用,只报告问题,不修改目标
TGDropdown是 Editor 创作控件,不是运行时游戏 UI- 通过 Registry 注册的 Inspector 控件不会自动改变 Workbench List 行、Preview、卡面布局或 Gameplay