It’s been a while since I’ve posted (sorry!) discussing startup flow / app structure in the past. I’ve been reworking Dystopia Punk (actually reset the whole project recently to clean things up) some more and wanted to cover some basic architecture / flow topics in more detail.
Apologies (or not), but this one’s gonna have a shit ton of code in it for better or worse. Don’t say I didn’t warn you!
Startup Flow in more Detail
Previously I’ve discussed project structure, initialization, MVP type patterns and other topics. Let’s put some of these to practice and walk through how Dystopia Punk launches and goes from startup splash screen to the main menu!
I briefly touched upon the service locator pattern previously, but let’s look at what that actually means in practice. Here’s a UPM package that I maintain that implements the Service Locator pattern on github!
The Service Manager
I have exactly ONE Singleton in my project, and that’s the Service Manager, implemented via this package on GitHub. Every other persistent class (things that I want to access throughout the lifecycle of the game) is managed via the Service Locator. I’ll call these classes ‘Services’ going forward.
Implementing the ServiceManager can be as simple or complex as you would like. First it’s a singleton, so I register the class like so:
public class ServiceManager : Singleton<ServiceManager>
{
}
Then I add an event to indicate when all of the Services are initialized
public static event Action OnServicesInitialized;
I can call this and notify any other class that has subscribed like so:
OnServicesInitialized?.Invoke();
Adding Services via the Service Locator
Within the ServiceManager class, I can add services easily like so:
var go = gameObject;
var auth = go.GetOrAddComponent<AuthenticationService>();
ServiceLocator.Add(auth);
C# Extensions
Quick sidebar: If you are paying attention, you probably noticed the ‘GetOrAddComponent’ method above that I call directly on the Service Manager Game Object. This is a very simple extension that I use all over the place instead of doing something like this:
var comp = GetComponent<MyComponent>();
if(comp == null)
{
comp = AddComponent<MyComponent>();
}
DoStuff();
I have this and a few other extensions as a UPM package here on GitHub.
Let’s take a look at what our ServiceManager class looks like at this point:
using UnityEngine;
using PixelWizards.Shared.Extensions;
using PixelWizards.Shared.Base;
public class ServiceManager : Singleton<ServiceManager>
{
public static event Action OnServicesInitialized;
private void Start()
{
// register our services
var go = gameObject;
// add our steam service
var steam = go.GetOrAddComponent<SteamService>();
ServiceLocator.Add(steam);
// and our auth service
var auth = go.GetOrAddComponent<AuthenticationService>();
ServiceLocator.Add(auth);
// let everyone know that we're done
OnServicesInitialized?.Invoke();
}
}
Fairly straightforward, right?
The above would work perfectly fine if none of our services have any dependencies on startup, however, Dystopia Punk has quite a few:
We need to:
Wait until Steam initializes
Create a steam login ticket
Use that ticket to log into Playfab
Finally once we’re logged into Playfab, we finally log in and connect to Photon.
Then and ONLY then initialize the rest of our services that depend on Playfab to retrieve player profile, inventory etc.
Finally we can call our ‘OnServicesInitialized event to let everyone else know we’re done
To complicate things further, while all of that is happening, we want to display a splash image, then display some splash videos (game logo / company logo) and finally if we’re still waiting for the above list, then finally display a ‘loading screen’ and either load directly to the main menu OR do the typical ‘press space to continue’ type of splash screen.
So … how do we do this?
Steam Authentication
I’m using the Heathen Steamworks library (which in turn is a wrapper for the Steamworks.NET package). I can’t recommend the Heathen package enough - also their team is fantastic and provide incredible support via their Discord as well.
Personally, I sponsor them on Github (and have for a while), but you can also grab it on the Asset Store if you prefer that route. I like the GitHub sponsor route as it grants me access to their GitHub directly (they update the package frequently) and is also packaged as a UPM package (which as you may be able to tell) I much prefer versus baking the code / package into my project.
Anyways, their package provides a wide array of ways to initialize Steam, but I prefer the simplest, which is to use their built in SteamworksBehaviour component, which looks like this:
I just attach this to the same component that I have the ServiceManager on and call it a day!
Steam Initialization : SteamService
The Authentication Service is the core of the login / auth process, but I have created a wrapper for Steam functionality simply called ‘SteamService’ - this way I compartmentalize all of the steam functionality (or attempt to anyways) and am not directly calling ‘Steam.DOSTUFF’ throughout my code.
Why? Fast forward to the time when you want to port your game to Xbox, or Switch or…even Epic Game Store - you need a way to abstract away things like platform-specific code in a way that makes it easy(er) to port in the future.
Let’s take a bit more detailed look at the SteamService, as it shows us how to create new Services (which are reference in the ServiceManager) and also shows us how we’ll start to handle the async startup flow that I described above.
The nice part about the ServiceLocator package that I use is that a service can be ‘just’ a simple Monobehaviour, like any Unity component (you can also have POCO services, but I typically create them as MonoBehaviour for simplicity).
Let’s create our SteamService:
using UnityEngine;
using PixelWizards.Shared.Base; // reference to ServiceLocator
public class SteamService : MonoBehaviour
{
public static SteamService Instance
{
get { return ServiceLocator.Get<SteamService>(); }
}
}
That was hard, right? The static Instance property allows us to reference the service anywhere like so:
SteamService.Instance.DoSomething();
Now we want to create an event that we can fire when Steam completes initializing.
/// <summary>
/// Steam has been initialized
/// </summary>
public static event Action onSteamInitialized;
Now…how do we connect our SteamService to the SteamworksBehaviour component that we mentioned above?
One way is to directly add the SteamService component into our scene (maybe on the same GameObject as the SteamworksBehaviour component and manually add a method via the Evt Steam Initialized UI, but…
I much prefer code that automagically hooks itself up and doesn’t require me to remember what I need to hook up in the Unity Editor.
So let’s add an Awake() method to our SteamService and get a reference to the SteamworkBehaviour component, like so:
using System; // needed for the Action event
using UnityEngine;
using PixelWizards.Shared.Base; // reference to ServiceLocator
public class SteamService : MonoBehaviour
{
public static SteamService Instance
{
get { return ServiceLocator.Get<SteamService>(); }
}
/// <summary>
/// Steam has been initialized
/// </summary>
public static event Action onSteamInitialized;
private SteamworksBehaviour steamBehaviour;
private void Awake()
{
}
}
In the Awake() method, we can get a reference to the SteamworksBehaviour and connect the event listeners via code, like so:
using System;
using UnityEngine;
using PixelWizards.Shared.Base;
using PixelWizards.Shared.Extensions; // so we can use GetOrAddComponent
public class SteamService : MonoBehaviour
{
public static SteamService Instance
{
get { return ServiceLocator.Get<SteamService>(); }
}
/// <summary>
/// Steam has been initialized
/// </summary>
public static event Action onSteamInitialized;
private SteamworksBehaviour steamBehaviour;
private void Awake()
{
steamBehaviour = gameObject.GetOrAddComponent<SteamworksBehaviour>();
steamBehaviour.evtSteamInitialized.AddListener(OnSteamInitialized);
}
}
The last line adds a listener to the evtSteamInitialized event and calls a ‘OnSteamInitialized()’ method…so we better add it!
using System;
using UnityEngine;
using PixelWizards.Shared.Base;
using PixelWizards.Shared.Extensions;
public class SteamService : MonoBehaviour
{
public static SteamService Instance
{
get { return ServiceLocator.Get<SteamService>(); }
}
/// <summary>
/// Steam has been initialized
/// </summary>
public static event Action onSteamInitialized;
private SteamworksBehaviour steamBehaviour;
private void Awake()
{
steamBehaviour = gameObject.GetOrAddComponent<SteamworksBehaviour>();
steamBehaviour.evtSteamInitialized.AddListener(OnSteamInitialized);
}
/// <summary>
/// Callback from steamworks when Steam is initialized
/// </summary>
public void OnSteamInitialized()
{
// fire our own event to let folks know that Steam is initialized
onSteamInitialized?.Invoke();
}
}
If you check the SteamworksBehaviour component you’ll see there’s also an event if there are issues initializing Steam, so let’s add that as well and add another event so we can handle it.
using System;
using UnityEngine;
using PixelWizards.Shared.Base;
using PixelWizards.Shared.Extensions;
public class SteamService : MonoBehaviour
{
public static SteamService Instance
{
get { return ServiceLocator.Get<SteamService>(); }
}
/// <summary>
/// Steam has been initialized
/// </summary>
public static event Action onSteamInitialized;
/// <summary>
/// There was an error initializing Steam
/// </summary>
public static event Action<string> onSteamInitError;
private SteamworksBehaviour steamBehaviour;
private void Awake()
{
steamBehaviour = gameObject.GetOrAddComponent<SteamworksBehaviour>();
steamBehaviour.evtSteamInitialized
.AddListener(OnSteamInitialized);
steamBehaviour.evtSteamInitializationError
.AddListener(OnSteamInitError);
}
/// <summary>
/// Callback from steamworks when Steam is initialized
/// </summary>
public void OnSteamInitialized()
{
// fire our own event to let folks know that Steam is initialized
onSteamInitialized?.Invoke();
}
private void OnSteamInitError(string msg)
{
Debug.Log("Steam was unable to initialized? Error: " + msg);
// TODO: show error message, prompt to exit
onSteamInitError?.Invoke(msg);
}
}
As development progresses there will be a ton of additional functionality that will be added to the SteamService, but this is enough to get us started!
Now we just need to be able to listen for this onSteamInitialized event somewhere so we can continue the initialization process!
AuthenticationService
Creating the AuthenticationService is the same as the SteamService:
public class AuthenticationService : MonoBehaviour
{
public static AuthenticationService Instance
{
get { return ServiceLocator.Get<AuthenticationService>(); }
}
}
The primary entry point for the Auth service is actually going to be that onSteamInitialized event. Once Steam is initialized, we can grab a ticket and do the rest of the login flow!
I recommend adding handles for any event that you might want to listen to in the objects Awake() method. If you add them later on (even in Start for example), there is no guarantee that the event hasn’t been fired already and you may have missed it!
The Awake method for the Auth Service looks like this:
private void Awake()
{
SteamService.onSteamInitialized += DoAuthFlow;
}
and there’s of course a corresponding DoAuthFlow method like so:
/// <summary>
/// This is a callback for the Steam initialization
/// </summary>
public void DoAuthFlow()
{
// Steam is ready we can login to all the things now!
}
Finally once the login flow has been completed, the AuthenticationService calls an event onUserAuthenticated, like so:
public static event Action onUserAuthenticated;
That we’ll call in DoAuthFlow, like so:
/// <summary>
/// This is a callback for the Steam initialization
/// </summary>
public void DoAuthFlow()
{
// Steam is ready we can login to all the things now!
onUserAuthenticated?.Invoke();
}
I’m not going to detail the entire login flow in this post, if you really want me to go into more detail, add a comment or ping me on Twitter / Mastodon etc and I’ll revisit things!
Now if you recall our ServiceManager implementation above, we simply had a Start() method that added all of the services and then called ‘OnServicesInitialized’ without waiting. Since I want to wait until the entire auth process (and any other service that we might be waiting for) is complete, we need to revise this startup flow somewhat.
Let’s revise our original ServiceManager and add a handler for onUserAuthenticated
using UnityEngine;
using PixelWizards.Shared.Extensions;
using PixelWizards.Shared.Base;
public class ServiceManager : Singleton<ServiceManager>
{
public static event Action OnServicesInitialized;
private void Start()
{
// register callbacks
AuthenticationService.onUserAuthenticated += OnUserAuthenticated;
// register our services
var go = gameObject;
// add our steam service
var steam = go.GetOrAddComponent<SteamService>();
ServiceLocator.Add(steam);
// and our auth service
var auth = go.GetOrAddComponent<AuthenticationService>();
ServiceLocator.Add(auth);
// let everyone know that we're done
OnServicesInitialized?.Invoke();
}
// callback from AuthService.
private void OnUserAuthenticated()
{
Debug.Log("User Authenticated!");
}
}
Note that even though I ‘just’ told you to add callbacks in Awake, in this case it’s fine to have it in Start because A) we won’t authenticated instantly, and B) the AuthenticationService is actually added to the game object below this, so there’s literally no way for it to be called before.
Know the rules and then break them ;P
So how do we wait until OnUserAuthenticated is called before calling OnServicesInitialized?
The easy way would be to simply call the event in the callback from the AuthService, like so:
// callback from AuthService.
private void OnUserAuthenticated()
{
Debug.Log("User Authenticated!");
// fire the event!
OnServicesInitialized?.Invoke();
}
Except that we have more than one thing that we want to listen for? Also once the authentication is complete, we actually want to initialize a bunch of other services to do ‘their’ thing on startup etc.
Basically we want a bunch of flags that we can set and ‘wait’ until they are set in sequence.
First, instead of simply calling a Start() method and ending, let’s start a Coroutine instead:
private void Start()
{
StartCoroutine(DoGameStartup());
}
Having a coroutine allows us to wait until certain conditions are completed and then continue.
Here’s a simplified version of DoGameStartup() method from DystopiaPunk:
/// <summary>
/// handles startup, waits for auth and splashscreens before finishing
/// </summary>
private IEnumerator DoGameStartup()
{
// register services
ProcessGameStartup();
// wait for auth
while (!_userAuthenticated)
{
yield return new WaitForEndOfFrame();
}
// wait for our splash screens if we finished too fast
while (!_splashCompleted)
{
yield return new WaitForEndOfFrame();
}
// tell everyone that we are ready to roll
OnServicesInitialized?.Invoke();
}
Let’s break it down a bit.
The first method ProcessGameStartup() registers our services, basically does what we had in our Start() method originally.
The second is the first major loop that we run.
while (!_userAuthenticated )
{
yield return new WaitForEndOfFrame();
}
This _userAuthenticated variable is simply a bool that we set once the auth flow is completed.
// callback from AuthenticationService
private void OnUserAuthenticated()
{
_userAuthenticated = true;
}
Next, if check if the splash screens are still running (there are a couple of video startup splash screens that take a second or so each)
// wait for our splash screens if we finished too fast
while (!_splashCompleted)
{
yield return new WaitForEndOfFrame();
}
The _splashCompleted variable is a boolean that we set on a callback from the SplashScreenRunner class. The splash screens themselves are initialized in ProcessGameStartup method and run independently.
Registering the callback on Start() in ServiceManager
SplashScreenRunner.OnSplashScreensCompleted += OnSplashScreensCompleted;
And then handling the callback to set the flag:
private void OnSplashScreensCompleted()
{
_splashCompleted = true;
}
Once _splashCompleted is set, and only then, do we fire the actual ServicesInitialized event:
OnServicesInitialized?.Invoke();
A simplified version of ServiceManager with the above structure integrated looks like this - what I have in DystopiaPunk is somewhat more complex than this, but for the sake of understanding the ramblings above here it is:
using UnityEngine;
using PixelWizards.Shared.Extensions;
using PixelWizards.Shared.Base;
public class ServiceManager : Singleton<ServiceManager>
{
public static event Action OnServicesInitialized;
private bool _userAuthenticated = false;
private bool _splashCompleted = false;
private void Start()
{
// register callbacks
AuthenticationService.onUserAuthenticated += OnUserAuthenticated;
SplashScreenRunner.OnSplashScreensCompleted += OnSplashScreensCompleted;
StartCoroutine(DoGameStartup())
}
// callback from AuthenticationService
private void OnUserAuthenticated()
{
_userAuthenticated = true;
}
// callback from SplashScreenRunner
private void OnSplashScreensCompleted()
{
_splashCompleted = true;
}
private IEnumerator DoGameStartup()
{
// register our services etc
ProcessGameStartup();
// wait for the user auth before continuing
while (!_userAuthenticated)
{
yield return new WaitForEndOfFrame();
}
// wait for our splash screens if we finished too fast
while (!_splashCompleted)
{
yield return new WaitForEndOfFrame();
}
// tell everyone that we are ready to roll
OnServicesInitialized?.Invoke();
}
// register our services, do other stuff on startup
private void ProcessGameStartup()
{
// register our services
var go = gameObject;
// add our steam service
var steam = go.GetOrAddComponent<SteamService>();
ServiceLocator.Add(steam);
// and our auth service
var auth = go.GetOrAddComponent<AuthenticationService>();
ServiceLocator.Add(auth);
}
}
Hopefully this made sense and gave you some ideas about how you can architect your Unity game with independent services that can very easily communicate together via Actions!
If you have any questions, want to yell at me for my terrible code or otherwise, please get in touch (leave a comment, hit me up on Twitter / Mastodon etc)!
I’ll chat about this crap all day if someone lets me.
Until next time!