Making a RTS game #43: Designing our main menu 2/2 (Unity/C#)

Let’s continue our RTS and link our main menu and game scenes!

This article is also available on Medium.

In the last episode, we prepared our main menu scene and created some UI dynamically to access data on our various maps and create new game sessions. Today, we’re going to finish up the logic and propagate the necessary data to go from the main menu to the game scene!

Updating the core scripts

First things first: let’s modify our CoreBooter and CoreDataHandler classes to take advantage of our multi-scene workflow.

At the moment, the booter script directly goes to my “Map1” scene; now, I want it to go to the main menu scene instead, and I also want to prepare a function to properly load any map by name.

So let’s write a new function, LoadMenu(), and update LoadMap() to load/unload all the necessary Unity scenes and re-compose the right multi-scene combo (remember that we want to load our scenes in additive mode so that they are overlaid together, as I explained in my Unity multi-scene workflow tutorial):

In LoadMap(), we have to make sure to unload the main menu scene if it was loaded previously. We also need to call the LoadMenu() method in the starting phase to automatically load the main menu.

The second thing we have to do is update the CoreDataHandler script so that it stores a bit more info. For now, we just keep track of a MapData variable: the problem is that it does not relate to a specific game session, just to a specific Unity map scene. What if I have two sessions on my “Map1” that I want to save in parallel?

To fix this issue, we’ll also generate a unique ID for each game session and use it when we save or load data to refer to this specific session uniquely. I’ll simply compose the name of the map with a random ID generated via the C# built-in System.Guid package:

While we’re at it, let’s also modify the “Core” scene: to avoid duplicates, we’ll move the EventSystem (for the UI canvases) to this “Core” scene and remove it from the “MainMenu” and “GameScene”:

This will prevent errors (because having multiple EventSystems isn’t really a good idea!) 😉

Propagating data between our scenes

After these changes, I’m now able to keep track of a game session and share this data between my main menu and my game scene easily, just by writing to and reading from the current CoreDataHandler Singleton instance.

Let’s do this in our MainMenuManager, in a new function called StartNewGame():

We can then assign this function to our “Start game” button in the UI:

If you try to open the “Core” scene and run the game, you’ll see that now when you pick a map and click the start button, you’ll get a transition to the game scene and the selected map will be loaded.

The only problem is that… all these nice player parameters we prepared in the menu have completely disappeared!

Transferring the player parameters

That’s because, for now, we are loading the game player parameters from a generic Scriptable Object with no link to the main menu configuration whatsoever… rather, what we need to do is use our brand new session ID to store the main menu GamePlayerParameters instance on the disk, in a specific folder, and then reload it in the game scene.

To do this, we first have to change a bit our util BinarySerializableData and BinarySerializableScriptableObject classes. There are two things we need to take care of:

  • we must have a way of choosing the save/load path ourselves (at the moment, it’s computed automatically based solely on the application’s data path)
  • we need to bypass the conditional field serialisation we prepared some tutorials ago so that, here, we serialise everything and we can restore a complete object in our game scene

To begin with, we should change the constructor of the BinarySerializableData class to also accept an optional forceAll boolean flag – if it’s enabled, then we’ll disregard the _fieldsToSerialize list and simply serialise all the fields we can:

Then, let’s go to our BinarySerializableScriptableObject script and update the SaveToFile() and LoadFromFile() functions to force a full serialisation and optionally use a user-defined file path:

Finally, we can make use of those changes in our MainMenuManager, when we start a new game, to save the GamePlayerParameters we prepared in a folder that is specific to this game session:

Note: here, I’m forcing my player index (myPlayerId) to always be 0 because it’s a single-player game and it’s more convenient, but in a multiplayer context this would obviously be different and more complex! 😉

And we can update our logic at the other end of the workflow, in the DataHandler, so that when the game scene first loads the parameters it looks at this folder for the player parameters:

And tadaa! If you re-run the game (again, starting from the “Core” scene), you’ll have a basic menu to create a new game session and you’ll directly be sent to the game scene with your map and game parameters all loaded up 🙂

