Inspector 扩展
通过派生内容类型和 Unity 序列化字段声明,为 Workbench 六个模式添加项目自有的持久化数据
Workbench 与普通 Unity Inspector 会从当前内容对象的序列化字段读取同一份 Schema,项目可以派生 GameCard、GameDeck、GamePlayerUnit、GameEnemyUnit、GameStatus 或 GameEncounter,加入 Unity 可序列化字段,再把这个具体类型创建到现有 GCS 数据库中,全程不需要修改 GCS Runtime 或 Editor 源码
扩展字段属于派生类型,已有的基础类型资产不会自动获得新字段,GCS 也不会把现有 GameCard 或其他基础资产自动转换成派生类型
Samples~/InspectorExtensions/ 提供可复制的 Runtime 与 Editor 程序集组合,其中包含六个模式的派生类、Typed Runtime Access、选项 Provider、校验、全宽自定义控件以及三种 TGDropdown 模式
把内容类型放进运行时程序集
派生内容类与选项 Provider 应放进可进入 Player 的程序集,并引用 TinyGiants.GCS.Runtime
{
"name": "YourGame.GCS.Content",
"references": [
"TinyGiants.GCS.Runtime"
]
}
派生类必须是顶层 Public、具体、非泛型类型,并继承对应的 GCS ScriptableObject 类型,每个内容类应单独放在同名 .cs 文件中,让 Unity 建立精确的 MonoScript 关联;缺少该关联的类型无法可靠保存为子资产,因此不会进入创建列表,Editor-only 程序集中的类型也不会进入创建列表,因为它们无法存在于 Player Build 中
| Workbench 模式 | 派生基类 | 保存位置 | 运行时枚举 |
|---|---|---|---|
| Card | GameCard | GameCardDatabase.Cards | GCSApi.Cards() |
| Deck | GameDeck | GameDeckDatabase.Decks | GCSApi.Decks() |
| Player | GamePlayerUnit | GamePlayerUnitDatabase.PlayerUnits | GCSApi.PlayerUnits() |
| Enemy | GameEnemyUnit | GameEnemyUnitDatabase.EnemyUnits | GCSApi.EnemyUnits() |
| Status | GameStatus | GameStatusDatabase.Statuses | GCSApi.Statuses() |
| Encounter | GameEncounter | GameEncounterDatabase.Encounters | GCSApi.Encounters() |
Unity 编译完成后,打开对应模式并点击 +,当可创建的具体类型超过一个时,Workbench 会打开类型选择器,其中包含内置基础类型和所有符合条件的项目类型,选择后会在当前数据库中创建该具体 ScriptableObject 子资产,在普通 Unity Inspector 中选中这个派生子资产时,也会显示同一套 Schema
using TinyGiants.GCS.Runtime;
public sealed class SampleCard : GameCard
{
public int RequiredLevel;
}
基础数据库列表可以保存该对象,因为 SampleCard 仍然属于 GameCard,其余五个模式遵循相同的继承规则
按 Unity 序列化规则声明字段
Workbench 枚举的就是 Unity 可见序列化属性,一个字段需要同时满足以下条件才会自动出现
-
字段是实例字段,不是属性、常量或静态字段
-
字段为 Public,或带有
[SerializeField] -
字段类型受 Unity 序列化支持
-
字段没有
[HideInInspector]
因此,Public 基础类型、枚举、Unity 对象引用、可序列化类和结构体、数组以及受支持的 List 都不需要额外注册,public int Level { get; set; } 这样的 C# 属性不是序列化字段,不会自动显示
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
using UnityEngine;
public sealed class SampleCard : GameCard
{
[GCSSection("Basic")]
[GCSLabel("Required Level")]
[Min(0)]
[Tooltip("Minimum character level required to add this card to a deck")]
public int RequiredLevel;
[Tooltip("Project-owned school used by project rules")]
public string School;
[GCSOptions("Fire", "Ice", "Lightning")]
[Tooltip("Element used by project combat rules")]
public string Element;
[GCSSection("Visual")]
[Tooltip("Voice line played by the project presentation layer")]
public AudioClip Voice;
[Header("Extension Data")]
[Range(0, 10)]
public int ComboLimit = 3;
[TextArea(3, 8)]
public string Lore;
}
这个示例把 Required Level、School 与 Element 加入 Basic,把 Voice 加入 Visual,再在内置 Behavior 之后为 Combo Limit 与 Lore 创建独立的 Extension Data Foldout
用声明顺序控制 Section
Workbench 不使用单独的 Order 数字,也不会按字母排序字段,Section 第一次出现的位置决定 Foldout 顺序,Section 内部字段按序列化声明顺序显示;后续复用已有 Section 时,字段会追加到该 Foldout,因此 Section 分组优先于一条全局的纵向字段顺序
| 声明 | 结果 |
|---|---|
[Header("Extension Data")] | 从当前字段开始选择或创建 Extension Data Foldout |
[GCSSection("Basic")] | 选择现有 Basic Foldout,不存在时创建它 |
| 后续字段没有这两个属性 | 在同一声明类型内继续沿用当前 Section |
| 派生声明类型的首个字段没有这两个属性 | 进入 Additional Foldout |
后续出现 [Header] 或 [GCSSection] | 从该字段开始切换当前 Section |
Section 名称严格区分大小写,使用完全一致的 Basic、Visual 或 Behavior 才会复用内置 Foldout,把派生字段路由到已有 Foldout 时会追加到该 Foldout 中,继承关系无法把派生声明插进 GCS 基类自身声明的两个字段之间
GCSSectionAttribute 还接受可选的图标字符串
[GCSSection("Progression", "★")]
public int RequiredLevel;
以下标准 Unity 属性在 Workbench 中有明确行为
| 属性 | Workbench 行为 |
|---|---|
[Header] | 创建或复用 Foldout,并把它设为当前 Section |
[Tooltip] | 把 Tooltip 应用到字段行与控件 |
[Space] | 在字段前增加纵向间距 |
[Min] | 把编辑后的整数或浮点数限制到声明的最小值 |
[Range] | 使用带输入框的整数或浮点 Slider |
[TextArea]、[Multiline] | 使用多行文本框 |
[Delayed] | 在字符串、整数或浮点编辑结束后提交输入 |
[InspectorName] | 替换自动美化后的字段标签或枚举选项标签 |
[SerializeField]、[HideInInspector] | 遵循 Unity 原生可见性规则 |
[GCSLabel] 的优先级高于 [InspectorName]。GCSInspectorControlRegistry 和 [GCSOptions] 这类负责选择控件的 GCS 注解,优先级高于同一字段上的 Unity CustomPropertyDrawer。两者都没有选择控件时,其他项目自有 PropertyAttribute 会回退到绑定的 Unity PropertyField,因此对应 CustomPropertyDrawer 仍然生效,其他可序列化类型也使用同一回退方式。字段需要特定 UI Toolkit 控件时,再通过Inspector 控件注册
提供单选选项
GCSOptionsAttribute 作用于序列化 string 字段,并接受以下三种来源之一
[GCSOptions("Fire", "Ice", "Lightning")]
public string Element;
[GCSOptions(
GCSInspectorOptionSource.CardRarity,
Editable = true,
EmptyValue = "Common")]
public string LootRarity;
[GCSOptions(typeof(SampleAbilityOptionProvider))]
public string AbilityId;
Editable = false 使用纯选择控件,Editable = true 保留同一套下拉列表,同时允许输入当前选项中不存在的项目自定义字符串;需要在清空字段时保存明确回退值,可设置 EmptyValue,内置 Rarity、Enemy Tier 与 Encounter Difficulty 分别使用 Common、Normal 与 Normal
| 内置来源 | GCS 提供的值 |
|---|---|
None | 不提供内置单选值 |
CardType | 内置和已创作的卡牌类型 |
CardRarity | 内置和已创作的卡牌稀有度 |
CardTag | 已创作的卡牌 Tag |
CardKeyword | 已注册 CardTag 的 ID 与显示标签 |
EnemyTier | 内置和已创作的敌人 Tier |
EncounterDifficulty | 内置和已创作的遭遇战 Difficulty |
动态 Provider 实现 Runtime-only 的 IGCSInspectorOptionProvider 契约,并提供 Public 无参构造函数,Workbench 需要重建选项时会创建 Provider,并把当前内容对象传给 GetOptions
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
using UnityEngine;
public sealed class SampleAbilityOptionProvider : IGCSInspectorOptionProvider
{
public IEnumerable<GCSInspectorOption> GetOptions(Object target)
{
yield return new GCSInspectorOption(
"fireball",
"Fireball",
new[] { "Combat", "Magic", "Fire" },
"Deals fire damage to one target");
yield return new GCSInspectorOption(
"guard-break",
"Guard Break",
new[] { "Combat", "Physical" },
"Reduces the target's armor");
}
}
Value 是序列化值,Label 是创作者看到的文本,字符串构造函数保存用 / 分隔的 GroupPath,上例使用的 List 构造函数则保存显式 GroupSegments,既支持任意页面深度,也允许单个 Segment 本身包含 /,Tooltip 用来说明选项,重复 Value 会按不区分大小写的方式去重,因此每个存储值都应保持稳定且唯一
提供多选值
GCSMultiSelectAttribute 作用于 List<string> 或 string[],构造方式与 GCSOptionsAttribute 相同,同样支持固定选项、Provider Type 与内置来源
[GCSMultiSelect(
typeof(SampleAbilityOptionProvider),
Style = GCSMultiSelectStyle.Chips)]
public List<string> GrantedAbilities = new List<string>();
[GCSMultiSelect(
GCSInspectorOptionSource.None,
Style = GCSMultiSelectStyle.CommaSeparated)]
public List<string> Factions = new List<string>();
| 样式 | 创作行为 |
|---|---|
CommaSeparated | 支持逗号分隔的自由输入和多选下拉框 |
Chips | 显示有序、可移除的胶囊项,并通过选项下拉框添加值 |
来源为 None 时,Workbench 还会从当前数据库资产中同类型内容的同名字段收集已有值,固定选项与 Provider 选项不受样式影响
选择其他 GCS 内容
GCSSubAssetAttribute 会把普通 ScriptableObject 引用替换为 GCS 内容选择器,选择器搜索已注册数据库,接受目标基类的派生实例,并可以显示为紧凑行或摘要卡片
[GCSSubAsset(
typeof(GameStatus),
Required = true,
Style = GCSSubAssetStyle.Compact)]
public GameStatus RequiredStatus;
[GCSSubAsset(
typeof(GameDeck),
Style = GCSSubAssetStyle.Card)]
public GameDeck RewardDeckOverride;
在 GCS ScriptableObject 引用的数组或 List 上使用 GCSSubAssetListAttribute
[GCSSubAssetList(typeof(GameEnemyUnit), Required = true)]
public List<GameEnemyUnit> Reinforcements = new List<GameEnemyUnit>();
Required 会在引用为空或必填 List 没有条目时显示行内警告,但不会自动增加 Issue Badge,需要把缺失引用纳入 Workbench 校验时,应另外注册项目 Validator
这些属性用于选择已有内容,不会在当前内容资产内部创建新的嵌套 ScriptableObject
使用 GCSInline 展开序列化设置
GCSInlineAttribute 会把可序列化类或结构体的直接可见子字段展开到当前 Section,不再增加一层嵌套属性框
using System;
using TinyGiants.GCS.Runtime;
using UnityEngine;
[Serializable]
public sealed class SampleRewardSettings
{
[Min(0)]
public int Gold;
[Range(0f, 1f)]
public float RareDropChance;
}
public sealed class SampleEncounter : GameEncounter
{
[Header("Extension Rewards")]
[GCSInline]
public SampleRewardSettings Rewards = new SampleRewardSettings();
}
引用为空、类型具体且具有无参构造函数时,Inspector 会初始化该对象,Inline 数据应保持可序列化,并只承载普通嵌套设置,不要把 GCSFlowGraphAttribute 放进 Inline 数据,FlowGraph Editor 需要内容对象上的直接字段路径
Inline 展示不会改变子字段的序列化路径,项目 Validator 定位顶层字段时应使用 nameof(SampleCard.RequiredLevel),定位嵌套字段时应使用 Rewards.Gold 或 Stats.RequiredLevel 这样的完整相对路径。Workbench 会先解析这个 PropertyPath,无法定位时再回退到显示用的 FieldLabel,完整校验契约见Inspector 控件
添加 FlowGraph 编辑字段
GCSFlowGraphAttribute 作用于直接的 GameEffectFlowGraph 字段,并添加 Edit FlowGraph 与静态摘要
[GCSFlowGraph(GCSFlowGraphSummary.Entries)]
public GameEffectFlowGraph AlternateBehavior = new GameEffectFlowGraph();
Entries 汇总入口节点,EnemyIntents 汇总 Pattern、Leaf Intent 与 Action 节点
这个属性只提供创作 UI,GCS 会自动执行并校验 Card、Status 与 Enemy 的内置 Behavior 字段,但不会自动执行或校验 AlternateBehavior 这样的项目新增字段,项目运行时代码必须决定何时运行该图,并通过项目 Validator 报告其创作错误,自定义 FlowGraph 字段应直接放在 Card、Status 或 Enemy 派生类型上,让编辑器可以应用对应的 Host 规则
复用其余内置表现控件
Runtime 元数据契约还提供以下专用控件
| 属性 | 所需字段 | 结果 |
|---|---|---|
[GCSAssetName] | string | 编辑字段值,并在值非空时重命名内容子资产 |
[GCSDescription] | string | 使用支持 Token 和语义引用的 GCS Description Editor |
[GCSLabel("Label")] | 任意序列化字段 | 覆盖显示标签 |
[GCSSegmentedBool("Off", "On")] | bool | 使用带自定义标签的双段 Boolean Picker |
[GCSDeckEntries] | GameDeck.Entries | 使用卡牌数量 Picker 与卡组摘要 UI |
[GCSSection("Name", "Icon")] | 任意序列化字段 | 从当前字段开始选择指定 Workbench Section |
属性应用到不兼容字段类型时,会回退到普通序列化属性控件,不会转换已保存的数据
在运行时读取同一份强类型数据
数据库与 GCSApi 返回基础类型,可以使用普通 C# 类型检查或 OfType<T>() 访问项目字段
using System.Linq;
using TinyGiants.GCS.Runtime;
SampleCard firstSampleCard = GCSApi.Cards()
.OfType<SampleCard>()
.FirstOrDefault();
if (firstSampleCard != null)
{
int requiredLevel = firstSampleCard.RequiredLevel;
}
if (cardInstance.GetActiveCard() is SampleCard activeSampleCard)
{
PlayVoice(activeSampleCard.Voice);
}
if (unitState.Source is SampleEnemyUnit sampleEnemy)
{
ApplyEnemyRules(sampleEnemy);
}
GCSApi.Decks()、PlayerUnits()、EnemyUnits()、Statuses() 与 Encounters() 使用相同方式,这些方法会枚举 Active 数据库并分配新 List,频繁读取时应缓存结果
派生字段属于持久化创作定义数据,单张卡牌实例的临时数值、单位当前状态和其他战斗状态应放在对应 Runtime State 或受支持的 Battle Variable 中,而不是写进 ScriptableObject 定义
在创作操作中保留数据
Workbench 使用 Unity 序列化保存派生字段,并在 Duplicate 或 Paste 时保留具体类型,复制和粘贴会拷贝全部序列化字段、分配唯一显示名称并生成新的 GCS Identity,只有复制对象的具体类型与当前模式兼容时才接受 Paste
Workbench Clipboard 保存当前 Unity Editor Domain 内的对象引用,它不是操作系统剪贴板,也不是跨 Session 或跨 Domain Reload 的传输格式
内置 Schema 控件进入 Unity Undo/Redo,创建、复制、删除与数据库成员变化同样会注册 Undo,项目自定义控件只有通过 GCSInspectorFieldContext.Modify 写入时才进入这条统一链路,具体方法见Inspector 控件
序列化名称属于已保存数据契约
-
重命名序列化字段时使用
[FormerlySerializedAs] -
保持派生类型名称与程序集归属稳定,否则提供明确的资产迁移
-
Duplicate 不会把基础资产转换成派生类型,它会保留源对象的具体类型
-
需要把已有基础资产转换为新派生类型时,应创建项目迁移工具
Inspector 扩展不会自动改变 Workbench List 行、Preview 摘要、Used By 行为、卡面 Prefab 或运行时表现,这些界面会继续读取原有 GCS 字段,直到项目代码或独立表现集成主动使用派生数据