Inspector controls
Hide fields, register UI Toolkit controls and validation rules, and reuse GCS fields and dropdowns
Workbench selects controls from serialized fields and Runtime attributes, Editor registries can hide fields, replace individual controls, or add validation rules while Workbench continues to draw the remaining fields
Custom control and validation code belongs in an Editor assembly. The derived content class and its serialized data remain in the runtime assembly described by Inspector extensions.
Samples~/InspectorExtensions/ in GCS-Samples.zip contains Runtime and Editor examples, extract the archive outside Unity and copy the required directories into project-owned folders
Reference the Editor assembliesโ
Create an Editor-only Assembly Definition that references the project content assembly, GCS Editor, and Shared Editor:
{
"name": "YourGame.GCS.Content.Editor",
"references": [
"YourGame.GCS.Content",
"TinyGiants.GCS.Runtime",
"TinyGiants.GCS.Editor",
"TinyGiants.Shared.Editor"
],
"includePlatforms": [
"Editor"
]
}
This Editor assembly is excluded from player builds, keep GCSInspectorVisibilityRegistry, GCSInspectorControlRegistry, GCSInspectorValidationRegistry, TGDropdown, and UI Toolkit Editor controls in Editor code
Hide inherited fieldsโ
Use GCSInspectorVisibilityRegistry to hide fields for a content type while preserving their declarations and saved data, register field names from an Editor assembly after each Domain Reload
[InitializeOnLoad]
public static class SampleInspectorRegistration
{
static SampleInspectorRegistration()
{
GCSInspectorVisibilityRegistry.Hide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
}
}
Workbench hides the three fields on SamplePlayer and omits empty foldouts when every field is hidden, the fields remain serialized and readable, GamePlayerUnit and other player types are unchanged
Base-type registrations also apply to derived types, remove them when disabling or replacing the Editor integration
GCSInspectorVisibilityRegistry.Unhide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
Register one field controlโ
GCSInspectorControlRegistry matches a content type and serialized field name. Register during every Domain Reload with [InitializeOnLoad] or [InitializeOnLoadMethod].
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using UnityEditor;
using UnityEngine.UIElements;
[InitializeOnLoad]
public static class SampleInspectorRegistration
{
static SampleInspectorRegistration()
{
GCSInspectorVisibilityRegistry.Hide<SamplePlayer>(
nameof(SamplePlayer.MaxHp),
nameof(SamplePlayer.BaseEnergy),
nameof(SamplePlayer.EnergyDisplay));
GCSInspectorControlRegistry.Register<SampleCard>(
nameof(SampleCard.SpellLoadout),
CreateSpellLoadoutControl);
GCSInspectorValidationRegistry.Register<SampleCard>(
"sample.card.progression",
ValidateProgression);
}
private static VisualElement CreateSpellLoadoutControl(
GCSInspectorFieldContext context)
{
context.UseFullWidth = true;
return new SampleSpellLoadoutField(context);
}
private static IEnumerable<GCSInspectorValidationResult> ValidateProgression(
SampleCard card)
{
if (card.RequiredLevel < 1)
{
yield return new GCSInspectorValidationResult(
GCSInspectorValidationSeverity.Error,
"Required Level must be at least 1.",
fieldLabel: "Required Level",
category: "Sample Progression",
propertyPath: nameof(SampleCard.RequiredLevel));
}
}
}
SampleSpellLoadoutField draws SampleCard.SpellLoadout, and the validator reports an error when RequiredLevel is below 1, registrations apply to the selected types and their derived types
Registering the same type and field again replaces the previous factory, when both base and derived types register that field, the closest matching type takes precedence
Use the serialized field contextโ
Read and edit fields through GCSInspectorFieldContext to use Workbench serialization, Undo, and refresh handling in custom controls
| Member | Use |
|---|---|
Target | Current concrete ScriptableObject |
SerializedObject | Serialized wrapper for the current target |
PropertyPath | Exact path of the field being replaced |
Field | Reflected field metadata |
Label | Final displayed label after GCSLabel, InspectorName, or nicification |
GetProperty() | Updates the serialized object and resolves the current property |
Modify(undoLabel, mutation) | Applies a serialized change, records Undo, marks the target dirty, and refreshes Workbench |
RegisterRefresh(refresh) | Refreshes the control after Undo/Redo or an external serialized change |
UseFullWidth | Makes the returned element occupy the complete section width |
Array edits, Undo, or Inspector rebuilds can invalidate a SerializedProperty, use GetProperty() to resolve the current property whenever reading it
Use SetValueWithoutNotify when updating controls through RegisterRefresh to avoid another edit callback, set UseFullWidth for a compound control that needs the full row
private static VisualElement BuildTimelineControl(
GCSInspectorFieldContext context)
{
context.UseFullWidth = true;
var timeline = new VisualElement();
timeline.AddToClassList("sample-timeline");
return timeline;
}
Factories return a UI Toolkit VisualElement, a null result or exception falls back to the field attribute control or a normal serialized control, exceptions are logged to the Console
Wrap IMGUI controls in IMGUIContainer and handle their focus and serialized changes
Reuse GCS field controls directlyโ
Control factories can also create these built-in GCS controls
| Control | Purpose |
|---|---|
GCSSelectField | Selection-only string field |
GCSEditableSelectField | Editable string plus option dropdown |
GCSTagField | Comma-separated values plus multiple-selection dropdown |
GCSChipMultiSelectField | Ordered removable chips plus multiple-selection dropdown |
GCSEnumDropdown<T> | Enum picker using GCS dropdown presentation |
GCSBuffDebuffPicker | Two-segment Boolean control with custom labels |
GCSDescriptionField | GCS description editor with token and semantic-reference support |
GCSSubAssetField | Compact or card-style selector for registered GCS content |
GCSSubAssetListField | Serialized list editor for GCS content references |
GCSFlowGraphField | FlowGraph button and static entry or enemy-intent summary |
GCSDeckEntriesField | GameDeck card count picker and summary |
Use Runtime attributes when they select the required control, register a factory for compound controls, conditional interaction, or project commands
Add project validationโ
Register validators through GCSInspectorValidationRegistry to run them alongside built-in checks, each registration uses a content type and ID, repeated registration replaces that entry, base-type rules also check derived objects
GCSInspectorValidationRegistry.Register<SampleEncounter>(
"sample.encounter.rewards",
encounter => ValidateRewards(encounter));
A validator returns zero or more GCSInspectorValidationResult values:
| Value | Result in Workbench |
|---|---|
Warning | Adds a warning to the current database Issue Badge |
Error | Adds an error to the current database Issue Badge |
Category | Displays the project-defined issue category |
Message | Appears after the content object's display name |
PropertyPath | Exact SerializedProperty.propertyPath used first to jump to and flash one Inspector row |
FieldLabel | Displayed-label fallback when PropertyPath is empty or cannot be resolved |
Use (severity, message, fieldLabel, category, propertyPath) to report an issue on one field, pass nameof(SampleCard.RequiredLevel) for a top-level field, Stats.BaseDamage for a nested field, or a full Unity path containing Array.data[0] for arrays and List<T>
Workbench resolves PropertyPath first and uses FieldLabel when the path is empty or unresolved, the label comes from [GCSLabel], [InspectorName], or Unity's formatted field name and may differ from the C# name
The four-parameter constructor (severity, message, fieldLabel, category) locates fields through fieldLabel, for an asset-wide issue leave fieldLabel and any applicable propertyPath empty
Validation reports issues without changing data or preventing saves, Workbench logs validator exceptions and continues with the remaining checks
Enforce runtime constraints in Runtime code, when overriding OnEnable or OnValidate, call the base implementation to retain GCS identity and base data handling
Build entries for TGDropdownโ
TGDropdown is the Editor popup used by GCS selection controls, entries provide an identifier, label, page path, and callback data
using System.Collections.Generic;
using TinyGiants.Shared.Editor;
private static List<TGDropdownEntry> BuildAbilityEntries()
{
return new List<TGDropdownEntry>
{
new TGDropdownEntry
{
Key = "fireball",
Label = "Fireball",
GroupPath = new List<string> { "Combat", "Magic", "Fire" },
Payload = "fireball",
Tooltip = "Deals fire damage",
Enabled = true
},
new TGDropdownEntry
{
Key = "guard-break",
Label = "Guard Break",
GroupPath = new List<string> { "Combat", "Physical" },
Payload = "guard-break",
Tooltip = "Reduces armor"
}
};
}
| Entry member | Meaning |
|---|---|
Key | Unique stable row identity for multiple-selection state |
Label | Visible row text |
GroupPath | Explicit page path with any number of segments |
Payload | Object returned to callbacks; equal payloads share one Multiple-mode selection state |
Tooltip | Optional row tooltip |
Enabled | Whether the item can be selected |
Each GroupPath element represents one page level, a / within an element is a normal character, Group still supports legacy paths such as Combat/Magic/Fire, a null GroupPath uses Group, and an empty list selects the root page
Search matches Label and the full path, GroupOrder prioritizes child pages at each level while other pages are sorted by name
Assign a unique, stable Key to each Multiple-mode row, missing or duplicate keys are replaced by internal identifiers valid only for that popup
Rows with equal Payload values share selection state, selecting or clearing one updates the others and invokes the callback once
Open a single-selection dropdownโ
Single mode passes the selected entry's Payload to OnPicked and closes the popup, with ShowNoneOption enabled, choosing None passes null
using System;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine.UIElements;
private static void OpenSingle(
Button anchor,
Action<string> commit)
{
TGDropdown.Open(new TGDropdownConfig
{
Title = "Ability",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Single,
ShowNoneOption = true,
NoneLabel = "No Ability",
OnPicked = payload => commit(payload as string),
GroupOrder = new[] { "Combat", "Utility" },
MinWidth = 260f
});
}
Call context.Modify from OnPicked to save Inspector field changes
Open a multiple-selection dropdownโ
Multiple mode reads initial state through IsSelected and reports each toggle through OnSelectionChanged, the example keeps changes in selection order and commits them when the popup closes
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine.UIElements;
private static void OpenMultiple(
Button anchor,
IEnumerable<string> initial,
Action<List<string>> commit)
{
var selected = new List<string>();
if (initial != null)
{
foreach (string value in initial)
{
if (!string.IsNullOrEmpty(value) && !selected.Contains(value))
selected.Add(value);
}
}
TGDropdown.Open(new TGDropdownConfig
{
Title = "Granted Abilities",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Multiple,
ShowNoneOption = false,
IsSelected = payload =>
payload is string id && selected.Contains(id),
OnSelectionChanged = (payload, isSelected) =>
{
if (!(payload is string id)) return;
if (isSelected && !selected.Contains(id)) selected.Add(id);
if (!isSelected) selected.Remove(id);
},
OnClose = () => commit(new List<string>(selected)),
SelectedItemsFirst = true,
ClearSearchOnSelection = true,
MaxHeight = 360f
});
}
For immediate saves, call context.Modify from OnSelectionChanged, for one Undo step, keep local changes and call context.Modify once from OnClose
Open a count dropdownโ
Count mode displays a non-negative count per entry, left-click requests +1 and right-click requests -1
using System;
using System.Collections.Generic;
using TinyGiants.GCS.Editor;
using TinyGiants.Shared.Editor;
using UnityEngine;
using UnityEngine.UIElements;
private static void OpenCounts(
Button anchor,
IDictionary<string, int> initial,
Action<Dictionary<string, int>> commit)
{
var counts = initial != null
? new Dictionary<string, int>(initial, StringComparer.Ordinal)
: new Dictionary<string, int>(StringComparer.Ordinal);
TGDropdown.Open(new TGDropdownConfig
{
Title = "Ability Copies",
AnchorRect = GCSVisualKit.ScreenRectOf(anchor),
Entries = BuildAbilityEntries(),
SelectionMode = TGDropdownSelectionMode.Count,
ShowNoneOption = false,
GetCount = payload =>
{
string id = payload as string;
return id != null && counts.TryGetValue(id, out int count)
? count
: 0;
},
OnCountChanged = (payload, count) =>
{
if (!(payload is string id)) return 0;
if (count == 0) counts.Remove(id);
else counts[id] = count;
return count;
},
OnClose = () =>
commit(new Dictionary<string, int>(counts, StringComparer.Ordinal)),
CountHint = "Left click +1 ยท Right click โ1",
MaxHeight = 520f
});
}
Count mode keeps the popup open after changes, OnCountChanged receives the requested non-negative count and returns the displayed count, clamp the maximum and save the result in this callback
Configure popup behavior consistentlyโ
Use TGDropdownConfig to set titles, sizes, and close callbacks for all three modes
| Setting | Effect |
|---|---|
Title, AnchorRect, Entries | Defines the popup heading, screen anchor, and data |
SelectionMode | Chooses Single, Multiple, or Count interaction |
ShowNoneOption, NoneLabel | Configures the optional Single-mode null row |
GroupOrder | Applies preferred child-page order at every path level |
Width, MinWidth, MaxHeight | Overrides or constrains popup dimensions |
SelectedItemsFirst | Moves selected Multiple-mode entries before unselected entries on each page |
ClearSearchOnSelection | Clears search after a Multiple-mode toggle |
CountHint | Replaces the Count-mode instruction text |
OnClose | Runs once for every popup close path |
The popup does not save project data automatically, write serialized fields through context.Modify and register control refreshes through RegisterRefresh
Respect the extension boundaryโ
-
Register controls and validation from an Editor assembly after every Domain Reload.
-
Keep serializable fields, option providers, and typed runtime access in a runtime assembly.
-
Use declarative attributes before replacing a field with a registry factory.
-
Use
context.Modifyfor every persisted custom-control change andRegisterRefreshfor Undo/Redo. -
Keep validation side-effect free; it reports an issue but does not mutate the target.
-
Treat
TGDropdownas an Editor authoring control, not a runtime game UI. -
Do not expect an Inspector control registered through the project API to change Workbench List rows, Preview, card-face layout, or gameplay automatically.