Skip to content

Loading and Load Phases

When loading a game there are many steps that have to run in a particular order. The Loader and LoadPhaseBase classes are meant to help you define and control those steps to make loading your game easier.

Loader in IdleKit

The Loader component in IdleKit is used to run a sequence of ILoadPhase operations; it also includes the ability to revert them, cancel their execution, and to report on the progress of their completion. The Loader also allows for retrieving of specific ILoadPhase objects from its collection.

Loader in action

Common use of the Loader class is for game start up; an example can be found inside the StartGame method of the Startup class. From this method you will see calls to other methods, like SetInitializationLoadPhases, which enqueue a sequence of load phases.

public virtual void StartGame()
{
    Context.Bind();

    ResetLoader();

    SetInitializationLoadPhases();
    SetLoadContentLoadPhases();

    Loader.Start();
}
protected virtual void SetInitializationLoadPhases()
{
    Loader.Enqueue(SequenceFlow.InSequence,
        Context.Container.Resolve<InitializeAddressablesPhase>(),
        Context.Container.Resolve<LoadAssetsPhase>().Initialize(CoreConstants.PRELOAD_ASSET_LABEL),
        Context.Container.Resolve<LoadDataPhase>().Initialize(CoreConstants.CONFIG_ASSET_LABEL),
        Context.Container.Resolve<InitializeServicesPhase>().Initialize(GetServiceTypes()),
        Context.Container.Resolve<InitializeUserLoadPhase>(),
        GetLoadGlobalDataLoadPhase()
    );
}

Load Phases

Load phases are the building blocks of the Loader. They can be used for any type of work you want to do in a sequence, but you will most often see them used to control the start up sequence of an IdleKit game. For example, a load phases exists to load user saved data - LoadContentLoadPhase. All load phases inherit from the LoadPhaseBase class to avoid repetition while implementing the ILoadPhase interface.

Load phases implement the template method pattern to encapsulate common functionality via the methods Start, Complete, Revert, and Cancel. The Start method is called for you by the Loader, and the remaining three methods should be called by you to advance the load phase to one of its ending states.

Some of these states you may not use often, for example, you may wish to allow the player to cancel a sequence of load phases that downloads additional data. In this case you would have your interface trigger the Cancel method. The Revert method is a little different; it should ensure your game is returned to a previously well-known state. You can react to revert events by using the Loader RevertTo method to roll back to a previous point in the loading sequence and optionally retry.

Load phases have four abstract methods which correspond to each of the template methods above, which allow you to implement custom functionality at each state of the load phase. These abstract methods are RunStartLogic, RunCompleteLogic, RunRevertLogic, and RunCancelLogic. You should implement these methods in any load phase you create, but note that throwing an exception in these methods may cause the Loader to halt. Likewise, if you do not call one of Complete, Cancel, or Revert, the Loader will never be notified that your load phase is done and it will halt.

Example: Creating a Load Phase

As an example, you could create a load phase to retrieve a set of player-specific, randomized tips for your game at start up, and if that fails default to some generic tips. The requirements for this load phase will be as follows:

  • Starting the load phase should initiate the request to obtain game tips.
  • Completing the load phase should store the game tips so they can be used later.
  • Cancelling the load phase should stop the game tips request.
  • Reverting the load phase should store the default, generic tips.

A few assumptions will also be made within this example:

  • There is a bound service for your backend system of a type GameBackend.
  • There is a saved data class called StaticTipsSavedData which can load generic tips.

Every load phase implementation begins with a class inheriting from LoadPhaseBase and implementing the required abstract methods.

public class GameTipsLoadPhase : LoadPhaseBase
{
    protected override void RunStartLogic()
    {
    }

    protected override void RunCancelLogic()
    {
    }

    protected override void RunRevertLogic()
    {
    }

    protected override void RunCompleteLogic()
    {
    }
}

To make this example realistic, let's assume that your GameBackend service has a method RandomizedTips(CancellationToken) and that it requests data over the network. If this request fails we should Revert the load phase, otherwise we should Complete it. Implementing this should be done within the RunStartLogic method.

private CancellationTokenSource _tokenSource;
private List<string> _tips;

