Making a RTS game #18: Preparing our game parameters (Unity/C#)

Today, let’s prepare a few things to make our game parameters ready for in-game display!

This article is also available on Medium.

In the past tutorials, we’ve added a few game parameters in particular for our sound system. The issue is that, for now, those parameters are defined in scriptable objects – meaning in project assets that can only be modified by developers and game designers. Once the game has been compiled and shipped to the players, those settings aren’t accessible anymore! Wouldn’t it be sweet if the player could decide whether or not to have the background music? Or control the volume of ambient sounds? Or even toggle off the FOV for a “sandbox” game?

To do that, we’ll create an in-game settings panel that displays some of our parameters. But first, we’ll need to rewrite our GameParameters and GameSoundParameters classes a bit, and to create our own Unity inspector to make it easier to setup our parameters…

Refactoring the GameParameters with some inheritance

So far, we’ve put most of our options in the GameParameters class, and some new parameters related to our sound system in a dedicated GameSoundParameters class. The problem is that this setup is not ideal for automating the display.

What I want is to be able to associate each class with a “game options menu”, i.e. have one menu for global game parameters, another for sound parameters, etc. At the moment, it’s pretty hard to do without writing somewhere by hand the list of all menus. And as always: writing by hand means running the risk of inconsistencies and loosing a lot on scalability. Whenever I’ll add a new system, I’ll have to remember to update this list, check for the display… this is not the right way to go!

Instead, we are going to refactor our game parameters classes with a little inheritance logic so that the UIManager can later pick up the proper options menu to display – and what to put inside of each!

Basically, we are going to go from this current configuration (where GameParameters and GameSoundParameters are two Scriptable Objects class in parallel)…

… to this new one (where we moved all the current contents from GameParameters to a new class, GameGlobalParameters, and we’ve re-created an empty GameParameters class to be the parent of the two others):

This way, we’ll be able to list GameParameters classes and automatically get all of our systems well-sorted.

This is pretty easy to do; we simply need to:

  1. rename the current GameParameters class to GameGlobalParameters (don’t forget to also rename the file itself)
  2. create a new GameParameters script that just inherits from the ScriptableObject built-in but doesn’t have any fields for now
  3. have the GameGlobalParameters and GameSoundParameters classes inherit from the new GameParameters

Here is the complete code of the 3 updated classes:

Note: I’ve also renamed the asset menu to “Game Global Parameters” to really match the new class name.

Be careful: because we’ve changed the “meaning” of GameParameters, the asset that you created before using this Scriptable Object is now invalid! You’ll need to recreate it (by using our new “Create > Scriptable Objects > Game Global Parameters” menu) and refill in the variables.

Updating the references to GameParameters

Also, now that we’ve changed the GameParameters class and moved everything to the GameGlobalParameters class, we are going to have some compilation errors, namely because of the GameManager and DayAndNightCycler. Why? Because both these scripts had a reference to our GameParameters asset instance and therefore are completely lost with the missing data.

Let’s fix this problem and improve our setup a bit at the same time!

First things first: we’re going to take care of the GameManager class. We’ll just change the type and rename the game parameters variable; and then remember to drag and drop our new asset in the inspector:

To do this renaming, either use the “refactor” feature of your IDE or do it by hand; if so, make sure to also change the BuildingPlacer.cs file. It has a reference to the GameManager.instance.gameParameters that needs to be changed to GameManager.instance.gameGlobalParameters.

Then, we can take care of our DayAndNightCycler. Indeed for now it doesn’t compile because it has a reference to the old game parameters… even though it could get the new ones directly from the GameManager singleton instance! To avoid confusion and errors, we’re going to remove this variable from the DayAndNightCycler script and instead go through the singleton.

What we need to be wary of is the scripts lifecycle: at the moment, we’re accessing the game parameters in the Awake() function of the DayAndNightCycler, but we’re only setting up the GameManager singleton instance later on, during its Start() function. So just swapping the references will result in a null reference.

We also need to move the initialisation logic of the day-and-night cycler to its Start() function, so the singleton has been properly initialised when we call it:

Finally, to insure that scripts run in the proper order, we’ll configure the “Script Execution Order” in the project settings. This is a nice Unity trick that allows us to make the scripts order deterministic:

Thanks to this setup, we’re absolutely sure that the GameManager Start() method will have run before the DayAndNightCycler Start() method does.

So – we’re done! No more compilation errors – we’re back on track 😉

Giving a name to our game parameters

In order to print them in the UI, we’ll need to have an easy access to the display name of our various game parameters classes. For example, I’d like the GameGlobalParameters to be shown in the settings panel in a little “Global” menu; but how do I define this “Global” label?

For that, I’m going to use another C# OOP feature: abstract classes and methods. As explained in the Microsoft docs:

The abstract modifier indicates that the thing being modified has a missing or incomplete implementation.

