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.