Lesson 10: Level Progression & Save System

Welcome to the essential lesson on game progression! You'll learn how to create a robust level progression system and save/load functionality that keeps players engaged and coming back to your 2D platformer.

What You'll Learn

In this lesson, you'll master:

  • Level Unlocking System - Progressive level access based on completion
  • Save/Load Functionality - Persistent game data across sessions
  • Level Selection UI - Beautiful level selection interface
  • Progress Tracking - Stars, scores, and completion status
  • Data Persistence - Player preferences and game settings

Why Level Progression Matters

A good progression system is crucial for player retention:

  • Player Engagement - Gives players goals to work toward
  • Sense of Achievement - Unlocking new content feels rewarding
  • Replayability - Players can revisit completed levels
  • Data Persistence - Progress isn't lost between sessions
  • Professional Polish - Makes your game feel complete

Step 1: Setting Up the Save System

Create Game Data Structure

First, let's create a data structure to store our game progress:

[System.Serializable]
public class GameData
{
    public int currentLevel = 1;
    public int totalStars = 0;
    public int totalScore = 0;
    public bool[] levelsUnlocked;
    public int[] levelStars;
    public float[] levelBestTimes;
    public bool soundEnabled = true;
    public bool musicEnabled = true;
    public float masterVolume = 1f;

    public GameData()
    {
        levelsUnlocked = new bool[20]; // Support up to 20 levels
        levelStars = new int[20];
        levelBestTimes = new float[20];

        // Unlock first level by default
        levelsUnlocked[0] = true;
    }
}

Save System Manager

Create a singleton manager to handle all save/load operations:

public class SaveSystem : MonoBehaviour
{
    public static SaveSystem Instance { get; private set; }

    private GameData gameData;
    private string saveFileName = "GameSave.json";

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            LoadGame();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public void SaveGame()
    {
        string json = JsonUtility.ToJson(gameData, true);
        string filePath = Path.Combine(Application.persistentDataPath, saveFileName);

        try
        {
            File.WriteAllText(filePath, json);
            Debug.Log("Game saved successfully!");
        }
        catch (System.Exception e)
        {
            Debug.LogError("Failed to save game: " + e.Message);
        }
    }

    public void LoadGame()
    {
        string filePath = Path.Combine(Application.persistentDataPath, saveFileName);

        if (File.Exists(filePath))
        {
            try
            {
                string json = File.ReadAllText(filePath);
                gameData = JsonUtility.FromJson<GameData>(json);
                Debug.Log("Game loaded successfully!");
            }
            catch (System.Exception e)
            {
                Debug.LogError("Failed to load game: " + e.Message);
                gameData = new GameData();
            }
        }
        else
        {
            gameData = new GameData();
            Debug.Log("No save file found, creating new game data.");
        }
    }

    // Getters and setters for game data
    public GameData GetGameData() => gameData;
    public void SetCurrentLevel(int level) => gameData.currentLevel = level;
    public void UnlockLevel(int level) => gameData.levelsUnlocked[level - 1] = true;
    public bool IsLevelUnlocked(int level) => gameData.levelsUnlocked[level - 1];
}

Step 2: Level Progression System

Level Manager

Create a system to track and manage level progression:

public class LevelManager : MonoBehaviour
{
    [Header("Level Settings")]
    public int totalLevels = 20;
    public int starsRequiredToUnlock = 3; // Stars needed to unlock next level

    [Header("Level Completion")]
    public int currentLevelStars = 0;
    public float levelTime = 0f;
    public bool levelCompleted = false;

    private SaveSystem saveSystem;

    void Start()
    {
        saveSystem = SaveSystem.Instance;
        StartLevelTimer();
    }

    void StartLevelTimer()
    {
        levelTime = 0f;
        InvokeRepeating(nameof(UpdateTimer), 0f, 0.1f);
    }

    void UpdateTimer()
    {
        if (!levelCompleted)
        {
            levelTime += 0.1f;
        }
    }

