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 控件
使用 GCSList 编辑列表
同一内容需要保存多组设置时,在数组或 List<T> 字段上添加 [GCSList],Workbench 和 GCS 内容 Inspector 会显示条目数量、Add Item 和每项的操作按钮,普通序列化对象的可见子字段沿用 GCS 字段控件
public sealed class SampleCard : GameCard
{
[GCSList]
public List<SampleCardStats> BonusStats = new List<SampleCardStats>();
}
点击 Add Item 添加一项后,填写 Base Damage、Critical Chance 和 Armor Penetration,需要调整顺序时,拖动条目左侧的手柄或点击右侧上下箭头,点击减号可移除条目,这些改动都会保存在原列表字段中,并支持 Unity Undo/Redo
新增普通元素时,数值从零开始、字符串为空、对象引用为 None,有无参构造函数的普通类则通过构造函数取得字段初始值,[GCSInline] 对象或列表元素中的子列表字段也可以用 [GCSList] 编辑
| 元素类型 | 编辑方式 |
|---|---|
int、float、string、bool 和普通枚举 | 使用 GCS 字段控件,普通枚举显示为下拉框 |
| 可序列化类或结构体 | 展开可见子字段,保留字段支持的 GCS 注解、数值范围和 Tooltip |
| Unity 对象引用 | 使用对象选择字段,从 GCS 数据库选择内容时用 [GCSSubAssetList] |
[SerializeReference] 列表 | 每项提供类型选择框,可选择兼容的具体、可序列化、非泛型类或 None |
有自定义 PropertyDrawer 的元素 | 保留原 Drawer 的绘制逻辑,列表外围使用 GCS 样式 |
[SerializeReference] 列表新增项为 None,选定具体类型后才会创建实例,更换类型会替换该项数据,可以通过 Undo 恢复,字段没有专用 GCS 控件时会沿用 Unity 属性控件
[GCSInline] 展开单个嵌套对象,[GCSList] 负责列表布局与条目操作,已有的自定义 Drawer 继续使用自己的绘制逻辑
字段需要按项目规则切换可见性时,在项目 Drawer 或注册的 Inspector 控件中定义条件,这些注解由 GCS 内容 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,Paste 要求源类型属于当前模式,而且 Clipboard 保存的是当前 Unity Editor Domain 中的对象引用,重启编辑器或 Domain Reload 后需重新复制
内置字段编辑、创建、复制、删除与数据库成员修改均支持 Unity Undo,自定义控件通过 GCSInspectorFieldContext.Modify 保存修改后,也支持 Undo/Redo,具体方法见Inspector 控件
修改已保存的派生类型或字段时,保留数据关联
- 重命名序列化字段时使用
[FormerlySerializedAs] - 保持派生类型名称与程序集归属稳定,否则提供明确的资产迁移
- Duplicate 不会把基础资产转换成派生类型,它会保留源对象的具体类型
- 需要把已有基础资产转换为新派生类型时,应创建项目迁移工具
List、Preview、Used By、卡面 Prefab 与运行时表现仍读取原有 GCS 字段,需要显示派生数据时,在对应界面或表现代码中读取它们