Roughly put, by defining a class or a method as abstract, you’re telling the program that its logic is set up elsewhere, and more precisely in its children classes. Here, we’re going to create a little getter for the name – and we’ll add the abstract modifier so we’re sure each child game parameters class defines it:

Trying out listing our game parameters

To check that everything’s working as expected so far and that we won’t have any issue displaying the parameters in-game, let’s do a test run. I won’t actually be doing any UI stuff in this article, but I’ll simply list the various systems and print their names thanks to our brand new GetParametersName() method. I’ll put it in the GameManager script, in the Start() method:

If you start the game and look at the console, you’ll see “Global” and “Sound” printed: the script has managed to automatically fetch our game parameters assets and show their display names.

That’s nice! After this refactor, it’s pretty easy for us to get our various systems and printing out their display names 🙂

Choosing the “in-game displayable” parameters

There is one important thing we need to prepare for our game parameters in-game display: an easy way to pick which ones we actually want the player to see! With out current setup, it’s an “all-or-nothing” situation: if we were to automate the display of the fields of a given game parameters class, we’d either display all of them or none at all.

The ideal scenario would be if we could toggle on and off the “in-game display” on each field individually. So let’s make that happen: we’re going to create our own custom Unity editor so that our game parameters asset show up with an additional togglable icon!

Before doing any actual customisation, let’s add a new field in our GameParameters class: the list of the fields that should be shown in-game – this is the list that we’ll update with our custom icons. To better encapsulate the data and avoid some headaches later on with our fields automatic listing, we’ll set the field as protected, and we’ll define some util functions to go along with it. Here is the full code of the updated GameParameters.cs file:

Because the field is not public, by default Unity won’t serialise it, meaning that it won’t get properly saved when we close the editor, or if we build the game. To insure that it does, we have to add the [SerializeField] custom attribute – this forces the serialisation of this particular bit of data along with the rest of the object.

Now, we can take care of our custom editor. To create our own inspector, we need to create a new script that it is in the “Editor” folder at the root of our assets directory – I’ll call it GameParametersEditor:

Let’s see what to actually write in this script. We’re going to proceed in 3 steps:

  1. first, we’ll create a basic custom editor that retrieves the game parameters asset we’re editing and prints its display name
  2. then, we’ll do some C# reflection to dynamically get the type of the fields and show appropriate GUI editors
  3. finally, we’ll add the in-game toggle as a little button that adds or removes the field name to/from the list of fields to show in-game

Step 1: Setting up the custom editor

To begin with, we need to be using the UnityEditor package so our class can inherit from the Editor Unity built-in. Then we need to override the OnInspectorGUI() method to replace the default inspector with our own.

This function is called regularly to redraw the various UI elements we give it in the custom editor window.

Also, you’ll notice that we have an attribute above the class (inside the square brackets): CustomEditor. This tells Unity what type of assets this custom editor should be shown for. The true parameter enables us to extend this customisation to all children classes of the given type which is absolutely crucial in our case! (Remember that the GameParameters class is abstract so we are never going to create instances of this one – instead, we’ll always be instantiating its child classes!)

We can start diving in the Unity’s EditorGUILayout class to actually display some UI. To write the name of our game parameters menu, we’ll use the LabelField function – it gives us a basic read-only text:

The EditorStyles class provides us with lots of utilities to change the way our UI elements are stylised. Here for example we’ve given the label a bold font face.

If you go back to Unity and click on one of game parameters asset, you’ll now see a very basic inspector that simply shows its display name: we’ve successfully injected our custom editor in place of the default one, yay! 🙂

Step 2: Time to reflect!

Alright – that was the easy part. The real meat of this script is going to be all about reflection. I’ve talked about it in the last episode of this tutorial and in another recent article on C# about generics – in C#, reflection allows you to dynamically interact with your object types. Here, we’re going to use it to get the type of each of the fields in our game parameters asset and display a nice value editor that depends on this type.

For example, we’re going to have a text input for strings, a checkbox for booleans, a number input for floats and ints and even special inputs for colours or vectors! For other variable types, we’ll show a default “any object” input that allows us to drag and drop audio clips, game objects, transforms…

All of these UI inputs work the same: you give them a current value, optionally some styling; and you get back the updated return value whenever the developer changes the content.

This piece of code is quite boring to write but there’s nothing too hard about it – we just try and tackle as many special cases as possible, or revert to the default “object” field input 😉

Also, I’ve used the System.Attribute.IsDefined built-in methods to check whether or not there is a custom “HideInInspector” attribute above the field that should prevent it from appearing. We’ll talk more about attributes in the next section!

In the end, the real trick is that, since we’re using reflection, we can’t directly get the current value and assign the return value to the variable. Instead, we have to use the C# reflection tools field.GetValue() and field.SetValue() that allow us to get or assign a value from/to a given field in an object by using a dynamic field reference.

