Experiences (XP & Levels)

The Experiences sub-asmdef of com.cupkekgames.resources provides a curve-driven XP tracker keyed by experience-track id (e.g. "Hero", "Crafting", "Reputation").

  • Assembly: CupkekGames.Resources.Experiences
  • Namespace: CupkekGames.Resources.Experiences (+ .Curves for curve types)
  • Deps: com.cupkekgames.data, com.cupkekgames.services

Types

TypeRole
Experience (struct)(string Id, long Amount) transient pair
ExperienceDefinitionSOPer-track ScriptableObject — display name, icon, max level, [CatalogKeyConstraint] CurveKey pointing into the curve catalog
ExperienceCatalogAssetCatalog<ExperienceDefinitionSO> with id "Experiences"
ExperienceTrackerIData runtime store. Get / AddExp / GetLevel / GetExpPercent + OnExperienceChanged and OnLevelUp(id, oldLevel, newLevel) events
ExperienceHelperPure static math (migrated from data.primitives). Reused by ExperienceTracker and direct callers
ExperienceConstantsCatalog id constants + DefaultMaxLevel = 90

Curve system (.Curves namespace)

TypeRole
ExperienceCurveSOAbstract base. One method: int GetRequiredExperience(int level)
ExperienceCurveCatalogAssetCatalog<ExperienceCurveSO> with id "ExperienceCurves"
PolynomialExperienceCurveSOoffset + multiplier * level^power
LinearExperienceCurveSOoffset + multiplier * level
SteppedExperienceCurveSODesigner-authored int[] per-level table

Curves as catalog entries — single source of truth

Authoring a ExperienceDefinitionSO doesn't embed a curve formula. Instead the definition's CurveKey references a registered ExperienceCurveSO asset by CatalogKey.

Why this shape:

  • One curve asset serves many tracks. "Hero" and "Pet" can both reference StandardQuadratic — tweak the formula once, both update.
  • Curve type is data, not code. Designers pick from a dropdown in the inspector. Adding a new curve flavor is one new ExperienceCurveSO subclass.
  • The package ships three concrete curves; consumers add their own for game-specific shapes.

Concrete example. HeroManager's legacy int RequiredExperience(int level) => 10 + 10 * level² formula becomes a PolynomialExperienceCurveSO asset with offset = 10, multiplier = 10, power = 2.

Authoring

  1. Create the curve catalogCreate → CupkekGames/Resources/Catalog/Experience Curves. Set _catalogId = "ExperienceCurves".
  2. Create one or more curve assetsCreate → CupkekGames/Resources/Experience Curve/{Polynomial | Linear | Stepped}. Tune values in inspector.
  3. Create the experiences catalogCreate → CupkekGames/Resources/Catalog/Experiences. Set _catalogId = "Experiences".
  4. Create one ExperienceDefinitionSO per trackCreate → CupkekGames/Resources/Experience Definition. Set display metadata + max level. Pick a curve in the CurveKey dropdown (filtered to assets registered in the curve catalog).
  5. Drop all catalog assets into a ServiceRegistrySO so they register on scene/init load.

Runtime

csharp
using CupkekGames.Resources.Experiences; public class MyGameSaveData : IGameSaveData, IData { public ExperienceTracker Experience = new(); // ... } // Earn XP — OnExperienceChanged fires; OnLevelUp fires if a threshold crossed: saveData.Experience.AddExp("Hero", 250); // Query level & progress: int level = saveData.Experience.GetLevel("Hero"); // resolves curve via ServiceLocator float pct = saveData.Experience.GetExpPercent("Hero"); // 0..1 within current level int needed = saveData.Experience.GetCurrentRequiredExperience("Hero");

The tracker resolves a track's curve lazily on each GetLevel / AddExp call. The lookup chain:

ExperienceTracker.AddExp("Hero", n) → ServiceLocator.Get<IAssetCatalog<ExperienceDefinitionSO>>("Experiences") → catalog.GetValue("Hero") → ExperienceDefinitionSO → def.CurveKey ("ExperienceCurves" / "StandardQuadratic") → ServiceLocator.Get<IAssetCatalog<ExperienceCurveSO>>("ExperienceCurves") → catalog.GetValue("StandardQuadratic") → ExperienceCurveSO → curve.GetRequiredExperience(level) → int

If either catalog isn't registered (e.g. in a unit test) the lookup returns null and level computation returns 0.

UI binding

Subscribe to both events on enable:

csharp
saveData.Experience.OnExperienceChanged += (id, oldValue, newValue) => { AnimateXpBar(oldValue, newValue); }; saveData.Experience.OnLevelUp += (id, oldLevel, newLevel) => { PlayLevelUpFlair(); UpdateLevelLabel(newLevel); };

OnLevelUp fires AFTER OnExperienceChanged for the same mutation, so the level-up burst can ride on top of the count-up tween.

Drop-table routing

Same pattern as currencies — a drop entry pointing at Experiences/Hero is valid markup:

csharp
foreach (var result in drops) { if (result.Key.Catalog == CurrencyConstants.CurrenciesCatalogId) wallet.Add(result.Key.Key, result.Amount); else if (result.Key.Catalog == ExperienceConstants.ExperiencesCatalogId) experience.AddExp(result.Key.Key, result.Amount); else inventory.AddItem(...); }

Authoring a new curve type

Subclass ExperienceCurveSO:

csharp
using UnityEngine; using CupkekGames.Resources.Experiences.Curves; [CreateAssetMenu( fileName = "LookupTableExperienceCurve", menuName = "MyGame/Experience Curve/Lookup Table")] public class LookupTableExperienceCurveSO : ExperienceCurveSO { [SerializeField] private TextAsset _csv; public override int GetRequiredExperience(int level) { // parse CSV, return per-level cost } }

Drop the new asset into your ExperienceCurveCatalog database; ExperienceDefinitionSO.CurveKey dropdowns include it automatically.

Save semantics

ExperienceTracker implements IData:

  • CloneData() → deep copy of the totals dictionary (no service-locator calls — safe for default→actual reset)
  • Validate() → always true
  • OnAfterDeserialize() → no-op

Serialize via your project's IDataSerializer.

ExperienceFeature (HeroManager-style per-Unit XP) — not in this package

If you want XP attached to a specific Unit (per-character XP that dies with the unit) rather than at save-data scope, use HeroManager's ExperienceFeature : IUnitFeature pattern from com.cupkekgames.units. That's a separate consumer of ExperienceHelper. A generic version belongs in a future com.cupkekgames.resources.units bridge package.

The two coexist:

ExperienceFeature (per-Unit)ExperienceTracker (this package)
LifecycleOne per Unit, dies with UnitOne per save, holds all tracks
Storageint _totalExpDictionary<string, long>
Curve sourceHardcoded in the classExperienceDefinitionSO.CurveKey → curve catalog
Event surfaceNoneOnExperienceChanged, OnLevelUp

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