Sequencing game events

I’m really struggling with coming up with the most elegant way to sequence a very simple chain of events in my GameManager.

The problem is similar but what is discussed here but involves the overall sequence of game events.

I’d like to do some cleanup (disable all enemies and player), then display a “Level Starting” with a level number for 3 seconds, then spawn everything. My GameManager script is on the same object as a UIManager script and I’d like to keep the UI separate from the game logic. I can easily achieve what I want by using events or C# Action, the GameManager invoking an event and the UIManager responding to it. But it seems too convoluted for what I need (the UIManager is the only script interested in displaying a “Level Starting” message). So I’m trying this by coupling the UIManager to the GameManager script, like this:

void Awake()
{
	// Check for unconnected prefabs
	Assert.IsNotNull(PrefabAsteroid);
	Assert.IsNotNull(PrefabUFO);
	Assert.IsNotNull(player);

	// Reset the count variable
	AsteroidController.countAsteroids = 0;

	// Connect to the UIManager
	UIManager = GetComponent<UIManager>();

	// Set the level number
	level = 0;
}

Then I start the game sequence as a coroutine, like this:

void Start()
{
	// Instantiate the player
	// This GameObject is persistent and doesn't get
	// destroyed until the end of the game.
	player = Instantiate(player, Vector2.zero, Quaternion.identity);
	player.Lives = 3;
	StartCoroutine(GameSequence());
}

Now I can tell the UI to display the message I need and do the next steps after some time:

IEnumerator GameSequence()
{
	// Disable player & enemies
	// Some function here
	
	// Display the level number
	UIManager.AnnounceLevel(level);
	yield return new WaitForSeconds(3);

	// Spawn asteroid
	// Start the UFO spawner
	// Spawn player
	// etc...

	// Restart when level is clear
}

However this way of sequencing stuff presents many problems, at least the way I see it:

  1. The UIManager is responsible for displaying the “Starting Level” message but it also handles its duration on the screen (3 seconds, set in UIManager). If I ever need to change it I have to do it in two places. If I pass the required duration to the UIManager and use the same variable in the yield statement I’m essentially making the GameManager decide how long the UI displays the message. What I if I want later to have the UI do a flashy 12-second animation when the level starts?

  2. Again, this can be solved with events but then I’d need the UIManager to invoke an event saying “I’ve finished”, which is going to make the GameManager coroutine too complex in handling that event (at least for me).

  3. It seems weird to have the entire game sequence inside a coroutine.

How would you achieve something as simple as that without over-engineering everything? Seems like a trivial thing to do, and I more or less managed to make it work, but it seems over-engineered to me.

Thanks a lot for your help.

This is the classical problem of flexible design vs. hard-coded solutions.

It is good that you are looking for more flexible solutions and making it cleaner, but also consider how likely it is you will need to change it? If not very likely, then a hard-coded solution is good enough…

That said, I love general solutions, and this can be solved various ways. One is using a state machine where each state is represented by a class, and each state does something different (e.g. CleanUpEnemies State script, CountDownState script, etc.). If you make these script MonoBehaviours and put them on separate GameObjects, you can use OnEnable() and OnDisable() methods to activate and deactivate a state.

However, I think in your case the primary problem is that you don’t separate the display logic from the control logic: your UIManager should only display things, and not know how long does it display a thing. For example, your GameManager can slowly decrease remainingTime in a Coroutine then call UIManager.SetCountDownText(((int)remainingTime).ToString());. This way you take out the logic from the UIManager.

Does this make sense? Feel free to comment if you have more questions!

Will do.
Thanks again.