Adding achievements and leaderboards is one of the easiest ways to make a finished prototype feel like a real game. Done well, they give players short-term goals, long-term mastery targets, and a social reason to keep playing without forcing you to redesign your entire core loop.

In this guide you will:

  • Design a simple achievement model that fits almost any Unity project.
  • Implement achievement tracking that is decoupled from your gameplay code.
  • Wire a basic leaderboard using score events and a pluggable backend.
  • Learn how to integrate with platform services later (Steam, Google Play, Apple Game Center) without rewriting everything.

This tutorial assumes you are comfortable with C# scripts, Scenes, and prefabs in Unity.

Step 1: Decide what achievements are actually for

Before you add popups and badges, decide what behavior you want to encourage.

  • Onboarding: “Complete the tutorial”, “Finish your first run”, “Beat your first boss”.
  • Skill growth: “Clear a level without taking damage”, “Win three games in a row”.
  • Exploration: “Find a hidden area”, “Use every weapon at least once”.
  • Retention: “Play on 3 different days”, “Reach level 20”, “Defeat 500 enemies”.

Write down 5–10 achievements that:

  • Connect directly to your core loop (not random side activities).
  • Scale from easy wins to serious mastery.
  • Are observable in code (you can tell when they happen).

If you struggle to define them, your game’s goals may be unclear—fixing that comes before any UI work.

Step 2: Create a reusable achievement data model

You do not want achievements scattered across random scripts. Instead, use a small data model that your systems can refer to in a consistent way.

In Unity, one clean option is a ScriptableObject:

using UnityEngine;

[CreateAssetMenu(menuName = "Progress/Achievement")]
public class AchievementDefinition : ScriptableObject
{
    public string id;              // e.g. "complete_tutorial"
    public string displayName;     // e.g. "First Steps"
    public string description;     // e.g. "Finish the tutorial."
    public int points;             // optional, for meta score
    public Sprite icon;            // UI icon
}

Then create a collection to hold all achievements:

using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(menuName = "Progress/Achievement Collection")]
public class AchievementCollection : ScriptableObject
{
    public List<AchievementDefinition> achievements;

    public AchievementDefinition GetById(string id)
    {
        return achievements.Find(a => a.id == id);
    }
}

Now your game can look up achievements by id rather than hard-coding text in scripts.

Step 3: Implement an achievement manager with events

Instead of making every script know about achievements, create a central AchievementManager that listens for events like “tutorial completed” or “enemy defeated”.

A simple pattern:

using UnityEngine;
using System;
using System.Collections.Generic;

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

    [SerializeField] private AchievementCollection collection;

    private readonly HashSet<string> unlockedIds = new HashSet<string>();

    public event Action<AchievementDefinition> OnAchievementUnlocked;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);

        LoadProgress();
    }

    public bool IsUnlocked(string id) => unlockedIds.Contains(id);

    public void Unlock(string id)
    {
        if (unlockedIds.Contains(id)) return;

        var definition = collection.GetById(id);
        if (definition == null)
        {
            Debug.LogWarning($"Missing achievement definition for id: {id}");
            return;
        }

        unlockedIds.Add(id);
        SaveProgress();

        OnAchievementUnlocked?.Invoke(definition);
        Debug.Log($"Unlocked achievement: {definition.displayName}");
    }

    private void LoadProgress()
    {
        // For a real project, use JSON or a save system.
        // For now, use PlayerPrefs as a simple demo.
        var saved = PlayerPrefs.GetString("achievements", string.Empty);
        if (string.IsNullOrEmpty(saved)) return;

        foreach (var id in saved.Split(','))
        {
            if (!string.IsNullOrWhiteSpace(id))
                unlockedIds.Add(id);
        }
    }

    private void SaveProgress()
    {
        var joined = string.Join(",", unlockedIds);
        PlayerPrefs.SetString("achievements", joined);
        PlayerPrefs.Save();
    }
}

Now gameplay code can simply say AchievementManager.Instance.Unlock("complete_tutorial"); The manager handles persistence and UI notifications.

Step 4: Hook achievements into real gameplay events

To avoid tight coupling, send semantic events from your systems. Here are a few examples:

  • Tutorial scene: when the last step is done, call Unlock("complete_tutorial").
  • Kill counter: when total enemies killed hits 100, unlock kill_100_enemies.
  • Session tracking: when player finishes a daily run, update a streak and unlock streak-based achievements.