    public void CompleteLevel(int starsEarned)
    {
        if (levelCompleted) return;

        levelCompleted = true;
        currentLevelStars = starsEarned;

        // Update save data
        GameData data = saveSystem.GetGameData();
        int levelIndex = SceneManager.GetActiveScene().buildIndex - 1;

        // Update stars if better score
        if (starsEarned > data.levelStars[levelIndex])
        {
            data.levelStars[levelIndex] = starsEarned;
            data.totalStars += (starsEarned - data.levelStars[levelIndex]);
        }

        // Update best time
        if (data.levelBestTimes[levelIndex] == 0f || levelTime < data.levelBestTimes[levelIndex])
        {
            data.levelBestTimes[levelIndex] = levelTime;
        }

        // Unlock next level
        if (levelIndex < totalLevels - 1)
        {
            data.levelsUnlocked[levelIndex + 1] = true;
        }

        // Save progress
        saveSystem.SaveGame();

        // Show completion UI
        ShowLevelCompleteUI();
    }

    void ShowLevelCompleteUI()
    {
        // This will be implemented in the UI section
        Debug.Log($"Level completed! Stars: {currentLevelStars}, Time: {levelTime:F1}s");
    }
}

Star Collection System

Implement a star collection system for level completion:

public class Star : MonoBehaviour
{
    [Header("Star Settings")]
    public int starValue = 1;
    public float collectRadius = 1f;
    public AudioClip collectSound;

    private bool collected = false;
    private LevelManager levelManager;

    void Start()
    {
        levelManager = FindObjectOfType<LevelManager>();
    }

    void Update()
    {
        if (!collected)
        {
            CheckPlayerProximity();
        }
    }

    void CheckPlayerProximity()
    {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player != null)
        {
            float distance = Vector2.Distance(transform.position, player.transform.position);
            if (distance <= collectRadius)
            {
                CollectStar();
            }
        }
    }

    void CollectStar()
    {
        collected = true;

        // Play collection effect
        if (collectSound != null)
        {
            AudioSource.PlayClipAtPoint(collectSound, transform.position);
        }

        // Add particle effect
        CreateCollectionEffect();

        // Update level manager
        if (levelManager != null)
        {
            levelManager.currentLevelStars += starValue;
        }

        // Hide the star
        gameObject.SetActive(false);
    }

    void CreateCollectionEffect()
    {
        // Create a simple particle effect
        GameObject effect = new GameObject("StarEffect");
        effect.transform.position = transform.position;

        // Add a simple visual effect
        SpriteRenderer effectRenderer = effect.AddComponent<SpriteRenderer>();
        effectRenderer.sprite = GetComponent<SpriteRenderer>().sprite;
        effectRenderer.color = Color.yellow;

        // Scale animation
        StartCoroutine(ScaleEffect(effect));
    }

    IEnumerator ScaleEffect(GameObject effect)
    {
        float duration = 0.5f;
        float elapsed = 0f;
        Vector3 startScale = Vector3.one;
        Vector3 endScale = Vector3.one * 2f;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float progress = elapsed / duration;
            effect.transform.localScale = Vector3.Lerp(startScale, endScale, progress);
            effect.GetComponent<SpriteRenderer>().color = Color.Lerp(Color.yellow, Color.clear, progress);
            yield return null;
        }

        Destroy(effect);
    }
}

Step 3: Level Selection UI

Level Selection Manager

Create a level selection interface:

public class LevelSelectionManager : MonoBehaviour
{
    [Header("UI References")]
    public Transform levelButtonParent;
    public GameObject levelButtonPrefab;
    public Text totalStarsText;
    public Text totalScoreText;

    [Header("Level Settings")]
    public int levelsPerRow = 5;
    public float buttonSpacing = 120f;

    private SaveSystem saveSystem;
    private GameData gameData;

    void Start()
    {
        saveSystem = SaveSystem.Instance;
        gameData = saveSystem.GetGameData();
        CreateLevelButtons();
        UpdateUI();
    }

