Inspector 扩展
通过派生内容类型和 Unity 序列化字段声明,为 Workbench 六个模式添加项目自有的持久化数据
从 GameCard、GameDeck、GamePlayerUnit、GameEnemyUnit、GameStatus 或 GameEncounter 派生项目内容类型,添加 Unity 序列化字段,编译后即可在 GCS 数据库中创建派生资产,Workbench 和普通 Unity Inspector 都会显示这些字段,无需修改 GCS 源码
扩展字段属于派生类型,已有的基础类型资产不会自动获得新字段,GCS 也不会把现有 GameCard 或其他基础资产自动转换成派生类型
下载 GCS-Samples.zip 并在 Unity 项目外解压,其中的 Samples~/InspectorExtensions/ 提供一组可复制的 Runtime 与 Editor 程序集,只把需要的目录复制到项目自有位置,并在修改样例时保持相同的程序集边界
把内容类型放进运行时程序集
派生内容类与选项 Provider 应放进可进入 Player 的程序集,并引用 TinyGiants.GCS.Runtime
{
"name": "YourGame.GCS.Content",
"references": [
"TinyGiants.GCS.Runtime"
]
}
派生类必须是顶层 public 类型,不能是抽象类或泛型类,每个内容类放在同名 .cs 文件中,供 Unity 关联 MonoScript,缺少 MonoScript 或位于 Editor-only 程序集中的类型不会出现在创建列表中
| 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 模式并点击 +,存在多个可创建类型时,先选择内容类型,内置 GameCard 排在首位,项目类型按显示名称排序,同名类型会附带程序集名称
选择 SampleCard 后,当前 Card Database 中会创建对应的子资产,Workbench 和普通 Unity Inspector 都会显示其字段,其余五个模式按各自的 GCS 基类创建派生内容
按 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 折叠区
用声明顺序控制 Section
Section 按首次出现的顺序排列,分区内的字段按声明顺序排列,后续字段使用同名 Section 时追加到该分区末尾,不需要设置 Order,也不会按字段名排序
| 声明 | 结果 |
|---|---|
[Header("Extension Data")] | 从当前字段开始选择或创建 Extension Data Foldout |
[GCSSection("Basic")] | 选择现有 Basic Foldout,不存在时创建它 |
| 后续字段没有这两个属性 | 在同一声明类型内继续沿用当前 Section |
| 派生声明类型的首个字段没有这两个属性 | 进入 Additional Foldout |
后续出现 [Header] 或 [GCSSection] | 从该字段开始切换当前 Section |
Section 名称区分大小写,使用 Basic、Visual 或 Behavior 可复用对应的内置折叠区,派生字段追加在已有字段之后,不能插入基类字段之间
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] 时,优先使用 [GCSLabel] 的标签,注册控件或 [GCSOptions] 等 GCS 属性指定的控件优先于 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 接口 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 序列化规则
FlowGraph Editor 使用内容对象上的直接字段路径,GCSFlowGraphAttribute 需放在内容类中,不能放在 Inline 数据内
Inline 不改变子字段的序列化路径,校验顶层字段时使用 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 节点
将图字段直接声明在 Card、Status 或 Enemy 派生类型上,FlowGraph Editor 会按所属内容类型检查节点
战斗运行时只自动执行内置 Behavior,Workbench 内置校验也只检查它,AlternateBehavior 等额外图需要由项目代码执行,并注册项目校验函数
复用其他内置字段控件
以下 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,源类型不属于当前模式时无法粘贴
Workbench Clipboard 保存当前 Unity Editor Domain 中的对象引用,重启编辑器或 Domain Reload 后需重新复制
内置字段编辑、创建、复制、删除与数据库成员修改均支持 Unity Undo,自定义控件通过 GCSInspectorFieldContext.Modify 保存修改后,也支持 Undo/Redo,具体方法见Inspector 控件
修改已保存的派生类型或字段时,保留数据关联
- 重命名序列化字段时使用
[FormerlySerializedAs] - 保持派生类型名称与程序集归属稳定,否则提供明确的资产迁移
- Duplicate 不会把基础资产转换成派生类型,它会保留源对象的具体类型
- 需要把已有基础资产转换为新派生类型时,应创建项目迁移工具
List、Preview、Used By、卡面 Prefab 与运行时表现仍读取原有 GCS 字段,需要显示派生数据时,在对应界面或表现代码中读取它们