protected override async void RunStartLogic()
{
    GameBackend backend = _resolver.Resolve<GameBackend>();
    _tokenSource = new CancellationTokenSource();

    try
    {
        _tips = await backend.RandomizedTips(_tokenSource.Token);
        Complete();
    }
    catch (Exception e)
    {
        Revert();
    }
}

The Revert and Complete logic share a common goal of binding tip data, so that can be factored out into a small private method called BindGameTips. The RunRevertLogic method should implement obtaining and binding the static tip data, whereas the RunCompleteLogic method can focus on binding the tip data obtained by the RunStartLogic method.

protected override void RunRevertLogic()
{
    List<string> staticTips = _resolver.Resolve<StaticTipsSavedData>().Load();
    BindGameTips(staticTips);
}

protected override void RunCompleteLogic()
{
    BindGameTips(_tips);
}

private void BindGameTips(List<string> tips)
{
    IBinder binder = _resolver.Resolve<IBinder>();
    binder.Bind<List<string>>().ToInstance(tips).ToId("game-tips").AsCached().Conclude();
}

The Cancel method is generally an external call, but it is also called by the Revert method. A good rule is if you need to revert you also probably need to cancel. In this example we can use the _tokenSource to cancel the asynchronous method call.

protected override void RunCancelLogic()
{
    _tokenSource.Cancel();
}

Viewing these implementations all together, the GameTipsLoadPhase looks like this:

public class GameTipsLoadPhase : LoadPhaseBase
{
    private CancellationTokenSource _tokenSource;
    private List<string> _tips;

    protected override async void RunStartLogic()
    {
        YourGameBackend backend = _resolver.Resolve<YourGameBackend>();
        _tokenSource = new CancellationTokenSource();

        try
        {
            _tips = await backend.RandomizedTips(_tokenSource.Token);
            Complete();
        }
        catch (Exception e)
        {
            Revert();
        }
    }

    protected override void RunCancelLogic()
    {
        _tokenSource.Cancel();
    }

    protected override void RunRevertLogic()
    {
        List<string> staticTips = _resolver.Resolve<StaticTipsSavedData>().Load();
        BindGameTips(staticTips);
    }

    protected override void RunCompleteLogic()
    {
        BindGameTips(_tips);
    }

    private void BindGameTips(List<string> tips)
    {
        IBinder binder = _resolver.Resolve<IBinder>();
        binder.Bind<List<string>>().ToInstance(tips).ToId("game-tips").Conclude();
    }
}

Binding and Enqueuing the Load Phase

To have GameTipsLoadPhase run as part of the initialization process it must be bound to the Container and then enqueued in a loader. The binding statement can go within your IInstaller implementation class.

public override void BindLoadPhases(IContainer container)
{
    ...

    container.Bind<GameTipsLoadPhase>().AsSingleton().Conclude();
}

The enqueueing statements using Loader are normally with your IStartup implementation class.

protected override void SetInitializationLoadPhases()
{
    Loader.Enqueue(SequenceFlow.InSequence,
        ...,
        Context.Container.Resolve<GameTipsLoadPhase>()
    );
}

That's it! The GameTipsLoadPhase will now run as part of your start up process.

Implementation Details

Loader

This object is built on top of the more general SequencerBase class, but restricts the type of sequence to ILoadPhase in order to conform to the ILoader interface. The ILoadPhase acts as a marker for the ISequenceable interface which is used to control the execution of the entire sequence.

SequencerBase

The Loader class is based on the underlying base class SequencerBase, which comprises most of the functionality responsible for starting/stopping the ISequenceables inside of the Loader, with the Loader class adding ways to track the progress of steps within the given SequencerBase.

SequencerBase supports sequential running of ISequenceable; there is an experimental parallel loading flow which is accessed by using SequenceFlow.InParallel when enqueing, but it is not used within IdleKit itself.

The most basic access point inside the SequencerBase is the Enqueue(SequenceFlow flow, params TSequenceable[] sequenceables) method which should be used to build a sequence of operations inside the SequencerBase, before the SequencerBase is started with its Start method. The method accepts a flow parameter, which determines whether the passed in sequenceables will be executed in sequence or at the same time.

Please note that the parallel execution inside of the ISequenceables inside the SequencerBase does not mean the ISequenceables are executed on different threads. Instead all the parallel ISequenceables inside the are put into a IParallelSequenceCollection and started at the same time.