    void CreateLevelButtons()
    {
        for (int i = 0; i < gameData.levelsUnlocked.Length; i++)
        {
            GameObject button = Instantiate(levelButtonPrefab, levelButtonParent);
            LevelButton levelButton = button.GetComponent<LevelButton>();

            int levelNumber = i + 1;
            bool isUnlocked = gameData.levelsUnlocked[i];
            int stars = gameData.levelStars[i];
            float bestTime = gameData.levelBestTimes[i];

            levelButton.SetupLevel(levelNumber, isUnlocked, stars, bestTime);

            // Position the button
            int row = i / levelsPerRow;
            int col = i % levelsPerRow;
            Vector3 position = new Vector3(col * buttonSpacing, -row * buttonSpacing, 0);
            button.transform.localPosition = position;
        }
    }

    void UpdateUI()
    {
        totalStarsText.text = $"Total Stars: {gameData.totalStars}";
        totalScoreText.text = $"Total Score: {gameData.totalScore}";
    }

    public void LoadLevel(int levelNumber)
    {
        if (gameData.levelsUnlocked[levelNumber - 1])
        {
            gameData.currentLevel = levelNumber;
            saveSystem.SaveGame();
            SceneManager.LoadScene(levelNumber);
        }
    }
}

Level Button Component

Create individual level buttons:

public class LevelButton : MonoBehaviour
{
    [Header("UI Elements")]
    public Text levelNumberText;
    public Image lockImage;
    public Image[] starImages;
    public Text bestTimeText;
    public Button button;

    [Header("Visual Settings")]
    public Color unlockedColor = Color.white;
    public Color lockedColor = Color.gray;
    public Color starEarnedColor = Color.yellow;
    public Color starEmptyColor = Color.gray;

    private int levelNumber;
    private bool isUnlocked;

    public void SetupLevel(int levelNum, bool unlocked, int stars, float bestTime)
    {
        levelNumber = levelNum;
        isUnlocked = unlocked;

        levelNumberText.text = levelNum.ToString();

        // Set button interactability
        button.interactable = unlocked;

        // Show/hide lock
        lockImage.gameObject.SetActive(!unlocked);

        // Update stars
        for (int i = 0; i < starImages.Length; i++)
        {
            if (i < stars)
            {
                starImages[i].color = starEarnedColor;
            }
            else
            {
                starImages[i].color = starEmptyColor;
            }
        }

        // Update best time
        if (bestTime > 0f)
        {
            bestTimeText.text = $"{bestTime:F1}s";
        }
        else
        {
            bestTimeText.text = "--";
        }

        // Set visual state
        UpdateVisualState();
    }

    void UpdateVisualState()
    {
        Color targetColor = isUnlocked ? unlockedColor : lockedColor;
        GetComponent<Image>().color = targetColor;
    }

    public void OnButtonClick()
    {
        if (isUnlocked)
        {
            LevelSelectionManager levelManager = FindObjectOfType<LevelSelectionManager>();
            levelManager.LoadLevel(levelNumber);
        }
    }
}

Step 4: Settings and Preferences

Settings Manager

Create a settings system for player preferences:

public class SettingsManager : MonoBehaviour
{
    [Header("Audio Settings")]
    public Slider masterVolumeSlider;
    public Slider musicVolumeSlider;
    public Slider sfxVolumeSlider;
    public Toggle soundToggle;
    public Toggle musicToggle;

    [Header("Game Settings")]
    public Toggle fullscreenToggle;
    public Dropdown qualityDropdown;
    public Dropdown resolutionDropdown;

    private SaveSystem saveSystem;
    private GameData gameData;

    void Start()
    {
        saveSystem = SaveSystem.Instance;
        gameData = saveSystem.GetGameData();
        LoadSettings();
        SetupEventListeners();
    }

    void LoadSettings()
    {
        // Load audio settings
        masterVolumeSlider.value = gameData.masterVolume;
        soundToggle.isOn = gameData.soundEnabled;
        musicToggle.isOn = gameData.musicEnabled;

        // Load display settings
        fullscreenToggle.isOn = Screen.fullScreen;
        qualityDropdown.value = QualitySettings.GetQualityLevel();

        // Setup resolution dropdown
        SetupResolutionDropdown();
    }

