Data-Driven Service Flow

This page documents the recommended data-driven architecture used across Luna Systems:

  • ServiceLocator for runtime resolution
  • Data package catalogs (ICatalog, IAssetCatalog<T>)
  • CatalogKey fields for serializable references
  • IDataSerializer/Newtonsoft for JSON serialization

The concrete example below uses Inventory sample assets/code from Packages/com.cupkekgames.luna/Samples~/Library, but the same pattern applies to other systems.

Big Picture

1) Authoring Model (Serializable Data First)

Inventory is the example model:

  • InventoryItemDefinitionSO : DataSO<InventoryItemDefinition>
  • InventoryItemDefinition : IData
  • InventoryItemDefinition.IconKey : CatalogKey
  • InventoryItem.Key : CatalogKey
  • InventoryItemReference.ItemKey : CatalogKey

This keeps references stable for JSON/saves and avoids direct hard references inside runtime data models.

1.5) Static feature definitions vs runtime feature state

IFeature on data models (e.g. InventoryItemDefinition.Features) is authored, static behavior/config: equip rules, tooltip layout, cooldown duration, etc.

Mutable data that belongs to a specific runtime instance (last use time, durability remaining, charges left) must not live on shared definitions. Use IFeatureStateData from the Data package and store a [SerializeReference] List<IFeatureStateData> on the runtime entity (for example InventoryItem).

  • Each state type implements IFeatureStateData.CloneState() for deep copy when cloning the owner (e.g. stack split).
  • Per-field serialization is decided on the state class: [SerializeField] for persisted values, [NonSerialized] for session-only fields (e.g. Time.time-based cooldown timestamps that should reset on load).
  • Features read/write state through the runtime context they already receive (e.g. ItemTooltipContext.Item.GetState<T>()), using definition fields for config and state fields for what changes at runtime.

Example (inventory consumable): ConsumableFeature holds CooldownDuration on the definition; ConsumableCooldownState on the stack holds LastUsedTime.

Example (tier override): ItemTierFeature.Tier is the authored default; ItemTierRuntimeState.TierOverride on the stack overrides tooltip and slot USS when set (empty override falls back to the definition).

The same split applies to other systems (abilities, quests, etc.)—Inventory is not a special case in the architecture.

When serializing polymorphic lists with Newtonsoft and TypeNameHandling, register concrete IFeatureStateData types in your SerializationTypeProviderSO known-types list.

1.6) Sub-interface type filtering for features

When multiple systems use IFeature (e.g. inventory has IItemFeature, units have IUnitFeatureDefinition), the FeatureDrawer shows all IFeature types in every dropdown — including types from unrelated systems.

Solution: Create a sub-interface and a matching [CustomPropertyDrawer]:

  1. Define a sub-interface that extends IFeature:
csharp
public interface IMySystemFeature : IFeature { }
  1. Use the sub-interface as the list type on your data model:
csharp
[SerializeReference] private List<IMySystemFeature> _features = new();
  1. Register a drawer for the sub-interface. Unity picks the most specific drawer, so [CustomPropertyDrawer(typeof(IMySystemFeature), true)] takes priority over the base FeatureDrawer:
csharp
[CustomPropertyDrawer(typeof(IMySystemFeature), true)] public class MySystemFeatureDrawer : PropertyDrawer { // Same pattern as FeatureDrawer, but BuildCache() scans for // typeof(IMySystemFeature).IsAssignableFrom(type) instead of IFeature }

This ensures the Inspector dropdown only shows feature types relevant to your system. The concrete feature classes implement your sub-interface and inherit CloneFeature() from IFeature.

Example: HeroManager defines IUnitFeatureDefinition : IFeature for unit definitions (CombatAttributesDefinition, CharacterDefinition, NpcDefinition). The UnitFeatureDefinitionDrawer only shows these three types, not inventory features.

2) Catalog IDs (Example Setup)

Sample constants:

  • InventoryConstants.ItemsCatalogId = "Items"
  • InventoryConstants.IconsCatalogId = "ItemIcon"

AssetCatalog<T> registers itself to ServiceLocator for:

  • ICatalog (key listing)
  • IAssetCatalog (untyped value lookup)
  • IAssetCatalog<T> (typed value lookup)

All are keyed by catalog id and use append: true, so multiple catalog assets can contribute to one id.

Concrete sample assets

From Luna Demo Service Registry Items.asset:

  • Item Provider Equipments.asset (_catalogId: Items)
  • Item Provider Potions.asset (_catalogId: Items)
  • ItemSpriteDatabase.asset (_catalogId: ItemIcon)
  • ItemTypeKeyProvider.asset (_catalogId: ItemType)
  • EquipmentTypeKeyProvider.asset (_catalogId: EquipmentType)
  • ItemTierKeyProvider.asset (_catalogId: ItemTier)
  • CupkekGames Data Serializer Registrar.asset (registers IDataSerializer)

Result: definitions can be split across multiple catalogs while still resolving through one logical key space (Items).

3) Runtime Resolution Path (Example)

InventoryItemDatabaseSO is the bridge that gameplay/UI code uses through IInventoryItemDatabase:

  1. GetItemDefinition(CatalogKey) validates catalog id (defaults to Items if empty)
  2. Iterates ServiceLocator.GetAll<IAssetCatalog<InventoryItemDefinitionSO>>("Items")
  3. Returns the first matching InventoryItemDefinitionSO.Data
  4. For icons, resolves IAssetCatalog<Sprite> by IconKey.Catalog (fallback ItemIcon)

