Documentation
Features
πŸ›ŽοΈ Services

πŸ›Ž Services

What are Services

  • Services are the core of GameSuite
  • Services allows running code without having to attach components to GameObjects
  • Services are an upgraded form of singletons, with the benefits but without the drawbacks:
    • Services allow access to code and systems from anywhere, just like singletons do
    • Services have a single point of entry and it is clear what is accessible
    • With singletons there is no single access point that is managing all singletons, it is unclear what are actually singletons in the project and it is hard to clean up singletons

Benefits of using services

  • Easy to use
  • Unified accessibility: all services are created and accessed the same way
  • Wait for services to be initialized (e.g. in a loading screen)
  • Support automated lifecycle management
  • Auto execution order management
  • Decoupled
  • Dependency injection of other dependent services
  • All services are reset-able /Β disposable
  • Single responsibility (depending on what you put in them)
  • Platform-specific implementation support
  • Execute from anywhere: code, components, flow, debug menu, etc.

Creating a service

In this chapter, we will use a hypothetical service called GameService. This would for example manage the life cycle of the gameplay, like loading a level, starting the game session and getting the game results.

In essence, a service is a script that is provided by an IServiceFactory<>. Factories allow the developer to return different services for different platforms, AB tests or otherwise.

This code example is the most bare bone service you could create, which is an empty service that can implement your custom logic.

using GameSuite;
 
namespace Game
{
	public class GameService
	{
        // Insert custom methods and variables here.
    }
 
	public class GameServiceFactory : IServiceFactory<GameService>
	{
		public int Order => 0;
        
		public GameService Create()
		{
			return new GameService();
		}
    }
}

The Order int property defines in which order this service should be initialized, check the Initialization order section for details.

⚠️

Factory class location
We do not advocate using multiple classes in a single file. However, when a factory always returns the same service, it is ok to add it to the bottom of the file. In a case of conditional services getting returned, for example: AndroidGameService and iOSGameService, create a new file for the factory class.

Calling a service

Accessing a service is even easier than creating one:

Services.Get<GameService>();

When accessing a Service multiple times in any script, it is advised to locally cache the service. This saves a very small amount of performance. But when not caching a service which gets called hundreds of times every frame, this can become a performance issue.

Do:

private GameService gameService;
 
protected void Awake()
{
	gameService = Services.Get<GameService>();
	gameService.LoadLevel();
	gameService.InitPlayer();
	gameService.StartGame();
}
 
protected void Update()
{
    gameService.Tick();
}

Avoid:

protected void Awake()
{
	Services.Get<GameService>().LoadLevel();
	Services.Get<GameService>().InitPlayer();
	Services.Get<GameService>().StartGame();
}
 
protected void Update()
{
    Services.Get<GameService>().Tick();
}

Shortcuts

Some of the Services have shortcuts as they are used very often. These are just wrappers for calling the actual services themselves.

ShortcutInternal serviceUsage
LibrariesLibraryServiceLibraries.Get<LevelLibrary>();
ConfigsConfigServiceConfigs.Get<GameConfig>();
SaveDataSaveDataServiceSaveData.Get<InventorySaveData>();

Dependency Injection

Use the IServiceFactory<T>.Create() method to pass in any dependencies to other services.

The code sample below injects 3 other services to be able to load a level.

using GameSuite;
 
namespace Game
{
	public class GameService : IDisposable
	{
        private UnityCallbackService unityCallbackService;
        private GameConfig gameConfig;
        
        public GameService(
            UnityCallbackService unityCallbackService,
            ConfigService configService,
            SaveDataService saveDataService)
        {
            this.unityCallbackService = unityCallbackService;
            unityCallbackService.OnUpdate += Update;
            gameConfig = configService.Get<GameConfig>();
            ProgressionSaveData progression = saveDataService.Get<ProgressionSaveData>();
            LoadLevel(progression.LevelIndex);
        }
        
        private void LoadLevel(int index)
        {
            // Load level logic here.
        }
        
        private void Update()
        {
            // Execute game update logic here.
        }
        
        public void Dispose()
        {
            if (unityCallbackService != null)
            {
                unityCallbackService.OnUpdate -= Update;
                unityCallbackService = null;
            }
        }
    }
 
	public class GameServiceFactory : IServiceFactory<GameService>
	{
		public int Order => 0;
        
		public GameService Create()
		{
			return new GameService(
                Services.Get<UnityCallbackService>(),
                Services.Get<ConfigService>(),
                Services.Get<SaveDataService>());
		}
    }
}

The IServiceFactory<T>.Create() method is only called once: when the service is created. To recreate the service through the factory, call Services.Reset<T>() to remove it. Next time the service is accessed, the IServiceFactory<T>.Create() method is called again.

⚠️

Cyclic dependency
Be mindful of cyclic dependency when passing in the dependencies of other services.

Service as an interface

One of the big pros of using services is being able to abstract them as well. Use an IServiceFactory<IGameService> to be able to get an interface service. The factory Create method should then conditionally return the correct game implementation.

public interface IGameService
{
    public string Name { get; }
    public void Start();
    public void Pause();
    public void Resume();
    public void Stop();
    public bool IsFinished();
}
 
public class IGameServiceFactory : IServiceFactory<IGameService>
{
    public int Order => 0;
 
    public IGameService Create()
    {
        return SaveData.Get<GameSaveData>().EasyDifficulty ? 
			   new EasyGameService() : 
			   new NormalGameService();
    }
}

Another version would be to have a selectable dropdown in a Config which allows you to select the gameplay mode and can at runtime be used to create the IGameService:

public class GameConfig : ConfigAsset
{
    public AssignableTo<IGameService> GameType;
}
 
