Battle Pass #

In this guide, we’ll explore how to use Achievements and Sub Achievements to create a Battle Pass. It has become an extremely common feature in almost all live-service style games where a player is rewarded incrementally for progress they make in the game.

For our example, we will define a parent Achievement, and many Sub Achievements to act as the steps in the Battle Pass. Each will have a dependency on the previous Sub Achievement to create a chain for the player to progress through sequentially.

  1. The pass starts at level 1.

    Initial Pass
    Initial Pass

  2. After levelling up, the pass has progressed!

    Pass After Level Up
    Pass After Level Up

Prerequisites #

To follow this guide you’ll need to:

Server-side #

Let’s start by taking a look at the server-side code that we’ll be using to implement the Battle Pass, beginning with creating the config file where we can define the pass and sub-steps.

main.go #

In our main.go file where Hiro is initialized, we need to make sure to add the required systems.

InitModule function #

1
2
3
4
5
6
7
8
// ...
systems, err := hiro.Init(ctx, logger, nk, initializer, binPath, hiroLicense,
    hiro.WithBaseSystem(fmt.Sprintf("base-system-%s.json", env), true),
    hiro.WithAchievementsSystem(fmt.Sprintf("base-achievements-%s.json", env), true))
if err != nil {
    return err
}
// ...

Hiro system definitions #

Next, we create our Hiro system definitions to implement a Battle Pass in our game. While each registered system needs a config file, you can set up base-system-dev1.json however you like, we only need to focus on achievements for this example in the base-achievements-dev1.json file.

Achievements #

For this example, we create a parent Achievement to act as the Battle Pass itself. We then create multiple Sub Achievements to act as the steps of the Battle Pass. Each step has a precondition_ids array with the previous step’s id. This is what allows us to have the sequential progression through the steps. We also set the max_count of the steps to define how much xp/progress is required to complete that step.

You will also want to assign rewards to these steps based on your game. Maybe some steps reward currencies, while others might reward items.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
    "achievements": {
        "battlePass": {
            "auto_reset": false,
            "category": "pass",
            "count": 0,
            "description": "The Battle Pass.",
            "max_count": 1,
            "name": "Battle Pass",
            "sub_achievements": {
                "step1": {
                    "max_count": 400
                },
                "step2": {
                    "max_count": 400,
                    "precondition_ids": [
                        "step1"
                    ]
                },
                "step3": {
                    "max_count": 400,
                    "reward": {
                        "guaranteed": {
                            "currencies": {
                                "gold": {
                                    "min": 100
                                }
                            }
                        }
                    },
                    "precondition_ids": [
                        "step2"
                    ]
                },
                "step4": {
                    "max_count": 400,
                    "precondition_ids": [
                        "step3"
                    ]
                },
                "step5": {
                    "max_count": 400,
                    "precondition_ids": [
                        "step4"
                    ]
                },
                // ...
            }
        }
    }
}

Client-side #

BattlePassGameCoordinator #

This file bootstraps our game with a list of systems to be used, and provides a list of systems for deterministic start-up. In our case, we’re initializing the Achievements system from Hiro.

1
2
3
4
5
6
7
8
9
// ...
systems.Add(nakamaSystem);

// Add the Achievements system
var achievementsSystem = new AchievementsSystem(logger, nakamaSystem);
systems.Add(achievementsSystem);

return Task.FromResult(systems);
// ...

BattlePassDisplay #

The BattlePassDisplay will update the UI whenever the AchievementsSystem is refreshed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class BattlePassDisplay : MonoBehaviour
{
    [SerializeField] private Slider progressSlider;
    
    private AchievementsSystem _achievementsSystem;
    private IDisposable _achievementsDisposer;

    private IAchievement _battlePass;

    public IEnumerator Start()
    {
        // Get the relevant systems and listen for updates.
        _achievementsSystem = this.GetSystem<AchievementsSystem>();

        _achievementsDisposer = SystemObserver<AchievementsSystem>.Create(_achievementsSystem, OnAchievementsSystemChanged);

        // Wait until achievements are ready to begin.
        yield return new WaitUntil(() => _achievementsSystem.IsInitialized);

        // Refresh the system to get the latest data.
        _ = _achievementsSystem.RefreshAsync();
    }

    public async void LevelUp()
    {
        // Progress the pass by 400xp.
        var subAchievements = _battlePass.SubAchievements.Select(x => x.Key);
        await _achievementsSystem.UpdateAchievementsAsync(subAchievements, 400);
    }

    private void OnAchievementsSystemChanged(AchievementsSystem system)
    {
        // Load the first pass we find as we only have one.
        _battlePass = system.GetAvailableAchievements("pass").FirstOrDefault();

        if (_battlePass == null)
        {
            Debug.LogWarning("No available battle pass.");
            return;
        }

        foreach (var step in _battlePass.SubAchievements)
        {
            // Create UI element to display the step's reward(s).
            Debug.Log(step.Value.Reward);
        }

        // Update the pass slider element to visualize progress.
        var completedSteps = _battlePass.SubAchievements.Count(x => x.Value.Count >= x.Value.MaxCount);
        progressSlider.value = completedSteps;
    }

    private void OnDestroy()
    {
        _achievementsDisposer?.Dispose();
    }
}