    void SetupEventListeners()
    {
        masterVolumeSlider.onValueChanged.AddListener(SetMasterVolume);
        musicVolumeSlider.onValueChanged.AddListener(SetMusicVolume);
        sfxVolumeSlider.onValueChanged.AddListener(SetSFXVolume);
        soundToggle.onValueChanged.AddListener(ToggleSound);
        musicToggle.onValueChanged.AddListener(ToggleMusic);
        fullscreenToggle.onValueChanged.AddListener(ToggleFullscreen);
        qualityDropdown.onValueChanged.AddListener(SetQuality);
        resolutionDropdown.onValueChanged.AddListener(SetResolution);
    }

    public void SetMasterVolume(float volume)
    {
        gameData.masterVolume = volume;
        AudioListener.volume = volume;
        saveSystem.SaveGame();
    }

    public void SetMusicVolume(float volume)
    {
        // Implement music volume control
        saveSystem.SaveGame();
    }

    public void SetSFXVolume(float volume)
    {
        // Implement SFX volume control
        saveSystem.SaveGame();
    }

    public void ToggleSound(bool enabled)
    {
        gameData.soundEnabled = enabled;
        // Implement sound toggle logic
        saveSystem.SaveGame();
    }

    public void ToggleMusic(bool enabled)
    {
        gameData.musicEnabled = enabled;
        // Implement music toggle logic
        saveSystem.SaveGame();
    }

    public void ToggleFullscreen(bool fullscreen)
    {
        Screen.fullScreen = fullscreen;
    }

    public void SetQuality(int qualityIndex)
    {
        QualitySettings.SetQualityLevel(qualityIndex);
    }

    public void SetResolution(int resolutionIndex)
    {
        Resolution[] resolutions = Screen.resolutions;
        if (resolutionIndex < resolutions.Length)
        {
            Resolution resolution = resolutions[resolutionIndex];
            Screen.SetResolution(resolution.width, resolution.height, Screen.fullScreen);
        }
    }

    void SetupResolutionDropdown()
    {
        resolutionDropdown.ClearOptions();
        List<string> options = new List<string>();

        foreach (Resolution resolution in Screen.resolutions)
        {
            options.Add($"{resolution.width}x{resolution.height}");
        }

        resolutionDropdown.AddOptions(options);
        resolutionDropdown.value = GetCurrentResolutionIndex();
    }

    int GetCurrentResolutionIndex()
    {
        for (int i = 0; i < Screen.resolutions.Length; i++)
        {
            if (Screen.resolutions[i].width == Screen.currentResolution.width &&
                Screen.resolutions[i].height == Screen.currentResolution.height)
            {
                return i;
            }
        }
        return 0;
    }
}

Step 5: Level Complete UI

Level Complete Screen

Create a completion screen that shows results:

public class LevelCompleteUI : MonoBehaviour
{
    [Header("UI Elements")]
    public GameObject levelCompletePanel;
    public Text levelTimeText;
    public Text starsEarnedText;
    public Text newBestTimeText;
    public Button nextLevelButton;
    public Button retryButton;
    public Button mainMenuButton;

    [Header("Star Display")]
    public Image[] starImages;
    public Color starEarnedColor = Color.yellow;
    public Color starEmptyColor = Color.gray;

    private LevelManager levelManager;
    private SaveSystem saveSystem;

    void Start()
    {
        levelManager = FindObjectOfType<LevelManager>();
        saveSystem = SaveSystem.Instance;
        SetupButtons();
    }

    void SetupButtons()
    {
        nextLevelButton.onClick.AddListener(LoadNextLevel);
        retryButton.onClick.AddListener(RetryLevel);
        mainMenuButton.onClick.AddListener(LoadMainMenu);
    }