This is why both Items and ItemIcon catalogs must be registered before inventory UI/gameplay runs.

4) Serialization Path (DataSO + IDataSerializer)

DataSO<T> uses ServiceLocator.Get<IDataSerializer>() for:

  • ToJson()
  • LoadFromJson()

In samples, DataSerializerRegistrar registers NewtonsoftDataSerializer as IDataSerializer.

Important requirement

NewtonsoftDataSerializer also depends on ServiceLocator.Get<SerializationManager>(). So if you call DataSO JSON APIs at runtime, you must register a SerializationManager too (for example through SerializationManagerRegistrar in the Newtonsoft sample registry).

In short:

  • Required for DataSO JSON methods: IDataSerializer + SerializationManager
  • Required for catalog-backed runtime lookups: relevant catalogs registered under expected ids

5) Minimal Setup Checklist

To apply this pattern in your own system:

  1. Register one or more catalogs for your domain ids
  2. Register services that resolve/bridge those catalogs to runtime APIs
  3. Register IDataSerializer (for example via DataSerializerRegistrar)
  4. Register SerializationManager if using Newtonsoft JSON calls (ToJson, LoadFromJson, save/load)
  5. Verify with Service Locator Debug window that all services and keys are present

5.1) Samples vs your project (what to own)

Importing Luna samples is for reference and prototyping. For a real project you should still:

  • Keep bootstrap assets (ServiceRegistrySO, registrar assets, catalog assets) under your Assets/... tree—not assumptions that a demo registry path will always exist or stay enabled.
  • Treat sample ServiceRegistrySO assets as templates: duplicate or recreate them and wire your providers. Sample registries ship with Register In Editor off so they do not silently populate the global locator; your own registry controls when and what registers (Service Locator).

Code: NewtonsoftDataSerializer, DataSerializerRegistrar, SerializationManager, SerializationManagerRegistrar, and SerializationTypeProviderSO live under the Library and Newtonsoft samples (CupkekGames.Data.Newtonsoft, CupkekGames.Newtonsoft). Either import those samples so the assemblies compile, or copy the source into your game assemblies and adjust asmdef references (including com.unity.nuget.newtonsoft-json).

Copying only one registrar asset (for example SerializationManagerRegistrar) is not enough for DataSO JSON: you need both registrars and a configured SerializationManagerRegistrar (see todo list below).

5.2) Todo: catalogs only (no DataSO JSON / no IDataSerializer)

Use this path if you only need CatalogKey resolution and never call ToJson / LoadFromJson on DataSO at runtime.

  • Create catalog assets (Create → CupkekGames → Data → Catalog) and set Catalog Id strings to match your CatalogKey.Catalog values (Data Catalogs).
  • Ensure catalogs register at runtime (scene ServiceRegistry, ServiceRegistrySO, sequencer node, or your bootstrap)—same pattern as samples, but your assets/scenes.
  • Register any bridge services your feature expects (for example an item database that queries IAssetCatalog<...> by id).
  • Open the Service Locator debug UI and confirm catalog keys and services resolve.

5.3) Todo: add Newtonsoft + DataSO JSON (IDataSerializer)

Add this block when you use DataSO.ToJson / LoadFromJson, inventory-style JSON, or other code that resolves ServiceLocator.Get<IDataSerializer>() / SerializationManager.

  • Add the com.unity.nuget.newtonsoft-json package if the project does not already reference it.
  • Ensure sample code is part of your build: import Library + Newtonsoft samples, or copy the relevant runtime folders into Assets with working asmdef references.
  • Create a SerializationManagerRegistrar asset (Create → CupkekGames → Data → Newtonsoft → Serialization Manager Registrar). Assign at least the package default type provider asset (Luna Newtonsoft Default Type Provider SO when using imported samples), or your own SerializationTypeProviderSO that supplies known types, converters, and optional contract resolver. Add game-specific providers (for example a subclass like DemoSerializationTypesSO) when you use TypeNameHandling with your own polymorphic types—see Newtonsoft JSON sample and §1.5 above for IFeatureStateData registration.
  • Create a DataSerializerRegistrar asset (Create → CupkekGames → Data → Newtonsoft Serializer Registrar). It has no extra fields; it registers NewtonsoftDataSerializer as IDataSerializer.
  • Add both registrar assets to your ServiceRegistrySO (or equivalent play-mode registration). Order only matters insofar as both must finish registering before the first JSON call; the demo registry lists DataSerializerRegistrar then SerializationManagerRegistrar, which is valid because the serializer does not touch SerializationManager until used.
  • Register your catalogs and other services as in §5.2.
  • Verify in the Service Locator debug UI: SerializationManager and IDataSerializer are present before exercising JSON APIs.

If SerializationManager must be initialized before use appears, the manager was never registered or SerializationManagerRegistrar did not run (missing registry wiring, wrong scene, or provider list empty / broken).

6) Common Failure Modes

  • No IAssetCatalog<...> registered with key '...'
    • Missing catalog registration.
  • IAssetCatalog<...>('...') not found in ServiceLocator
    • Wrong/missing catalog id registration.
  • SerializationManager must be initialized before use
    • IDataSerializer exists, but SerializationManager was not registered/initialized.

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL