This page documents the recommended data-driven architecture used across Luna Systems:
ServiceLocator for runtime resolutionICatalog, IAssetCatalog<T>)CatalogKey fields for serializable referencesIDataSerializer/Newtonsoft for JSON serializationThe concrete example below uses Inventory sample assets/code from Packages/com.cupkekgames.luna/Samples~/Library, but the same pattern applies to other systems.
Inventory is the example model:
InventoryItemDefinitionSO : DataSO<InventoryItemDefinition>InventoryItemDefinition : IDataInventoryItemDefinition.IconKey : CatalogKeyInventoryItem.Key : CatalogKeyInventoryItemReference.ItemKey : CatalogKeyThis keeps references stable for JSON/saves and avoids direct hard references inside runtime data models.
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).
IFeatureStateData.CloneState() for deep copy when cloning the owner (e.g. stack split).[SerializeField] for persisted values, [NonSerialized] for session-only fields (e.g. Time.time-based cooldown timestamps that should reset on load).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.
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]:
IFeature:public interface IMySystemFeature : IFeature { }[SerializeReference] private List<IMySystemFeature> _features = new();[CustomPropertyDrawer(typeof(IMySystemFeature), true)] takes priority over the base FeatureDrawer:[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.
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.
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).
InventoryItemDatabaseSO is the bridge that gameplay/UI code uses through IInventoryItemDatabase:
GetItemDefinition(CatalogKey) validates catalog id (defaults to Items if empty)ServiceLocator.GetAll<IAssetCatalog<InventoryItemDefinitionSO>>("Items")InventoryItemDefinitionSO.DataIAssetCatalog<Sprite> by IconKey.Catalog (fallback ItemIcon)This is why both Items and ItemIcon catalogs must be registered before inventory UI/gameplay runs.
DataSO<T> uses ServiceLocator.Get<IDataSerializer>() for:
ToJson()LoadFromJson()In samples, DataSerializerRegistrar registers NewtonsoftDataSerializer as IDataSerializer.
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:
IDataSerializer + SerializationManagerTo apply this pattern in your own system:
IDataSerializer (for example via DataSerializerRegistrar)SerializationManager if using Newtonsoft JSON calls (ToJson, LoadFromJson, save/load)Importing Luna samples is for reference and prototyping. For a real project you should still:
ServiceRegistrySO, registrar assets, catalog assets) under your Assets/... tree—not assumptions that a demo registry path will always exist or stay enabled.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).
DataSO JSON / no IDataSerializer)Use this path if you only need CatalogKey resolution and never call ToJson / LoadFromJson on DataSO at runtime.
CatalogKey.Catalog values (Data Catalogs).ServiceRegistry, ServiceRegistrySO, sequencer node, or your bootstrap)—same pattern as samples, but your assets/scenes.IAssetCatalog<...> by id).DataSO JSON (IDataSerializer)Add this block when you use DataSO.ToJson / LoadFromJson, inventory-style JSON, or other code that resolves ServiceLocator.Get<IDataSerializer>() / SerializationManager.
com.unity.nuget.newtonsoft-json package if the project does not already reference it.Assets with working asmdef references.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.DataSerializerRegistrar asset (Create → CupkekGames → Data → Newtonsoft Serializer Registrar). It has no extra fields; it registers NewtonsoftDataSerializer as IDataSerializer.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.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).
No IAssetCatalog<...> registered with key '...'
IAssetCatalog<...>('...') not found in ServiceLocator
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)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction