Today, we’re finally going to create our in-game settings panel! 🙂
This article is also available on Medium.
The last episode has been about preparing and refactoring stuff so that we can display our game parameters neatly in the UI. The goal is to allow players to modify those settings in-game, rather than only giving the developers and game designers access to it. Also, we’ll make sure that we have a good control over our audio channels via the use of audio mixers.
Alright – we’re long overdue for a bit of UI 😉
Note: just like in the previous tutorials about UI, I won’t go into the details of all my UI elements because the style of your game should remain personal. I’ll simply explain my overall hierarchy and mention the important elements that I’ll use in my scripts.
Preparing our game settings UI panel and toggling it
Let’s start working on our game settings panel! We’ll just be crudely placing some elements for now, so that we have enough to implement our panel toggling logic and to differentiate between the normal “playing the game” and the new “looking at the UI” modes.
For starters, let’s add a new UI panel in our Canvas – we can call it “GameSettingsPanel” for example. This is the game object that we will enable and disable as we show and hide our settings. Note that I’ve decided to go for a fixed pixel-set size, but you can also rely more on margins that dynamically adapt with the screen size:
This panel consists of several subitems:
- the “Menus”: it’s the vertical sidebar on the left that will display the various possible game parameters “contexts” – there is one per game parameters asset (currently: “Global” and “Sound”)
- the “PanelContent”: it’s the right part of the panel that actually changes depending on the selected menu, and lists the various parameters we want the player to be able to modify. It uses a ScrollView to easily handle long lists of parameters 🙂
The “Menus” use a VerticalLayout to automatically stack the different menu buttons on top of each other. Each menu button will be an instance of a prefab called “GameSettingsMenuButton” – I just made one based on Unity’s basic button after changing the background colour:
There are two little things worth noting on the setup of this button:
- I used a built-in Unity image, the “Background” sprite, but I made sure to set it to “Sliced” mode (for the Image Type parameter): this way, it is not stretched to fit the size of the button but instead it is properly scaled to get nice small rounded corners
- also, I set the Image base colour to be white and I then used the specific button state colours to specify the actual colour for the normal, hover, pressed and focused states – this base colour is the tint that is applied to the state colours, so here, there will be no tint and we directly use the colours we defined for the various states
Now, let’s add a little button in the top right corner of the screen (in the top bar) to allow the player to open and close this panel. Remember to also apply a HorizontalLayout to the “Controls” parent object, like we did before for the in-game resources display:
I set up the top bar button just like the panel menu buttons (with the sliced type and the white base colour).
We’re now ready to add references to those UI elements in our
UIManager script and implement our panel toggling logic. We simply need to add a few public variables and a new public function to call from our UI button:
Make sure that you assign the
gameSettingsPanel variable properly in the inspector and that you set the GameSettingsButton’s on-click function (in the top bar):
If you run the game, the settings panel will be hidden initially but you’ll be able to toggle it by clicking on the button in the top right corner. To hide it again, just click on the same button again!
Pausing/resuming the game while we’re in the UI
Before actually working on our game settings UI panel, we also need to make sure that when the player is browsing through this UI, the game is paused in the background. This “paused” state implies that:
- the camera can’t move
- we don’t check for any interaction with the units: no selection via box dragging, or via groups…
- and of course we can’t move units if we have some selected
- also, the day and night cycle should stop at its current position until we’ve resumed playing, and any other coroutine like unit production should be temporarily frozen
- and we’d like the sounds to fade out quickly when we enter the UI (but the background music should remain to keep the overall ambiance)
Of course, all of these specific states should be reverted back to normal when the player exits the UI panel.
I’m going to use two techniques to take care of all of this: one, we’re going to send events from the
UIManager class whenever we toggle the panel to tell the rest of our systems that the game is paused or resumed; two, we’re going to define a
gameIsPaused boolean variable in our
GameManager that will be accessible to all through its Singleton instance. This way, we’ll be able to simply check this flag to block things like the camera movement.
Edit (09/2022) – thanks to Frédéric!
The following concept was added to properly interrupt and restore the coroutine execution throughout the entire game, and not just the day/night cycle manager.
To pause all of our coroutines at once, we’ll also use a nice Unity tool: the
Time.timeScale. In short this variable is what controls the “flow of time” in the game: if it’s zero, then everything is stopped, if it’s one (the default) then things run at normal speed and if it’s 0.5, you have slow-mo. In our case by turning it to 0 or 1, we’ll be able to “pause the game time” and instantly interrupt any coroutine.
Sending our events
First off, let’s create our events in the
UIManager – we just need to modify the
ToggleGameSettingsPanel() function we created before to send either a “PauseGame” or a “ResumeGame” event depending on the state toggling:
gameIsPaused variable, and using
Now, we’ll take care of receiving those messages in the
GameManager script and adding the
You see that we directly cancel the units navigation logic in this class
Update() method if the game is paused. We also take this opportunity to “freeze time” using the
Time.timeScale built-in global parameter.
And since we’ve added this public variable to our
GameManager and it has a Singleton instance, all the other scripts will be able to access it and “disable themselves” temporarily if the game is paused! So let’s use it in our
UnitsSelection and our
BuildingPlacer classes (at the top of their
Pretty straight-forward, right? 🙂
Fading out sounds (but not music)
Last but not least, let’s take care of the sounds! Remember that we want to do two things: on the one hand, the music should remain in the background (even though we will lower its volume and add some high frequency cut to make it “dimmer”); on the other hand, SFX should completely fade away.
It might sound complicated, but Unity provides us with an incredible tool for sound management: the audio mixers. So far, I’ve kept it simple and we’ve only tinkered with AudioSources and the AudioListener. We can, however, add an intermediate layer: thanks to the mixers, we can take in the sounds produced by our sources and add some effects to them or do some mastering (~volume management), before eventually sending them through the listener. The mixers are basically “middlemen” that transform the sound and spit it back to other mixers or to the AudioListener directly.
The system is really powerful and allows for complex chaining and grouping. For more details, I really encourage you to look at this pair of videos from the Unity dev team – they’re not too long and they’re a great introduction to the system and how to set it up in a Unity project:
Feel free to jump to after the videos if you’re familiar with the Unity audio mixers!
So, based on those videos, here is the setup that I made for our RTS project:
- we have a “SoundEffects” audio mixer
- we have two groups: the “Music” and the “SoundEffects” that are both children of the “Master” group, like in the Unity tutorial
- we have two snapshots of the audio mixers for the “paused” and the “unpaused” game states
In the “unpaused” snapshot, I simply lowered the volume of the music; in the “paused” snapshot, I put the volume of sound effects to the minimum value, I lowered the volume of the music a bit more and I activated the lowpass filter to slightly muffle the ambiance:
Note: remember that decibels (dB) use a logarithmic scale, so tiny changes to the value result into quite big differences in the output! Make sure to test it out regularly in order to avoid completely killing all sounds every time 🙂
I also exposed the volume of the “Music” and “SoundEffects” groups (and renamed them) so that we can access them from our scripts later on:
Then I restored all AudioSources volumes to 1: now that we’re doing the final mix in our mixers, there’s no point doing it by hand all throughout the scene hierarchy!
Finally, I rerouted all of our audio sources to the “SoundEffects” group except for the ambiance music AudioSource on the “GAME” object. So that’s all of the following assignments:
- on the “GAME” object, the music AudioSource outputs to the “Music” audio group:
- on the “GAME” object, the global ambient sounds AudioSource outputs to the “SoundEffects” audio group
- on all of our unit prefabs, all AudioSources output to the “SoundEffects” audio group
Now that all that is done, it’s actually really easy and quick to handle the “mute sounds on pause” situation. We’ll update the
SoundManager script to add the import of the
UnityEngine.Audio package, declare some public variables to reference our snapshots and react to the “PauseGame” and “ResumeGame” events:
(Don’t forget to assign the
unpaused public variables in the Inspector)
Finally, because we’ve used the global
Time.timeScale parameter for our pause/unpause mechanic, we’ll have to make sure to configure our
AudioMixer asset to have an “Unscaled Time” update mode (otherwise it won’t do anything because it will be stopped by the zeroed-out time scale).
And that’s it! If you run the game now, you’ll see that as soon as you click the “Settings” button and enter the UI, the sounds fade out rapidly, the music gets a bit dimmer and there is a little lowpass effect on it. The normal “unpaused” state is restored when you exit the panel.
Loading up our game parameters into the game settings panel
Before we go back to scripting, we need to prepare a few prefabs to show in our UI.
First, there are the type-dependent displays we’ll want to add to the UI panel for each of our game parameters properties. At the moment, we just have boolean or sliders to show; so I’ve simply taken Unity’s built-in “Toggle” and “Slider” UI elements and dragged them to my project Assets to make prefabs out of them:
Then, we need a little wrapper for those properties so that we group the name of the property and its type-dependent input together:
And finally, we need some buttons for the left sidebar, to be able to switch from one game parameters asset display to another:
Note: I’m using some additional textures and colours here but a basic Unity UI button can work. I’ll give more details about the styling assets I used at the end of this article 😉
Now, we can add the required code snippets to our
UIManager – it looks like a lot but it’s really similar to stuff we’ve done previously in this series 😉
(Make sure to assign all the public variables in the Inspector!)
These functions work as follows:
- first, we read the list of possible game parameters assets from the Resources folder and we map each to its display name in a Dictionary (to optimise future queries)
_SetupGameSettingsPanel()initialises the panel thanks to the list of game parameters assets. This list gives us the name of the different option sections which allows us to instantiate menu buttons and to register callbacks for those buttons that ask for the proper panel content based on this section name (when they call the
Don’t forget that because of variable scoping, we need to extract the button callback function assignment to a small util function,
_SetGameSettingsContent()looks for the game parameters asset corresponding to the menu name it was passed and displays each parameter that has been registered in the
FieldsToShowInGamelist. Parameters are shown differently depending on their type – we once again use C# reflection to check the variable type and pull out the right UI prefab.
Of course, the
_SetGameSettingsContent() method is highly dependent on the way you configured your UI element, and the dimensions should be adapted to better fit your style! 🙂
By the way, the order in which you assign those properties to your slider matters! If you try and set the value before you’ve set the proper range, you might get a nasty surprise – that’s because the value is automatically clamped to the current min/max; so if you give a value out of this range, it will be changed for the closest threshold. Since the default range is [0, 1], if you start by setting our music volume value as the slider value (which is at 60), it will be decreased down to 1. Long story short: assign the range first and the value second to avoid issues!
Quick fix: changing the ranges for our music/SFX volumes
Last time, when we talked about the ranges and the sliders, we worked on our game sound parameters and took the music and SFX volumes as examples. But I did mention that this 0-100 range was not ideal – indeed, now, we see that it would be better to match the volume slider in our audio mixers and work with decibels. So let’s update our
The associated Scriptable Objects will automatically update and clamp the values to -12 dB for the music volume and 0 dB for the SFX volume.
Updating the data when we touch the UI
We’re almost there. The only thing left to do is… to actually be able to change settings from this nice panel! For now, it’s a pretty display but it only reflects the current game parameters and doesn’t allow us to update them 😉
Again, we’ll be using events sent by the
UIManager whenever the UI elements are modified to tell the other scripts something has happened. Unity’s toggles and sliders have, of course, a built-in event handler parameter for when the value of the input is updated. It’s called
onValueChanged and it’s a delegate – so we can simply hook our logic here:
Note: if you want a little refresher on events, publishers and subscribers in C#, I’ve recently wrote a little article on this topic.
These callback functions have a double purpose:
- they update the corresponding game parameters Scriptable Object asset so that our modifications are saved when we quit the editor runtime
- they emit events that automatically contain the name of the parameter that was modified and the new value, sent along in the message, so that we can listen to these events in the relevant scripts and run the proper callbacks
For example, the on/off toggle of the day-and-night cycle and the FOV should be handled by the
GameManager, like this:
Make sure you also access the
fov via a public variable, like here, instead of directly using
GetComponent() like before; this is better in terms of caching and it will avoid null references when you go from the “disabled” to the “enabled” state! You’ll need to drag and drop the “FogOfWar” object in this slot in the Inspector, too.
The music and SFX volumes update is quite straight-forward too since we’ve exposed these parameters in the audio mixers:
(As usual, remember to assign the
masterMixer public variable in the Inspector)
As shown in the Unity videos I linked above, you can add more groups and mixers to have a more detailed control of the various sounds in your game – for example, you might want to distinguish the ambient sounds from the unit contextual sounds. It’s up to you to decide how “crowded”/”powerful” the in-game settings panel should be… 🙂
If you run the game now and update the FOV for example, you’ll see that the change is directly applied in the game scene and on the Scriptable Object (here, I show my game screen on the left and the Inspector editing the
GameGlobalParameters asset on the right):
An important note however is that when the game first starts, the mixers will override the game parameters Scriptable Object asset for the initial sound volumes. So if you set a different value in the asset and in the mixers “unpaused” state for your initial music/SFX volumes, it’s the one in the mixer that will get the upper hand!
Bonus: adding an overlay to our game settings panel
Something that is quite common in UIs in general (not only video games) is to have a light overlay behind modal boxes. This is what turns the page darker in the background if you open up a panel on top of it. It really helps the viewer focus its eyes on the modal.
Note: you can even use the overlay to blur the background a little…
Also, oftentimes you want the user to be able to click outside of the modal to close it. This overlay can be useful for this feature because it gives an actual UI element to receive this click event, rather than looking for “clicks outside of a UI panel”.
To add this overlay, we can just create a new panel that stretches across the entire screen and drag our current “GameSettingsPanel” object under it. Then, we’ll rename the new panel to “GameSettingsPanel”, and the old one to “Panel”. Finally, we need to reupdate the public variable in our
UIManager Inspector: drag in the new panel into the
This overlay is typically a plain colour with high transparency (either a light dark or a light white, depending on the overall vibe of your UI). In my case, I chose a transparent dark. The source image “white” is a basic 64×64 solid white texture that I then tint using the Unity’s
Image Color component field:
Now, simply add an “EventTrigger” component to our new “GameSettingsPanel” and set it to call the
And that’s it! If you run the game, you’ll see that the background is now a bit darker when the panel is open, and you can click on this overlay zone to close the modal window.
To give you an idea of a possible UI style (very wood-focused), here is a screenshot of my in-game settings panel after loading in some fonts and downloading some free textures (from Texturer and ambientcg.com, mostly). I added some test fields to show you different UI elements:
In this episode, we’ve finally managed to take advantage of all the preparation from the previous tutorials and display our in-game settings panel. Now, the player can update the game settings at runtime…
… except that they won’t get saved in an actual game build!
Sure, we’ve managed to update our Scriptable Objects so that the Unity editor keeps track of our changes; but once the game is built and shipped, those Scriptable Objects don’t work the same anymore. Next time, we’ll see that we need to introduce serialisation to really save the player data from one play session to another 😉