    public void ShowLevelComplete(int starsEarned, float levelTime, bool newBestTime)
    {
        levelCompletePanel.SetActive(true);

        // Update UI elements
        levelTimeText.text = $"Time: {levelTime:F1}s";
        starsEarnedText.text = $"Stars: {starsEarned}/3";

        if (newBestTime)
        {
            newBestTimeText.text = "NEW BEST TIME!";
            newBestTimeText.color = Color.green;
        }
        else
        {
            newBestTimeText.text = "";
        }

        // Update star display
        for (int i = 0; i < starImages.Length; i++)
        {
            if (i < starsEarned)
            {
                starImages[i].color = starEarnedColor;
            }
            else
            {
                starImages[i].color = starEmptyColor;
            }
        }

        // Show/hide next level button based on progression
        GameData data = saveSystem.GetGameData();
        int currentLevel = SceneManager.GetActiveScene().buildIndex;
        bool hasNextLevel = currentLevel < data.levelsUnlocked.Length;
        nextLevelButton.gameObject.SetActive(hasNextLevel);
    }

    void LoadNextLevel()
    {
        int currentLevel = SceneManager.GetActiveScene().buildIndex;
        SceneManager.LoadScene(currentLevel + 1);
    }

    void RetryLevel()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }

    void LoadMainMenu()
    {
        SceneManager.LoadScene(0); // Assuming main menu is scene 0
    }
}

Pro Tips for Level Progression

Data Persistence Best Practices

Save Frequently:

  • Save after every level completion
  • Save when settings change
  • Save when player makes progress

Error Handling:

  • Always wrap save/load operations in try-catch blocks
  • Provide fallback data if save file is corrupted
  • Test save/load on different platforms

Performance:

  • Don't save every frame
  • Use async operations for large save files
  • Compress save data if needed

Level Design Considerations

Progressive Difficulty:

  • Start with simple mechanics
  • Introduce new challenges gradually
  • Provide optional difficulty levels

Star Requirements:

  • Make 1 star achievable for all players
  • Require 2 stars for basic progression
  • Reserve 3 stars for completionists

Unlock Conditions:

  • Use stars as primary unlock currency
  • Consider time-based unlocks
  • Add special unlock conditions for variety

Troubleshooting Common Issues

Save File Not Loading

Problem: Save data not persisting between sessions Solution: Check file permissions and path validity

void ValidateSavePath()
{
    string path = Application.persistentDataPath;
    if (!Directory.Exists(path))
    {
        Directory.CreateDirectory(path);
    }
}

Level Not Unlocking

Problem: Next level remains locked after completion Solution: Verify unlock logic and save timing

void DebugUnlockLogic()
{
    GameData data = saveSystem.GetGameData();
    Debug.Log($"Current level: {data.currentLevel}");
    Debug.Log($"Levels unlocked: {string.Join(", ", data.levelsUnlocked)}");
}

UI Not Updating

Problem: Level selection UI not reflecting progress Solution: Ensure UI updates after save operations

void RefreshUI()
{
    // Force UI refresh after save
    levelSelectionManager.UpdateUI();
}

Mini Challenge: Complete Progression System

Create a complete level progression system for your 2D platformer:

  1. Implement Save System: Create persistent data storage
  2. Add Level Selection: Build level selection interface
  3. Create Star System: Add collectible stars to levels
  4. Build Completion UI: Show results and progression
  5. Add Settings Menu: Player preferences and options

Success Criteria:

  • Players can unlock levels by earning stars
  • Game progress persists between sessions
  • Level selection shows completion status
  • Settings are saved and loaded properly
  • UI provides clear feedback on progress

What's Next?

In the next lesson, you'll learn about Performance Optimization. You'll implement:

  • Frame rate optimization
  • Memory management
  • Platform-specific optimizations
  • Profiling and debugging tools

Community & Support

Share your progression system in our Discord community:

  • Get feedback on your level design
  • Share screenshots of your level selection
  • Ask questions about save systems
  • Connect with other developers

Key Takeaways

  • Progression is Key: Good progression keeps players engaged
  • Save Everything: Don't lose player progress
  • Clear Feedback: Players need to see their achievements
  • Flexible Design: Allow for different play styles
  • Test Thoroughly: Save/load systems need extensive testing

Ready to create a progression system that keeps players coming back? Your level progression and save system are the foundation of player retention - make them count!


Ready to level up your 2D platformer development skills? Join our Discord community to share your progression systems and get feedback from fellow developers!