The Experiences sub-asmdef of com.cupkekgames.resources provides a curve-driven XP tracker keyed by experience-track id (e.g. "Hero", "Crafting", "Reputation").
CupkekGames.Resources.ExperiencesCupkekGames.Resources.Experiences (+ .Curves for curve types)com.cupkekgames.data, com.cupkekgames.services| Type | Role |
|---|---|
Experience (struct) | (string Id, long Amount) transient pair |
ExperienceDefinitionSO | Per-track ScriptableObject — display name, icon, max level, [CatalogKeyConstraint] CurveKey pointing into the curve catalog |
ExperienceCatalog | AssetCatalog<ExperienceDefinitionSO> with id "Experiences" |
ExperienceTracker | IData runtime store. Get / AddExp / GetLevel / GetExpPercent + OnExperienceChanged and OnLevelUp(id, oldLevel, newLevel) events |
ExperienceHelper | Pure static math (migrated from data.primitives). Reused by ExperienceTracker and direct callers |
ExperienceConstants | Catalog id constants + DefaultMaxLevel = 90 |
.Curves namespace)| Type | Role |
|---|---|
ExperienceCurveSO | Abstract base. One method: int GetRequiredExperience(int level) |
ExperienceCurveCatalog | AssetCatalog<ExperienceCurveSO> with id "ExperienceCurves" |
PolynomialExperienceCurveSO | offset + multiplier * level^power |
LinearExperienceCurveSO | offset + multiplier * level |
SteppedExperienceCurveSO | Designer-authored int[] per-level table |
Authoring a ExperienceDefinitionSO doesn't embed a curve formula. Instead the definition's CurveKey references a registered ExperienceCurveSO asset by CatalogKey.
Why this shape:
StandardQuadratic — tweak the formula once, both update.ExperienceCurveSO subclass.Concrete example. HeroManager's legacy int RequiredExperience(int level) => 10 + 10 * level² formula becomes a PolynomialExperienceCurveSO asset with offset = 10, multiplier = 10, power = 2.
Create → CupkekGames/Resources/Catalog/Experience Curves. Set _catalogId = "ExperienceCurves".Create → CupkekGames/Resources/Experience Curve/{Polynomial | Linear | Stepped}. Tune values in inspector.Create → CupkekGames/Resources/Catalog/Experiences. Set _catalogId = "Experiences".ExperienceDefinitionSO per track — Create → CupkekGames/Resources/Experience Definition. Set display metadata + max level. Pick a curve in the CurveKey dropdown (filtered to assets registered in the curve catalog).ServiceRegistrySO so they register on scene/init load.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) → intIf either catalog isn't registered (e.g. in a unit test) the lookup returns null and level computation returns 0.
Subscribe to both events on enable:
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.
Same pattern as currencies — a drop entry pointing at Experiences/Hero is valid markup:
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(...);
}Subclass ExperienceCurveSO:
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.
ExperienceTracker implements IData:
CloneData() → deep copy of the totals dictionary (no service-locator calls — safe for default→actual reset)Validate() → always trueOnAfterDeserialize() → no-opSerialize via your project's IDataSerializer.
ExperienceFeature (HeroManager-style per-Unit XP) — not in this packageIf 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) | |
|---|---|---|
| Lifecycle | One per Unit, dies with Unit | One per save, holds all tracks |
| Storage | int _totalExp | Dictionary<string, long> |
| Curve source | Hardcoded in the class | ExperienceDefinitionSO.CurveKey → curve catalog |
| Event surface | None | OnExperienceChanged, OnLevelUp |
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