Store project data in definitions
Use case
Use a definition extension for project data that describes what a board, cell, or structure is and does not change during play. Examples include terrain cost, biome identity, or a project ruleset ID.
Use a runtime state extension instead when the value changes through actions. Use a policy when the project needs to accept or reject an operation rather than store data.
Contract and ownership
Choose the narrowest scope:
| Data belongs to | Contract |
|---|---|
| The whole board definition | IGridBoardDefinitionExtension |
| One cell definition | IGridCellDefinitionExtension |
| One structure definition | IGridStructureDefinitionExtension |
The ownership lifecycle has three boundaries:
- Grid Toolkit clones the supplied value into definition ownership.
- Consumers receive an independent IGridExtensionReadModel, not the owned object.
- Implement IGridDefinitionFreezableExtension when mutable setup properties must reject changes after the definition is built.
Implementation
The example stores a positive movement cost on a cell. It supports runtime ownership cloning, authoring cloning, validation, freezing, and detached reads.
Excerpt — define and validate the immutable value. Tested using public APIs.
public sealed partial class TerrainCostDefinitionExtension :
GridRuntimeDefinitionExtensionBase<TerrainCostReadModel>,
IGridCellDefinitionExtension,
IGridDefinitionFreezableExtension,
IGridAuthoringCloneableExtension,
IGridAuthoringValidatableExtension
{
public const string Id = "game.terrain-cost";
[SerializeField] private int cost = 1;
private bool frozen;
public TerrainCostDefinitionExtension() { }
public TerrainCostDefinitionExtension(int cost)
{
Cost = cost;
}
public override string ExtensionId => Id;
public int Cost
{
get => cost;
set
{
if (frozen)
throw new InvalidOperationException("Definition data is immutable after build.");
if (value < 1)
throw new ArgumentOutOfRangeException(nameof(value));
cost = value;
}
}
}
Excerpt — clone ownership, validate authoring, and freeze the built value. Tested using public APIs.
public sealed partial class TerrainCostDefinitionExtension
{
protected override GridRuntimeExtensionBase<TerrainCostReadModel> CreateOwnedClone(
GridExtensionRuntimeCloneContext context) =>
new TerrainCostDefinitionExtension(Cost);
protected override TerrainCostReadModel CreateDetachedReadModel() =>
new TerrainCostReadModel(Cost);
public IGridExtension CloneForAuthoring() =>
new TerrainCostDefinitionExtension(Cost);
public IEnumerable<string> ValidateForAuthoring()
{
if (Cost < 1)
yield return "Terrain cost must be at least one.";
}
public void FreezeForDefinition() => frozen = true;
}
Excerpt — expose a detached read-only value. Tested using public APIs.
public sealed class TerrainCostReadModel : IGridExtensionReadModel
{
public TerrainCostReadModel(int cost)
{
Cost = cost;
}
public string ExtensionId => TerrainCostDefinitionExtension.Id;
public int Cost { get; }
}
Register and read in code
Call SetExtension(...) on the
GridCellDefinitionBuilder before the
board definition is built. After Build(), obtain the cell definition and request its detached
read model:
Excerpt — register and read a definition extension in code. Tested using public APIs.
public static TerrainCostReadModel BuildProgrammaticCell(int cost)
{
GridBoardDefinitionBuilder boardBuilder =
new GridBoardDefinitionBuilder(SquareGridTopology.Instance);
GridCellDefinitionBuilder cellBuilder =
boardBuilder.AddCell(SquareGridCoordinate.Zero);
cellBuilder.SetExtension(new TerrainCostDefinitionExtension(cost));
GridBoardDefinition definition = boardBuilder.Build();
if (!definition.TryGetCell(
SquareGridCoordinate.Zero,
out GridCellDefinition cell) ||
!cell.TryGetExtensionView(
TerrainCostDefinitionExtension.Id,
out TerrainCostReadModel readModel))
{
throw new InvalidOperationException("Terrain cost was not built.");
}
return readModel;
}
Board and structure definition builders provide the equivalent scope-specific extension methods.
Register and read authored data
For authored square cells, project tooling calls AddOrReplaceExtension(...) on
SquareGridCellAuthoringData.
Other topology-specific authoring data exposes the same method through
GridCellAuthoringData.
During the build, ApplyTo(...) validates the authored value and clones it into the definition
builder:
Excerpt — apply authored extension data to a definition builder. Tested using public APIs.
public static TerrainCostReadModel BuildAuthoredCell(int cost)
{
SquareGridCellAuthoringData authoredCell = new();
authoredCell.SetCoordinate(
new SquareGridCoordinateAuthoringData(0, 0));
authoredCell.AddOrReplaceExtension(
new TerrainCostDefinitionExtension(cost));
GridBoardDefinitionBuilder boardBuilder =
new GridBoardDefinitionBuilder(SquareGridTopology.Instance);
GridCellDefinitionBuilder cellBuilder =
boardBuilder.AddCell(SquareGridCoordinate.Zero);
authoredCell.ApplyTo(cellBuilder);
GridBoardDefinition definition = boardBuilder.Build();
if (!definition.TryGetCell(
SquareGridCoordinate.Zero,
out GridCellDefinition cell) ||
!cell.TryGetExtensionView(
TerrainCostDefinitionExtension.Id,
out TerrainCostReadModel readModel))
{
throw new InvalidOperationException("Authored terrain cost was not built.");
}
return readModel;
}
The returned TerrainCostReadModel is a point-in-time copy. Changing the original authoring object
cannot change the built definition. A project-owned custom Inspector may expose the serialized
extension list; the built-in generic cell Inspector does not promise a special field for every
project extension.
Verification
Test that assignment and build each create independent ownership, the stable extension ID is preserved, invalid authored values are reported, the built definition is read-only, and detached read models do not change when another source object changes.
If the definition data must be saved inside definition snapshots, also provide and register an
IGridExtensionSnapshotResolver for the same stable extension ID.
Troubleshooting
| Symptom | Check |
|---|---|
| The value disappears after building. | Confirm the owned clone and read model preserve the extension ID and return an independent non-null value. |
| The Inspector cannot preserve the value. | Implement the authoring clone contract and keep the serialized type public and serializable. |
| Gameplay needs to change the value. | Move it to a runtime extension and mutate it through an action; definition extensions are immutable. |
Related
- Customize board data and rules
- Definitions and state
- Runtime extensions and detached read models
- Snapshot resolvers and restore policies
- IGridDefinitionExtensionOwnership
- IGridDefinitionFreezableExtension
- GridRuntimeDefinitionExtensionBase
- IGridAuthoringCloneableExtension
- IGridAuthoringValidatableExtension
Done when
The authoring value, definition-owned value, and read model are independent objects with the same stable ID and the built value cannot be mutated accidentally.