public class IGameServiceFactory : IServiceFactory<IGameService>
{
    public int Order => 0;
 
    public IGameService Create()
    {
        return Activator.CreateInstance(Configs.Get<GameConfig>().GameType);
    }
}

To clear and recreate the IGameService at any time, use the Services.Reset<IGameService>() method. There is a flow action and a ConditionalAction available to reset all or a specific service from an inspector dropdown.

Reset a service safely
In order to be able to clean up the game at any time safely, it is advised to implement IDisposable on the interface so it is enforced through its implementations and will get called automatically when resetting the service.

Switching service interfaces by the user
The ServiceFactory Create method is only called when the service is accessed the first time (unless the service implements IInitializable, then it is created on app startup). If for example the user can pick a different game mode on the fly, have the factory Create return the right implementation.

Initializing a service

Services implementing IInitializable will get created at app launch time automatically. This decouples the project further as there is no longer need to create a new GameObject somewhere in the hierarchy, which would exist just for the purpose of calling a method on a MonoBehaviour.

using GameSuite;
 
namespace Game
{
	public class GameService : IInitializable
	{
Β Β Β Β Β Β Β Β public bool IsInitialized { get; set; }
 
        public void Initialize()
        {
            // Do initialization here.
            IsInitialized = true;
        }
    }
 
	public class GameServiceFactory : IServiceFactory<GameService>
	{
		public int Order => 0;
 
		public GameService Create()
		{
			return new GameService();
		}
	}
}

Initialization order

Every factory has an order of which they will be created if the Services implement IInitializationService, by default the initialization order is 0.

When multiple services use the same order value, their execution order is not guaranteed. But in most scenarios, it will not matter.

These build-in services have the following initialization order:

ServiceInitialization Order
Analytics-100
Addressable-90
Backend-80
ABTesting-70
SaveData-60
Config-50

This allows you to inject your service initialization before or in between the built-in services if required.

Reset Services

All services can be reset with a single call:

Services.ResetAll();

You can also reset individual services like so:

Services.Reset<GameService>();

Ideally, your app should be able to do a clean restart from anywhere in the app cycle without any data persisting from the previous session. In situations where an app update is required, the app has to restart because of new data getting loaded or the game has reconnected to the server, you might want to restart the app from the main menu so all new data gets loaded.

A hard reset can be tested from the Debug menu which resets all services, unloads all GameObjects and scenes and tries to run a fresh start.

Disposing Services

When requiring logic to be executed when the service is destroyed, implement IDisposable and clean up any subscribed callbacks and other actions there.

using GameSuite;
 
namespace Game
{
	public class GameService : IDisposable
	{
        public void Dispose()
        {
            // Disposing variables and unsubscribe to callbacks here.
        }
    }
 
	public class GameServiceFactory : IServiceFactory<GameService>
	{
		public int Order => 0;
 
		public GameService Create()
		{
			return new GameService>();
		}
	}
}

Always check if cached services and other variables were not already disposed of by doing null checks in the Dispose() method.

Do not use the default c# destructors in Unity as they are unreliable. In c# this would be noted as ~ClassName.

Services as a MonoBehaviour

It is also possible to make a service inherit from a MonoBehaviour and have all the functionality as a Unity component.

using GameSuite;
 
namespace Game
{
	public class GameService : MonoBehaviour, IInitializationService, IDisposable
	{
Β Β Β Β Β Β Β Β public bool IsInitialized => true;
 
        public void Dispose()
        {
             GameObject.Destroy(gameObject);
        }
    }
 
	public class GameServiceFactory : IServiceFactory<GameService>
	{
		public int Order => 0;
 
		public GameService Create()
		{
			return new GameObject(β€œGameService”).AddComponent<GameService>();
		}
	}
}
⚠️

Avoid MonoBehaviours where possible
Even though it is possible to have Services as MonoBehaviours, this is almost never required: Use the UnityCallbackService for most Unity-specific callback behaviours such as updates, ApplicationPause, delayed calls, or running and stopping coroutines. Use a regular service to executer custom code. Try to detach from the classical way of working with Unity: everything is a MonoBehaviour.

Static code
When calling state-independent code, where the previous state does not need to be stored, just use static classes and methods without static variables. When using state dependent code: use services.

Using Services from GameObjects

Use the CallServiceMethod component to execute any Service method from a GameObject.

This can be done using a default Unity trigger, such as Awake, OnEnable, OnDestroy. It is also possible to execute manually with any UnityEvent or through custom code. This allows for example a button press to call any service method.

Any created service and methods will automatically show up in these drop-downs:


Call Service Method

Use the ServicesInitializedEvent component to trigger something once all the services that implement IInitializationService have been initialized.

This could for example be useful to show a visual queue when loading is completed.


Services Initialized Event

Services in the Flow

Flow Conditions

The ServiceInitialized condition can be added to a connection between nodes to check if all services are initialized or a specific service is initialized.

This can also be used to enforce certain execution of services in a row, aside from the build-in factory creation order before jumping to the next node.


Service initialized condition

Flow Actions

On nodes, it is also possible to start the Initialization process of services by adding the InitializeServices action:


Service initialized task

You can even call any Service methods from a node, either when the node starts or ends:


Service invoke task

Conditional Actions

Call any service method from Conditional Actions as well, this is not to be confused with Flow Conditions and Flow Actions as they are separate systems.

Debug Menu


All Services are automatically available to browse through from the debug menu as well. Open the debug menu: \[Game State\] > \[Services\]

Use the Debug menu to:

  • View any public and private variables
  • Call any public and private methods

Service debug menu
Service debug menu details