Making a RTS game #41: Preparing for a main menu… (Unity/C#)

Let’s continue our RTS and prepare our project for multi-scenes!

This article is also available on Medium.

So far, we’ve worked in just one scene: our game scene, where we have our map, and units, and day/night system and so on. In a real game, there would be other scenes so that the player first hits a main menu and then gradually “moves” to the game.

We won’t be creating our main menu in this episode, but in the next one…

However, today, we are going to prepare this scene transition and add some relevant metadata to our game scene so that the player can create or load games from the main menu with a bit of knowledge on what they’re jumping into 🙂

Adding metadata to our maps

Let’s say you’re in a menu and you want to choose a map to play a new game on: what sort of info would you like to have to discriminate between the maps?

The most obvious variables are its name, its size, the maximum number of players it can handle (depending on the number of spawnpoints you’ve defined) and its overall landscape. The three first parameters are pretty easy to find and store: we can pick a name ourselves, get the size from the “Terrain” object in our game scene, and count the number of spawnpoints in our scene.

Having a preview of the landscape is also pretty straight-forward thanks to the map capture utility we added a couple of tutorials ago. Thanks to this capture tool, we can quickly create screenshots as JPG images – and so we’ll be able to reload them in our menu UI pretty easily 🙂

Creating Scriptable Objects for the map basic metadata

But let’s leave the screenshot aside for now and focus on the basic metadata: the name, the size and the max number of players. We’ll also want to have a reference to the Unity scene to load – that’s the actual scene asset that will be loaded if we want to play on this map.

We can store those in a Scriptable Object of a new type, the MapData:

Note: we could of course think of other map-dependent info, like special treasures, or unique day/night settings… up to you to find original ideas!

For now, I’ve just made a basic scene called “Map1” with a 200×200 terrain, so I can make an associated MapData object:

We’ll talk in a just a second about this “Map1” scene, and what game objects should (or should not be) present in it… when we prepare our multi-scene workflow (see below) 🙂

In the rest of the tutorial, I’ll assume that the Scriptable Object associated with a scene has the same name as the scene itself – in my case, the MapData asset is named “Map1”, too.

Automating the map metadata extraction

At that point, I’ve specified the terrain size by hand because I knew that it was 200 wide, and I’ve had to input the scene name and the number of spawnpoints, too.

We could however automate this to avoid making silly mistakes: rather than setting the data ourselves, it would be better to have some scripts fill in what we can! The idea is to do something similar to the minimap capture utility: we’ll prepare a static function and a custom editor tool to:

  • auto-set the scene reference
  • directly look at the “Terrain” object and extract the size from its terrain data
  • get the number of child game objects of the “Spawnpoints” object, to get the max number of players
  • if need be, default the map name to be the same as the scene name

The script will try to load the map data Scriptable Object from the project folder, if it exists, or else it will create it from scratch.

So, let’s say we have this new tool script, MapMetadataExtractor:

The script works as follows:

  1. first, we check that the folder where we store the MapData Scriptable Objects exists (here, I’ve hardcoded it to: “Resources/ScriptableObjects/Maps”), or we create it
  2. we get the scene name from the Scene asset that is passed to the function, and set this scene as active in the hierarchy
  3. we try to get the MapData Scriptable Object associated with this scene; if it doesn’t exist, we create a new MapData instance and set the map name to be the one of the scene
  4. then, we search for the “Terrain” object and get the terrain data bounds
  5. we use these bounds to set the mapSize field of the MapData object
  6. we get the number of spawnpoints in the scene
  7. finally, we update the Scriptable Object by saving the project assets

Now the question is: how can we use this Extract() method? We’d like to call it from the editor layout, but we don’t really need a full window, like with the minimap capture… this time, we just need a button to run the function.

But where should we put this button?

The only info we need is which scene to call the function with – and this info is available in the Hierarchy of game objects. So what I want to do is extend the item display in the Hierarchy to integrate an “Extract” button, if the item is a scene:

To do this, we just need to create another editor script (remember to put it in the “Editor” folder!) with a static constructor, and a specific HierarchyWindowItemOnGUI() function assigned to the editor’s hierarchy item display callback.

The class needs to be assigned a specific attribute: [InitializeOnLoad].

To check whether the item we’re displaying is a scene or a “normal” game object, we simply have to check if the object is null or not; we can then use a little util method to retrieve back the scene asset from the item, _GetSceneFromInstanceID().

Finally, we can use IMGUI functions to add a button in the Hierarchy item Rect – this button will call the MapMetadataExtractor.Extract() when it’s clicked.

All in all, here is the MapMetadataExtractorHierarchyExtensions script:

As soon as you save and recompile, you’ll see the “Extract” button show up in the Hierarchy for all the scene items. If you click the button, then you’ll either update or create the associated MapData asset 🙂

Preparing our multi-scene workflow to share data between scenes

Now that we have some info on our maps, we’ll want to use it and, more importantly, share it throughout our game sessions. To do this, I will rely on a multi-scene workflow.

An overview of our scene organisation

