Inspector 扩展
通过派生内容类型和 Unity 序列化字段声明,为 Workbench 六个模式添加项目自有的持久化数据
Workbench 与普通 Unity Inspector 都会根据当前内容对象的序列化字段生成编辑控件,项目只需从 GameCard、GameDeck、GamePlayerUnit、GameEnemyUnit、GameStatus 或 GameEncounter 派生自己的内容类型,并声明需要保存的 Unity 序列化字段;编译完成后,这些派生类型便可以直接创建到现有 GCS 数据库中,不需要修改 GCS Runtime 或 Editor 源码
扩展字段属于派生类型,已有的基础类型资产不会自动获得新字段,GCS 也不会把现有 GameCard 或其他基础资产自动转换成派生类型
下载 GCS-Samples.zip 并在 Unity 项目外解压,其中的 Samples~/InspectorExtensions/ 提供一组可复制的 Runtime 与 Editor 程序集;只把需要的目录复制到项目自有位置,并在修改样例时保持相同的程序集边界
把内容类型放进运行时程序集
派生内容类与选项 Provider 应放进可进入 Player 的程序集,并引用 TinyGiants.GCS.Runtime
{
"name": "YourGame.GCS.Content",
"references": [
"TinyGiants.GCS.Runtime"
]
}
能够进入创建列表的派生类必须是顶层 public、具体、非泛型类型,并继承对应的 GCS ScriptableObject;每个内容类还应单独放在同名 .cs 文件中,让 Unity 为它建立准确的 MonoScript 关联。没有对应 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() |
以下代码定义了一个只包含 RequiredLevel 字段的 SampleCard:
using TinyGiants.GCS.Runtime;
public sealed class SampleCard : GameCard
{
public int RequiredLevel;
}
Unity 编译完成后,打开 Card 模式并点击 +;可创建的具体类型超过一个时,Workbench 会先打开类型选择器,其中包含内置 GameCard 与所有符合条件的项目类型。基础类型固定排在首位,其余类型按显示名称排序,两个类型同名时,列表会附加程序集名称以便区分
选择 SampleCard 后,Workbench 会在当前数据库中创建一个 SampleCard 子资产,直接在普通 Unity Inspector 中选中该资产时,也会看到相同的序列化字段;当前 Card Database 可以保存该对象,因为 SampleCard 仍然属于 GameCard,其余五个模式遵循相同的继承规则
按 Unity 序列化规则声明字段
Workbench 按 Unity 的序列化规则枚举可见字段,一个字段需要同时满足以下条件才会自动出现
-
字段是实例字段,不是属性、常量或静态字段
-
字段为
public,或带有[SerializeField] -
字段类型受 Unity 序列化支持
-
字段没有
[HideInInspector]
满足这些条件后,基础类型、枚举、Unity 对象引用、可序列化类和结构体、数组以及受支持的 List<T> 都不需要额外注册,但 public int Level { get; set; } 这样的 C# 属性不属于序列化字段,因此不会自动显示
随包 SampleCard 使用 [GCSSection] 把字段加入现有 Section,也可以从指定字段开始创建新的 Section
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Runtime;
using UnityEngine;
public sealed class SampleCard : GameCard
{
[GCSSection("Basic")]
[Min(1)]
[Tooltip("Minimum character level required to play this card.")]
public int RequiredLevel = 1;
[GCSSection("Visual")]
[Tooltip("Optional voice line used by project presentation code.")]
public AudioClip VoiceLine;
[GCSSection("Extension Data", "◆")]
[Range(0, 100)]
public int Power = 10;
[TextArea(2, 5)]
public string Lore;
}
这个示例把 Required Level 加入 Basic,把 Voice Line 加入 Visual,再用 ◆ 图标为 Power 与 Lore 创建独立的 Extension Data Foldout
用声明顺序控制 Section
Workbench 不使用单独的 Order 数字,也不会按字母排序字段,Section 第一次出现的位置决定 Foldout 顺序,Section 内部按序列化声明顺序显示字段;后续字段再次使用已有 Section 时,会追加到对应 Foldout 中,因此最终顺序先按 Section 分组,再按各 Section 内的声明顺序排列
| 声明 | 结果 |
|---|---|
[Header("Extension Data")] | 从当前字段开始选择或创建 Extension Data Foldout |
[GCSSection("Basic")] | 选择现有 Basic Foldout,不存在时创建它 |
| 后续字段没有这两个属性 | 在同一声明类型内继续沿用当前 Section |
| 派生声明类型的首个字段没有这两个属性 | 进入 Additional Foldout |
后续出现 [Header] 或 [GCSSection] | 从该字段开始切换当前 Section |
Section 名称严格区分大小写,只有使用完全一致的 Basic、Visual 或 Behavior 才会复用对应的内置 Foldout;派生字段会追加到目标 Foldout 中,继承关系无法把它插入 GCS 基类自身声明的两个字段之间
GCSSectionAttribute 接受字符图标,也接受以 Assets/ 或 Packages/ 开头的项目相对图标路径;显式指定图标后,即使复用同名内置 Section,也会优先显示该图标
[GCSSection("Progression", "★")]
public int RequiredLevel;
[GCSSection(
"Presentation",
"Assets/YourGame/GCS/Editor/Icons/presentation.png")]
public Color PresentationTint;
资产路径可以指向已导入的 Sprite 或 Texture2D;路径无效时,Workbench 会回退到该 Section 的内置图标,并针对该路径向 Console 写入一次警告
以下标准 Unity 属性在 Workbench 中有明确行为
| 属性 | Workbench 行为 |
|---|---|
[Header] | 创建或复用 Foldout,并把它设为当前 Section |
[Tooltip] | 把 Tooltip 应用到字段行与控件 |
[Space] | 在字段前增加纵向间距 |
[Min] | 把编辑后的整数或浮点数限制到声明的最小值 |
[Range] | 使用带输入框的整数或浮点 Slider |
[TextArea]、[Multiline] | 使用多行文本框 |
[Delayed] | 在字符串、整数或浮点编辑结束后提交输入 |
[InspectorName] | 替换自动美化后的字段标签或枚举选项标签 |
[SerializeField]、[HideInInspector] | 遵循 Unity 原生可见性规则 |
字段同时使用 [GCSLabel] 与 [InspectorName] 时,Workbench 优先采用 [GCSLabel] 指定的标签;GCSInspectorControlRegistry 或 [GCSOptions] 已经为字段选择控件时,Workbench 会优先使用该控件,而不是同一字段上的 Unity CustomPropertyDrawer
如果没有 GCS 控件接管字段,Workbench 会使用绑定的 Unity PropertyField,项目自己的 PropertyAttribute 与 CustomPropertyDrawer 仍然生效;如果这种回退仍无法满足交互需求,再按Inspector 控件注册专用 UI Toolkit 控件
提供单选选项
GCSOptionsAttribute 作用于序列化 string 字段,并接受固定选项、GCS 内置来源或项目 Provider 三种来源
[GCSOptions(
"Physical", "Fire", "Frost", "Lightning", "Arcane",
Editable = true)]
public string DamageSchool = "Physical";
[GCSOptions(typeof(SampleFactionOptionProvider))]
public string Faction = "alliance.kingdom";
Editable = false 使用纯选择控件,Editable = true 则在保留下拉列表的同时,允许输入列表之外的项目自定义字符串;Workbench 会收集当前数据库中同名字段已经保存的非空值,并把它们加入后续下拉列表,因此新输入的值也可以被其他同类内容继续选择
字段清空后需要保存指定值时,可以设置 EmptyValue,内置 Rarity、Enemy Tier 与 Encounter Difficulty 的回退值分别为 Common、Normal 与 Normal;这个示例中的 DamageSchool 可以选择预设值,也可以输入新值,Faction 则保存 SampleFactionOptionProvider 返回的 Value,同时在下拉框中显示对应 Label
| 内置来源 | 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 SampleFactionOptionProvider : IGCSInspectorOptionProvider
{
public IEnumerable<GCSInspectorOption> GetOptions(Object target)
{
yield return new GCSInspectorOption(
"alliance.kingdom",
"Kingdom Alliance",
new[] { "World", "Alliance", "Kingdom" },
"Human kingdoms joined under one banner.");
yield return new GCSInspectorOption(
"horde.ashen",
"Ashen Horde",
new[] { "World", "Horde", "Ashen Marches" },
"Raiders from the volcanic frontier.");
}
}
Provider 返回的每个选项由四项数据组成
| Provider 成员 | 用途 |
|---|---|
Value | 写入序列化字段的值 |
Label | 创作者在下拉框中看到的文本 |
GroupPath | 显式页面路径,支持任意深度,单个路径元素可以包含 / |
Tooltip | 选项的悬停说明 |
Workbench 会按不区分大小写的方式去除重复 Value,因此每个存储值都应保持稳定且唯一
提供多选值
GCSMultiSelectAttribute 作用于 List<string> 或 string[],构造方式与 GCSOptionsAttribute 相同,同样支持固定选项、Provider Type 与内置来源
[GCSMultiSelect(
"Humanoid", "Construct", "Undead", "Beast", "Quest",
Mode = GCSMultiSelectMode.Freeform)]
public List<string> Traits = new List<string>();
[GCSMultiSelect(
typeof(SampleSpellOptionProvider),
Mode = GCSMultiSelectMode.OptionsOnly)]
public List<string> GrantedSpells = new List<string>();
| 模式 | 创作行为 |
|---|---|
Freeform | 接受逗号分隔的项目自定义值,同时在多选下拉框中提供已有选项 |
OptionsOnly | 显示有序、可移除的胶囊项,只能通过所提供的选项下拉框添加值 |
Traits 使用 Freeform 模式,可以输入新值,也可以从下拉菜单中选择已有值,Workbench 会从当前数据库中其他 SampleCard.Traits 收集已经填写的值,让它们进入后续选择列表;GrantedSpells 使用 OptionsOnly 模式,只能选择 SampleSpellOptionProvider 返回的内容,并把已选值显示为有序、可移除的胶囊标签
选择其他 GCS 内容
GCSSubAssetAttribute 会把普通 ScriptableObject 引用替换为 GCS 内容选择器,选择器从已注册数据库中查找目标类型,也接受该类型的派生实例,并根据 Style 设置将字段显示为紧凑行或摘要卡片
[GCSSubAsset(
typeof(SampleStatus),
Style = GCSSubAssetStyle.Card)]
public SampleStatus SignatureStatus;
SampleCard.SignatureStatus 只能选择 SampleStatus;需要保存多个 GCS 内容引用时,在对应的数组或 List<T> 字段上使用 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 SampleCardStats
{
[Min(0)]
public int BaseDamage = 6;
[Range(0f, 1f)]
public float CriticalChance = 0.1f;
}
public sealed class SampleCard : GameCard
{
[GCSInline]
public SampleCardStats Stats = new SampleCardStats();
}
字段引用为空,并且字段类型是带无参构造函数的具体类型时,Inspector 会自动创建该对象;Inline 数据仍需满足 Unity 序列化规则,并且只用于保存普通的嵌套设置,不要把 GCSFlowGraphAttribute 放进 Inline 数据,因为 FlowGraph Editor 需要内容对象上的直接字段路径
Inline 展示不会改变子字段的序列化路径,项目 Validator 定位顶层字段时使用 nameof(SampleCard.RequiredLevel),定位嵌套字段时使用 Stats.BaseDamage 这样的完整相对路径;Workbench 会优先解析 PropertyPath,无法定位时再使用 FieldLabel 匹配显示标签,完整校验契约见Inspector 控件
添加 FlowGraph 编辑字段
GCSFlowGraphAttribute 作用于直接的 GameEffectFlowGraph 字段,并添加 Edit FlowGraph 与静态摘要
public sealed class ProjectCard : GameCard
{
[GCSFlowGraph(GCSFlowGraphSummary.Entries)]
public GameEffectFlowGraph AlternateBehavior = new GameEffectFlowGraph();
}
Entries 汇总入口节点,EnemyIntents 汇总 Pattern、Leaf Intent 与 Action 节点
GCSFlowGraphAttribute 只提供创作 UI,GCS 自动执行和校验的是 Card、Status 与 Enemy 自带的 Behavior 字段;AlternateBehavior 这样的项目字段不会自动执行,也不会进入内置校验,项目运行时代码需要决定何时执行该图,项目 Validator 则负责报告配置错误;自定义 FlowGraph 字段应直接声明在 Card、Status 或 Enemy 派生类型上,以便编辑器应用对应的 Host 规则
复用其他内置字段控件
以下 Runtime 属性对应 GCS 内置字段控件:
| 属性 | 所需字段 | 结果 |
|---|---|---|
[GCSAssetName] | string | 编辑字段值,并在值非空时重命名内容子资产 |
[GCSDescription] | string | 使用支持 Token 和语义引用的 GCS Description Editor |
[GCSLabel("Label")] | 任意序列化字段 | 覆盖显示标签 |
[GCSSegmentedBool("Off", "On")] | bool | 使用带自定义标签的双段 Boolean Picker |
[GCSDeckEntries] | GameDeck.Entries | 使用卡牌数量 Picker 与卡组摘要 UI |
[GCSSection("Name", "字符或资产路径")] | 任意序列化字段 | 从当前字段开始选择指定 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.VoiceLine);
}
if (unitState.Source is SampleEnemy sampleEnemy)
{
ApplyEnemyRules(sampleEnemy);
}
GCSApi.Decks()、PlayerUnits()、EnemyUnits()、Statuses() 与 Encounters() 使用相同方式,这些方法每次都会枚举当前启用的数据库并创建新列表,频繁读取时应缓存结果
派生字段用于保存持久化的内容数据,卡牌实例的临时数值、单位当前状态和其他战斗状态应保存在对应的 Runtime State 或受支持的 Battle Variable 中,不要写回 ScriptableObject 定义
在创作操作中保留数据
Workbench 使用 Unity 序列化保存派生字段,Duplicate 与 Paste 会保留源对象的具体类型,并复制全部序列化字段;新对象会获得唯一名称和新的 GCS Identity,源对象类型与当前模式不兼容时,Paste 会被拒绝
Workbench Clipboard 保存的是当前 Unity Editor Domain 内的对象引用,不是操作系统剪贴板,也不能跨 Session 或 Domain Reload 使用
内置字段控件,以及创建、复制、删除和数据库成员变化都会注册 Unity Undo,项目自定义控件只有通过 GCSInspectorFieldContext.Modify 写入时才会进入同一条 Undo/Redo 链路,具体方法见Inspector 控件
序列化名称属于已保存数据契约:
- 重命名序列化字段时使用
[FormerlySerializedAs] - 保持派生类型名称与程序集归属稳定,否则提供明确的资产迁移
- Duplicate 不会把基础资产转换成派生类型,它会保留源对象的具体类型
- 需要把已有基础资产转换为新派生类型时,应创建项目迁移工具
Inspector 扩展不会自动改变 Workbench List 行、Preview 摘要、Used By、卡面 Prefab 或运行时表现,这些界面仍然读取各自原有的 GCS 字段;派生数据需要出现在这些位置时,项目应在对应的界面或表现代码中主动读取它