Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Why and how to use the Coordinator pattern.
Purpose
The Coordinator pattern helps manage navigation inside the game or application.
Motivation
We all know that kind of situation when your game have a Lobby screen, where you can go and play some level and if user doesn’t have enough coins we should offer him to buy some after we should show a popup that says that purchase is successful and let him continue play the level he chooses.
But how to manage this kind of flow correctly?
Let’s look at the options that we have here:
The if statement is gimmick and not reusable. So we threw them away immediately.
State machine is cool. But we will need to keep not only states that correspond with screens, but also logic, when users have enough coins and when not, etc.
We can also try a Strategy pattern here and there. But it’s a local solution. And we need something that we can build on our architecture.
The next thing is some sort of popup manager with queue and other stuff. And this solution will work unless you have some cycles and branching.
So, let's look at what Coordinator Pattern can offer to us.
The Pattern
First things first. Let’s define what it should do. We need to present the flow and dismiss it.
public class GameCoordinator : ICoordinator { public void Present() { //TODO: create view and do some preparation } public void Dismiss() { //TODO: destroy view and do some cleaning } }
This will be the main coordinator that will handle other coordinators, such as: LoadingCoordinator, MainScreenCoordinator and GameplayCoordinator.
public class GameCoordinator : ICoordinator { private LoadingCoordinator _loadingCoordinator; private MainScreenCoordinator _mainScreenCoordinator; private GameplayCoordinator _gameplayCoordinator; public void Present() { //TODO: create view and do some preparation StartLoading(); } public void Dismiss() { //TODO: destroy view and do some cleaning _loadingCoordinator = null; _mainScreenCoordinator = null; _gameplayCoordinator = null; } private void StartLoading() { _loadingCoordinator = new LoadingCoordinator(); _loadingCoordinator.Present(); } private void StopLoading() { _loadingCoordinator.Dismiss(); } private void StartMainScreen() { _mainScreenCoordinator = new MainScreenCoordinator(); _mainScreenCoordinator.Present(); } private void StopMainScreen() { _mainScreenCoordinator.Dismiss(); } private void StartGameplay() { _gameplayCoordinator = new GameplayCoordinator(); _gameplayCoordinator.Present(); } private void StopGameplay() { _gameplayCoordinator.Dismiss(); } }
So now let’s define the problems with this code:
We don’t know when we should start one Coordinator and stop another.
We need to make a transition between two Coordinators.
Not sure that I'm happy with creating new instances of the Coordinators using the simple new keyword.
Coordinator inside coordinator
Delegate
I’m a big fan of SOLID principle and specially of letter S. Which stands for single responsibility. I prefer to move event dispatching to separate class, so we will not bloat the Coordinator class, especially if its has a lot of sub flows.
public class GameDelegate : IDelegate { public Action OnLoad; public Action OnStartGameplay; public Action OnStopGameplay; }
And let’s modify the GameCoordinator:
public class GameCoordinator : ICoordinator { private LoadingCoordinator _loadingCoordinator; private MainScreenCoordinator _mainScreenCoordinator; private GameplayCoordinator _gameplayCoordinator; private GameDelegate _delegate = new GameDelegate(); public void Present(IDelegate parentDelegate = null) { //TODO: create view and do some preparation _delegate.OnLoad += StopLoading; _delegate.OnLoad += StartMainScreen; _delegate.OnStartGameplay += StopMainScreen; _delegate.OnStartGameplay += StartGameplay; _delegate.OnStopGameplay += StopGameplay; _delegate.OnStopGameplay += StartMainScreen; StartLoading(); } public void Dismiss() { //TODO: destroy view and do some cleaning _delegate.OnLoad -= StopLoading; _delegate.OnLoad -= StartMainScreen; _delegate.OnStartGameplay -= StopMainScreen; _delegate.OnStartGameplay -= StartGameplay; _delegate.OnStopGameplay -= StopGameplay; _delegate.OnStopGameplay -= StartMainScreen; _loadingCoordinator = null; _mainScreenCoordinator = null; _gameplayCoordinator = null; } private void StartLoading() { _loadingCoordinator = new LoadingCoordinator(); _loadingCoordinator.Present(_delegate); } private void StopLoading() { _loadingCoordinator.Dismiss(); } private void StartMainScreen() { _mainScreenCoordinator = new MainScreenCoordinator(); _mainScreenCoordinator.Present(_delegate); } private void StopMainScreen() { _mainScreenCoordinator.Dismiss(); } private void StartGameplay() { _gameplayCoordinator = new GameplayCoordinator(); _gameplayCoordinator.Present(_delegate); } private void StopGameplay() { _gameplayCoordinator.Dismiss(); } }
Further improvements here can be usage of Signal pattern or Observer. You can use some centralized event propagator or you can go for something like built-in signals in Zenject if you want to break your application into independent modules.
Router
Let’s look again into the GameCoordinator class as we can see here we have a lot of code duplications: we starting and stopping coordinators. Since I prefer to stick with the Don’t Repeat Yourself (DRY) principle, let’s introduce separate class for transitions, so if the way transition happens in our game changes we will not going to go through all classes and change it everywhere.
public class Router : IRouter { public void Transition(ICoordinator parent, ICoordinator current, ICoordinator next) { current?.Dismiss(); next.Present(parent.Delegate); } }
And again we should modify the GameCoordinator:
public class GameCoordinator : ICoordinator { private LoadingCoordinator _loadingCoordinator; private MainScreenCoordinator _mainScreenCoordinator; private GameplayCoordinator _gameplayCoordinator; public IDelegate Delegate { get => _delegate; } private GameDelegate _delegate = new GameDelegate(); private Router _router = new Router(); public void Present(IDelegate parentDelegate = null) { //TODO: create view and do some preparation _delegate.OnLoad += OnLoad; _delegate.OnStartGameplay += OnStartGameplay; _delegate.OnStopGameplay += OnStopGameplay; StartLoading(); } public void Dismiss() { //TODO: destroy view and do some cleaning _delegate.OnLoad -= OnLoad; _delegate.OnStartGameplay -= OnStartGameplay; _delegate.OnStopGameplay -= OnStopGameplay; _loadingCoordinator = null; _mainScreenCoordinator = null; _gameplayCoordinator = null; } private void StartLoading() { _loadingCoordinator = new LoadingCoordinator(); _router.Transition(this, null, _loadingCoordinator); } private void OnLoad() { _mainScreenCoordinator = new MainScreenCoordinator(); _router.Transition(this, _loadingCoordinator, _mainScreenCoordinator); _loadingCoordinator = null; } private void OnStartGameplay() { _gameplayCoordinator = new GameplayCoordinator(); _router.Transition(this, _mainScreenCoordinator, new GameplayCoordinator()); _mainScreenCoordinator = null; } private void OnStopGameplay() { _mainScreenCoordinator = new MainScreenCoordinator(); _router.Transition(this, _gameplayCoordinator, _mainScreenCoordinator); _gameplayCoordinator = null; } }
For future improvements you can implement screen blocking here, play sounds, some video effects, tweens and do other coordinators orchestration here.
Factory
What I dont like as well is using new keyword everywhere. So we can use here a simple Factory.
The factory implementation is pretty straightforward, but if you like you can complicate it as much as you want.
public class CoordinatorFactory { public ICoordinator CreateGameplayCoordinator() { return new GameplayCoordinator(); } public ICoordinator CreateLoadingCoordinator() { return new LoadingCoordinator(); } public ICoordinator CreateMainScreenCoordinator() { return new MainScreenCoordinator(); } }
Modify the main coordinator:
public class GameCoordinator : ICoordinator { private ICoordinator _loadingCoordinator; private ICoordinator _mainScreenCoordinator; private ICoordinator _gameplayCoordinator; public IDelegate Delegate { get => _delegate; } private GameDelegate _delegate = new GameDelegate(); private IRouter _router = new Router(); private CoordinatorFactory _factory = new CoordinatorFactory(); public void Present(IDelegate parentDelegate = null) { //TODO: create view and do some preparation _delegate.OnLoad += OnLoad; _delegate.OnStartGameplay += OnStartGameplay; _delegate.OnStopGameplay += OnStopGameplay; StartLoading(); } public void Dismiss() { //TODO: destroy view and do some cleaning _delegate.OnLoad -= OnLoad; _delegate.OnStartGameplay -= OnStartGameplay; _delegate.OnStopGameplay -= OnStopGameplay; _loadingCoordinator = null; _mainScreenCoordinator = null; _gameplayCoordinator = null; } private void StartLoading() { _loadingCoordinator = _factory.CreateLoadingCoordinator(); _router.Transition(this, null, _loadingCoordinator); } private void OnLoad() { _mainScreenCoordinator = _factory.CreateMainScreenCoordinator(); _router.Transition(this, _loadingCoordinator, _mainScreenCoordinator); _loadingCoordinator = null; } private void OnStartGameplay() { _gameplayCoordinator = _factory.CreateGameplayCoordinator(); _router.Transition(this, _mainScreenCoordinator, new GameplayCoordinator()); _mainScreenCoordinator = null; } private void OnStopGameplay() { _mainScreenCoordinator = _factory.CreateMainScreenCoordinator(); _router.Transition(this, _gameplayCoordinator, _mainScreenCoordinator); _gameplayCoordinator = null; } }
For further improvements you can use any IoC framework that you like. Previously mentioned Zenject does the job nicely.
Coordinator inside coordinator
The only unsolved thing left here is a design of nested Coordinators. This is pretty straightforward. We will keep children is HashSet, so insertion and removing them will be O(1). Let’s change ICoordinator and create a base class for all the Coordinators.
public class AbstractCoordinator : ICoordinator { public virtual IDelegate Delegate { get; } private HashSet<ICoordinator> _children = new HashSet<ICoordinator>(); public void Present(IDelegate coordinatorDelegate = null) { OnPresent(coordinatorDelegate); } public void Dismiss() { foreach (var coordinator in _children.ToList()) { coordinator.Dismiss(); } _children.Clear(); OnDismiss(); } public void AddChild(ICoordinator coordinator) { if (_children.Contains(coordinator)) { return; } _children.Add(coordinator); } public void RemoveChild(ICoordinator coordinator) { if (!_children.Contains(coordinator)) { return; } _children.Remove(coordinator); } protected virtual void OnPresent(IDelegate coordinatorDelegate = null) { throw new NotImplementedException(); } protected virtual void OnDismiss() { throw new NotImplementedException(); } }
Also we will change the Router a bit, so it will take of removing and adding coordinators to each other.
public class Router : IRouter { public void Transition(ICoordinator parent, ICoordinator current, ICoordinator next) { current?.Dismiss(); parent.RemoveChild(current); next.Present(parent.Delegate); parent.AddChild(next); } }
There is possible case when you will need to relocate one coordinator to another. So you will have to take it from there and figure out how to it by youself.
What next?
I will not going to describe the implementation of the other coordinators, since a goal of this article is to briefly describe the Coordinator pattern. But you can download it from here: https://github.com/thenitro/CoordinatorPatternExample. Also there is a separate repository with abstract stuff, that I made: https://github.com/thenitro/SharpCoordinatorPattern. I connected the lib via submodule and you can do the same way for your project.
Also in this article I didn’t covered working with views. There are many options how to organize it: you can use a asset provider with serialize fields and static access, you can use the Addressables, or load it from the Resources.
Feel free to comment and share this article.
Read more about:
BlogsYou May Also Like