The idea of a multi-scene workflow is to stack multiple Unity scenes at the same time in the Hierarchy by using the additive scene loading mode. This has several advantages: it facilitates collaborative dev, helps you reduce the size of each scene and share data or objects between them. If you want more details on this, you can check out this other article I wrote a while ago on why this technique is interesting.

Here, I want to change my “SampleScene” and transform it into a template “GameScene” that contains everything but the terrain and its spawnpoints. These map-specific objects will remain in the map scenes (for example, in my case, “Map1”).

This will allow me to quickly switch the map-dependent info for another set (i.e. to reload another map) without having to worry about all the “wrapping” game objects).

However, I do need a way of keeping track of which map is currently loaded – and I’ll need to access this info from various places: first, in the main menu, to set it; then, during the loading process, to get the right Unity scene in my Hierarchy; and in my GameScene to retrieve valuable info like the terrain size.

A nice trick here is to use a “Core” scene that is loaded at the very beginning and never unloaded; it contains several empty game objects with just pure data or tool C# scripts – like the script in charge of booting up the right scene, or a shared data storage. This is a sweet alternative to the DontDestroyOnLoad() technique 😉

The following diagram shows the overall organisation I’m aiming for:

Note: here I show in blue the components shared between scenes and in red the ones specific to one map.

As you can see, my “Core” scene contains two pieces of logic: the “booter” (that swaps between scenes and handles the loading/unloading of scenes in the Hierarchy) and the “data handler” (that stores cross-scene data).

Then, my “GameScene” is a template for an actual game session – it contains all the objects that should be present no matter which map is loaded.

Finally, the “Map1”, “Map2”, “Map3”… scenes are examples of specific terrains and spawnpoints sets. The idea is to have only one of these loaded at the same time, and the combo of “Core” + “Game” + “Map X” gives us one full scene.

Setting up the Core scene

Preparing our “Core” scene isn’t very hard: we just need to implement our two new C# scripts – CoreBooter and CoreDataHandler.

The “data handler”

Let’s first take care of the shared data storage. This “data handling” script will help us transfer info from the main menu to the game context, and most notably for now the map that we want to load and play on.

Also, since it’s unique and we want to have it easily accessible, we’ll create a Singleton instance for this script.

The “booter”

Ultimately, the “booter” will load up the main menu so that we can choose which map we want to play on. For now, though, we don’t yet have this menu: let’s just bypass this step and instead directly load the “Map1” terrain.

There are two important things to remember:

  • whenever possible, you should use the async loading and unloading functions because they are way more optimised; in fact, Unity will probably warn you the sync versions are deprecated or unsafe!
  • because we want to keep the “Core” scene in the background, we have to load our new scenes in additive mode

When we load a map for a game, we’ll want to first load the map-specific scene and only then the GameScene template to make sure that, when the GameManager kicks in, all the terrain/spawnpoints info is already present and accessible.

We also want to insure that the active scene is the map-specific one so that the objects we instantiate further down the line are created in this context.

This gives us the following C# script:

You see that the “booter” also updates the shared data storage and sets the current MapData instance so that the other scenes can then read it back and use it in their logic.

Making a scene!

The scene is extremely simple: we can just place both those scripts on empty game objects in our brand new scene called “Core”:

Note that you could even put everything on the same object, but I find it better to quickly see at a glance what different pieces of logic I have in my “Core” scene by using multiple game objects 😉

Taking care of the game context

Now, we’ll simply need to remove all map-dependent game objects from the “SampleScene” and rename it to “GameScene”.

It’s quite straight-forward – I’ll remove the “Terrain” object and all the additional trees or rocks I’d placed by hand, and keep all the rest!

In my “Map1” scene, I just have my terrain, the spawnpoints and a plane for the water:

Adding our scenes to the build settings

For the scripts and the scene loading to work properly, don’t forget to add your scenes to the build settings of the project! This is project-wide data that Unity uses to know which scenes to include in a build of the game and that therefore determines which scenes can be loaded.

To access it, go to the File > Build settings… menu. Here, you can add the open scenes to the list or drag them from your projects folder:

Handling different terrain sizes

The final thing we should take care of in this episode is make sure that our GameScene can handle different map sizes. For now, it assumes that the terrain’s dimensions is 700 in both directions: this should obviously be computed using the current map mapSize metadata!

Basically, we need to move a few anchors and adapt some collider sizes so that the minimap fixes we did last time continue to work properly.

We can take care of this in the GameManager by adding a few public reference slots to various game objects in the GameScene and setting up the right values in the Awake() process:

Now, when we start the scene, the GameManager will automatically use the data stored in the CoreDataHandler Singleton instance to get the map metadata, and then use this metadata to configure the minimap-related components 😉

Conclusion

Today, we’ve taken care of various things in our project to “smooth out” the transition from the main menu, that we’ll create next time, to our current game scene. We have a way of sharing data between scenes, and we’re also more agnostic of the map-dependent parameters (most notably: the terrain size and max number of players).

So, next time, we’ll finally jump into making this main menu, creating new games, etc.

Leave a Reply

Your email address will not be published.