Improving the scene transitions

But… wait. It’s a bit weird to actually see the map load on the side, right? It would be better to hide it while it’s loading; so time to add some scene transitions with a little fade to a black screen! 😉

I’ve actually already talked about animating Unity UI via C# scripting in another tutorial – overall, the idea is to use coroutines to update your UI elements parameters without blocking the main process. Here, for example, I’ll add a black panel on top of everything else and change it’s alpha transparency linearly over a few seconds to create a fade-out/fade-in effect.

Here is a demo of what we’ll have in the end:

The black panel is a simple UI image that is added to the “Core” scene so that it can be used anytime in the game. Also, we have to disable raycasting so that this UI element doesn’t block the mouse events on the other elements:

And finally, we can make sure that it’s above all other UI canvases by setting the Canvas “Sort order” parameter – and we can remove the “Graphic Raycaster” component that is added by default since we don’t need raycasting:

Now, the important thing is to properly time and chain the different events. All in all, a scene transition is going to work as follows:

  • first, we start the UI transition of the black screen from transparent (alpha = 0) to full black (alpha = 1) – it will take 1 second
  • then, we’ll load/unload the right scenes (while the screen is completely dark and hides the elements that could be loading weirdly)
  • finally, we’ll do our second UI transition, the fade-in, to turn the screen back to transparent – again, during 1 second

Since we’ll have the same process when going from the main menu to the game scene, or the other way around, we can define a single util coroutine for our scene switching:

As you can see, here, I simply lerp the UI image colour between transparent and black, or black and transparent, in the two while-loops. Then, I use the WaitUntil() built-in method and check if the async scene load/unload process is finished. This way, I’m sure that the scenes will be ready before exiting the transition!

But you might notice that I’m not calling LoadMenu() and LoadMap(): I’m using private functions with underscore prefixes (_LoadMenu() and _LoadMap()). That’s because I want the initial LoadMenu() and LoadMap() to abstract away all this complexity – so I’ll turn them into wrappers for the coroutines and do the real scene load/unload into the new private methods:

Once you’ve implemented this, you’ll instantly get the initial fade-in into the main menu scene and the scene transitions without changing the rest of the code (since we’ve kept our LoadMenu() and LoadMap() as entry points)! 🙂

A quick fix: “no cameras rendering”?

The quick warning message we see on the screen during the transition, “No cameras rendering”, is because of this short moment during which the main menu is unloaded and the game scene hasn’t been loaded yet – so there is no camera… and that’s not really a great situation 😉

To fix this, we have to swap around the unload and load calls in our _LoadMap() function:

But since we will briefly have all the game objects from both the main menu and the game scenes at the same time, we’ll need to be careful about two things:

  • at the moment, our BuildingPlacer instance in the game scene makes a reference to the Camera.main during its initialisation phase; if we have multiple cameras with the “MainCamera” tag at the same time, this could cause a wrong reference and further on some null errors
  • we’ll temporarily have multiple audio listeners – which Unity doesn’t like too much: one in the “MainMenu” scene (directly on the camera object) and one in the “GameScene” (on the “GroundTarget” game object)

We can fix the first issue by removing the “MainCamera” tag from the camera in the main menu scene: it doesn’t really need it and it will simply solve our problem!

For the audio listeners, the trick is to disable them when we do the switch:

And with that, we now have a nice transition with a fade to black, no warnings, and all of the right data transferred to the brand new game scene!

Conclusion

In this episode, we’ve finished our main menu scene and we’ve made sure that all the data is propagated through our multi-scene workflow. We’ve also added some nice scene transitions and we’ve talked a bit about the warnings you can have in Unity because of duplicate elements or components.

Next time, we’ll switch to another topic and talk about improving our 3D models: we’ll see how to import rigged and animated character models from Blender into Unity, how to use an Animator component to have them move during the game and how to link this Animator to our character behaviour tree!

Leave a Reply

Your email address will not be published.