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:
- Implement Save System: Create persistent data storage
- Add Level Selection: Build level selection interface
- Create Star System: Add collectible stars to levels
- Build Completion UI: Show results and progression
- 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!