跳到主要内容

Inspector 控件

Guide

隐藏字段、注册自定义 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,返回 null 或抛出异常时,Workbench 改用字段属性指定的控件或普通序列化控件,异常会写入 Console

IMGUI 控件需要放进 IMGUIContainer,并自行处理焦点和序列化变化

直接复用 GCS 字段控件​

控件工厂也可以直接创建以下 GCS 内置控件

控件用途
GCSSelectField纯选择字符串字段
GCSEditableSelectField可编辑字符串与选项下拉框
GCSTagField逗号分隔值与多选下拉框
GCSChipMultiSelectField有序、可移除胶囊项与多选下拉框
GCSEnumDropdown<T>使用 GCS 下拉表现的枚举 Picker
GCSBuffDebuffPicker带自定义标签的双段 Boolean 控件
GCSDescriptionField支持 Token 与语义引用的 GCS Description Editor
GCSSubAssetField从已注册 GCS 内容中选择紧凑行或卡片式引用
GCSSubAssetListFieldGCS 内容引用的序列化 List Editor
GCSFlowGraphFieldFlowGraph 按钮与入口或敌方意图静态摘要
GCSDeckEntriesFieldGameDeck 卡牌数量 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 字段行
FieldLabelPropertyPath 为空或无法解析时使用的显示标签回退值

字段校验使用五参数构造函数 (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 模式已选项放到未选项之前
ClearSearchOnSelectionMultiple 模式切换后清空搜索
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