跳到主要内容

Inspector 扩展

Guide

通过派生内容类型和 Unity 序列化字段声明,为 Workbench 六个模式添加项目自有的持久化数据

Workbench 与普通 Unity Inspector 都会根据当前内容对象的序列化字段生成编辑控件,项目只需从 GameCardGameDeckGamePlayerUnitGameEnemyUnitGameStatusGameEncounter 派生自己的内容类型,并声明需要保存的 Unity 序列化字段;编译完成后,这些派生类型便可以直接创建到现有 GCS 数据库中,不需要修改 GCS Runtime 或 Editor 源码

提示

扩展字段属于派生类型,已有的基础类型资产不会自动获得新字段,GCS 也不会把现有 GameCard 或其他基础资产自动转换成派生类型

下载 Inspector 扩展样例

下载 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 模式派生基类保存位置运行时枚举
CardGameCardGameCardDatabase.CardsGCSApi.Cards()
DeckGameDeckGameDeckDatabase.DecksGCSApi.Decks()
PlayerGamePlayerUnitGamePlayerUnitDatabase.PlayerUnitsGCSApi.PlayerUnits()
EnemyGameEnemyUnitGameEnemyUnitDatabase.EnemyUnitsGCSApi.EnemyUnits()
StatusGameStatusGameStatusDatabase.StatusesGCSApi.Statuses()
EncounterGameEncounterGameEncounterDatabase.EncountersGCSApi.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,再用 图标为 PowerLore 创建独立的 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 名称严格区分大小写,只有使用完全一致的 BasicVisualBehavior 才会复用对应的内置 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,项目自己的 PropertyAttributeCustomPropertyDrawer 仍然生效;如果这种回退仍无法满足交互需求,再按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 的回退值分别为 CommonNormalNormal;这个示例中的 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 字段;派生数据需要出现在这些位置时,项目应在对应的界面或表现代码中主动读取它