Note: the EditorGUILayout.BeginHorizontal() and EditorGUILayout.EndHorizontal() are a UI utility to display the name of the fields and their matching editor in a row.

Go back to Unity – the script should recompile and the editor automatically update to show you the new edit fields:

Step 3: Adding our “show in-game” toggles

To add the buttons at the beginning of the row, we’ll simply call the GUILayout.Button() function before we print the field name (just after we’ve started our horizontal group). This Unity’s UI method is a bit peculiar: to define the callback that should be run when clicking on the button, you actually put the GUILayout.Button() inside an if statement, and whatever is in this conditional branch will be executed when you click on the button:

We’ll also change the text on the button from “+” to “-” as we toggle the option on and off – this way, it will be easier to know which fields are currently visible in-game.

But we’d like this specific “prefix” content to be easily changeable if we decide on something else in the future. So to decouple the “prefix” logic from the “show the field logic”, we are going to pass it to the _DrawField() function we just created using a delegate. If you want more info on this C# variable type, you can check out one of my recent articles on this topic; in short, delegates are a way to pass in methods to other methods as parameters. You first define the prototype of functions you want to pass in (because C# remains a strongly-typed language!) and you can then “implement” the delegate by creating actual functions with a body and some logic that follow this prototype.

Here, we want to create a delegate that can receive the asset and the field we’re working on, and display a UI button. It doesn’t return anything, so it’s a void method. The prototype, and therefore the delegate declaration, will be something like:

private delegate void DDrawPrefix(Object obj, FieldInfo field);

Note: the D prefix for delegate variables is a personal convention that I’m currently trying out; it’s inspired by the more conventional I prefix for instances… 😉

Now, we can “implement” (or “instantiate”) this delegate in our code to deal with the UI button creation, and pass it to the _DrawField() function, like this:

Thanks to delegates, we can very easily change the way the “prefix” of our fields is displayed, and we could quickly export it to somewhere place to share it between multiple scripts (as opposed to having to copy again and again the same snippet of code).

Here is the updated editor:

In the code, you’ll notice that we have two weird lines:

EditorUtility.SetDirty(parameters);
AssetDatabase.SaveAssets();

Those are another scheme to force Unity to register this data update and have it saved when we close the editor or we build our game. The first line is a way of telling the program that this field has been modified and should be considered obsolete – we often talk of “dirty” states when we mean “something is not up-to-date, that it’s not proper anymore”. The second line directly tells Unity to update the assets so that if we close the editor after we’ve clicked the “+”/”-” button, the change is indeed written in the data.

On top of that: using sliders for ranged integers and floats

There is actually another little improvement we can make to our custom editor, once again by leveraging C# reflection tools. When we have integers and floats, sometimes, we want their value to be restricted to a given range.

For example, the volume of the background music and the sound effects (SFX) can be represented by an int between 0 and 100, so we have a percentage.

Note: we’ll see in the next episode that this range is not really the best for our RTS and that it’s better to use decibel values, but it’s easier to picture at the moment, so let’s stick with this theoretical percentage mindset…

To do this on the data side, it’s pretty easy: Unity provides with a built-in custom attribute, Range, that lets us define a min and max value for our variable:

If we had the default Unity inspector, we’d automatically get a slider for our variables, now that they have a Range attribute. The only problem is… that our custom editor simply ignores this attribute. So a user could enter just any value, even if it falls out of the range!

To make sure that int and float ranges use sliders, we’ll need to do some reflection to check out the field’s attributes and, if need be, replace our EditorGUILayout.IntField with an EditorGUILayout.IntSlider (same for the FloatField that will be replaced by Slider). This is possible thanks to the C# System.Attribute class:

And we get two nice sliders! 🙂

Bonus: re-testing it!

To wrap this up and test it out, let’s extend our little loop from before, in the GameManager, to also show the list of fields that are displayed in-game. This is quite straight-forward – I’m updating the little test code snippet I put in my Start() method previously:

Say we’ve toggled some options on in our custom editor by clicking the “+” icon (so it’s now displaying a “-” instead): the enableDayAndNightCycle and enableFOV parameters. This means that the player will eventually be able to update those values in the in-game settings panel.

And we can check that our debugs indeed match this list!

Conclusion

Now that we’ve checked everything’s working fine, you can clean up the lines we added in the Start() function of the GameManager to revert to what we had before – in the next article, we won’t be doing this plain console stuff anymore but rather displaying actual UI elements!

This article was a bit more refactoring than usual, but it’s really going to pay off in the next episode, when we create our in-game settings panel. Thanks to this preliminary work, we’ll be able to list our game parameters assets in a breeze and quickly create matching menus. Also, we’re now familiar with how to display type-dependent UI elements, so it will help us design said panel 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *