Building a Scalable XP System in Unity with ScriptableObjects and Events
The climb from game jam curiosity to seasoned Unity developer is paved with more than just good intentions. It demands a robust, scalable game architecture, and few things highlight architectural deficiencies quite like implementing a progression system. A poorly designed XP system can quickly become a spaghetti code monster, making future balancing and feature additions a nightmare. Let’s ditch the typical XP bar tutorial and build something truly adaptable, leveraging ScriptableObjects and a simple event-driven architecture to create an XP system that’s not just functional, but smart.
Defining the Level Curve: The Power of ScriptableObjects
The most-downloaded assets on Wayline this month — sorted by what real developers actually use.
Forget hardcoding level requirements. That’s a recipe for imbalance and endless tweaking in code. The better approach is to define your level curve using ScriptableObjects. They’re data containers that exist independently of scene instances, meaning they can be easily edited in the Inspector without recompiling your code. This allows for rapid iteration and experimentation.
Here’s a step-by-step guide to creating a ScriptableObject for your level curve:
- Create a new C# script named
LevelData.cs. - Replace the contents with the following code:
using UnityEngine;
[CreateAssetMenu(fileName = "LevelData", menuName = "GameData/LevelData", order = 1)]
public class LevelData : ScriptableObject
{
public int level;
public int requiredXP;
}
- Create another C# script named
LevelCurve.cs - Replace the contents with the following code:
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "LevelCurve", menuName = "GameData/LevelCurve", order = 0)]
public class LevelCurve : ScriptableObject
{
public List<LevelData> levels;
public int GetRequiredXPForLevel(int level)
{
foreach (var levelData in levels)
{
if (levelData.level == level)
{
return levelData.requiredXP;
}
}
Debug.LogError("Level not found in LevelCurve: " + level);
return -1; // Indicate an error
}
}
Now, in your Unity project, right-click in the Project window, go to Create -> GameData -> LevelCurve. Name it "DefaultLevelCurve". Select the newly created asset. You’ll see a list in the Inspector where you can add LevelData objects. Populate this list with your desired level progression, specifying the level number and the XP required to reach it. For example, Level 1 might require 100 XP, Level 2 might require 250 XP, and so on. To create a LevelData asset, right-click in the Project window, go to Create -> GameData -> LevelData. Add these level data objects to your level curve.
Pitfall: Forgetting to sort your levels list in LevelCurve.cs by level number can lead to incorrect XP requirements. Add a sorting step after populating the list in the Inspector to avoid this.
Awarding XP: The Event-Driven Approach
Directly calling XP-granting functions from every action in your game is a maintenance nightmare. Imagine having to track down every instance of player.AddXP(50) when you need to adjust the reward for defeating a specific enemy. The solution is an event system.
Create a simple XPGainedEvent.cs script:
using UnityEngine;
using UnityEngine.Events;
[System.Serializable]
public class IntEvent : UnityEvent<int> {}
public class XPGainedEvent : MonoBehaviour
{
public static XPGainedEvent current;
public IntEvent onXPGained;
void Awake()
{
current = this;
}
public void Raise(int xp)
{
onXPGained.Invoke(xp);
}
}
Create a new GameObject in your scene and attach the script XPGainedEvent.
Now, when an action occurs that should award XP (e.g., defeating an enemy), don’t directly modify the player’s XP. Instead, call XPGainedEvent.current.Raise(xpAmount);. This raises an event that any other script can listen for.
Here’s how your PlayerXP.cs script might look:
using UnityEngine;
using UnityEngine.UI;
public class PlayerXP : MonoBehaviour
{
public LevelCurve levelCurve;
public int currentLevel = 1;
public int currentXP = 0;
public Image xpBar; // Reference to the UI Image for the XP bar
public Text levelText; // Reference to the UI Text to display the current level
void Start()
{
XPGainedEvent.current.onXPGained.AddListener(AddXP);
// Initialize UI
UpdateXPBar();
UpdateLevelText();
}
public void AddXP(int xp)
{
currentXP += xp;
CheckForLevelUp();
UpdateXPBar();
}
void CheckForLevelUp()
{
int requiredXP = levelCurve.GetRequiredXPForLevel(currentLevel);
if (currentXP >= requiredXP)
{
currentXP -= requiredXP;
currentLevel++;
UpdateLevelText();
CheckForLevelUp(); // In case of multiple level ups
}
}
void UpdateXPBar()
{
int requiredXP = levelCurve.GetRequiredXPForLevel(currentLevel);
float fillAmount = (float)currentXP / requiredXP;
xpBar.fillAmount = fillAmount;
}
void UpdateLevelText()
{
levelText.text = "Level: " + currentLevel;
}
}
Attach this PlayerXP.cs to your player GameObject. Drag your DefaultLevelCurve ScriptableObject into the Level Curve field of the PlayerXP component in the Inspector. Also make sure you create a UI Image object for the XP bar as well as a UI Text object to display the level and drag those into the corresponding fields in the Inspector.
Value Beyond Surface Level: Notice how CheckForLevelUp recursively calls itself. This handles scenarios where the player gains enough XP to level up multiple times at once, preventing the need for complex loop structures.
Visualizing Progress: UI and Polish
A functional XP system is useless if the player can’t track their progress. Bind the PlayerXP script to a UI element to display the current level and an XP bar.
In the PlayerXP script, you’ll need to add references to UI elements (specifically, a UnityEngine.UI.Image for the XP bar and a UnityEngine.UI.Text for the level display) and update them in the AddXP function. You’ll also need to add code to calculate the fill amount for the XP bar based on the player’s current XP and the required XP for the next level.
Challenge: Getting the XP bar to fill smoothly requires careful consideration of the UI update cycle. Use Time.deltaTime to animate the bar gradually, rather than snapping to the new value instantly. This makes the progress feel more rewarding.
Common Mistakes and How to Avoid Them
- Hardcoding Values: Avoid hardcoding XP rewards or level requirements. Use ScriptableObjects for data-driven design.
- Ignoring Edge Cases: Account for scenarios like negative XP, level caps, and extremely large XP gains.
- Performance Issues: Constantly updating the UI can be expensive. Optimize your UI update calls to minimize performance impact.
By using ScriptableObjects and an event system, you can build a flexible and scalable XP system that adapts to the needs of your game. This approach will save you countless hours of debugging and rebalancing, allowing you to focus on what truly matters: creating a compelling player experience.