Prefer small, focused scripts that bridge gameplay to the manager, such as:

public class TutorialCompletionTracker : MonoBehaviour
{
    public void OnTutorialFinished()
    {
        AchievementManager.Instance.Unlock("complete_tutorial");
    }
}

This keeps your main systems readable while still feeding progress into achievements.

Step 5: Design and implement a simple leaderboard

Leaderboards answer a different question: “How do I compare to other players?”

Start with a minimal model:

[Serializable]
public class LeaderboardEntry
{
    public string playerName;
    public int score;
}

In a production game you will likely use:

  • A platform API (Steam, PlayFab, Epic Online Services, Apple, Google).
  • A custom backend (serverless functions plus a database).

For a prototype or offline game, you can start with local leaderboards stored via JSON.

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class LocalLeaderboard : MonoBehaviour
{
    [SerializeField] private int maxEntries = 10;

    public List<LeaderboardEntry> Entries { get; private set; } = new List<LeaderboardEntry>();

    private void Awake()
    {
        Load();
    }

    public void SubmitScore(string playerName, int score)
    {
        Entries.Add(new LeaderboardEntry { playerName = playerName, score = score });
        Entries = Entries
            .OrderByDescending(e => e.score)
            .Take(maxEntries)
            .ToList();

        Save();
    }

    private void Load()
    {
        var json = PlayerPrefs.GetString("leaderboard", string.Empty);
        if (string.IsNullOrEmpty(json)) return;

        var wrapper = JsonUtility.FromJson<LeaderboardWrapper>(json);
        if (wrapper != null && wrapper.entries != null)
            Entries = wrapper.entries;
    }

    private void Save()
    {
        var wrapper = new LeaderboardWrapper { entries = Entries };
        var json = JsonUtility.ToJson(wrapper);
        PlayerPrefs.SetString("leaderboard", json);
        PlayerPrefs.Save();
    }

    [System.Serializable]
    private class LeaderboardWrapper
    {
        public List<LeaderboardEntry> entries;
    }
}

In your game over or victory screen:

  • Ask for the player name (or use a profile).
  • Submit the score to LocalLeaderboard.
  • Refresh a simple UI list bound to Entries.

Later, you can swap the internals of SubmitScore and Load/Save to call a real API without changing your UI.

Step 6: Integrate with platform services (optional)

If you plan to ship on Steam, mobile, or consoles, achievements and leaderboards often come from:

  • Steamworks on PC.
  • Google Play Games Services on Android.
  • Apple Game Center on iOS and macOS.

The pattern is:

  • Keep your local achievement and score model as your source of truth.
  • Add thin adapters that:
    • Map local ids to platform IDs.
    • Mirror unlock and score events to the platform SDK.

This way:

  • Your game logic remains testable without network access.
  • You can still provide offline achievements/leaderboards for players who never sign in.

Step 7: UX and player feedback

Well-designed achievements and leaderboards are as much UX as they are code.

  • Show a small, non-blocking toast when an achievement unlocks.
  • Provide a dedicated achievements screen where players can browse what they have and what is left.
  • On leaderboards, allow filters like “friends only” or “daily / weekly / all-time” once you move to an online service.
  • Avoid cluttering the screen with notifications during critical gameplay moments (boss fights, cutscenes).

Ask in playtests:

  • Do players understand why they got each achievement?
  • Do leaderboards motivate them, or make them feel discouraged?
  • Are there hidden achievements that feel unfair rather than fun to discover?

Mini implementation checklist

  • [ ] Define 5–10 achievements that reinforce your core loop.
  • [ ] Create AchievementDefinition and AchievementCollection assets.
  • [ ] Implement AchievementManager with save/load and an OnAchievementUnlocked event.
  • [ ] Wire at least 3 achievements into real gameplay events.
  • [ ] Implement a basic local leaderboard and hook it into your game over flow.
  • [ ] Add a minimal UI for viewing achievements and scores.

Once this backbone is in place, adding new achievements or alternative leaderboard backends becomes a content task instead of a refactor.

Where to go next

  • If you want deeper monetization and live-ops systems, pair this post with a business-focused guide or course on live service design.
  • If you are shipping on Steam or consoles, explore their official SDK docs for platform achievements and cloud-backed leaderboards.
  • For now, get a small set of achievements and a simple leaderboard working in one scene—then reuse the same architecture across the rest of your project.