Hello! Welcome back to our course on building a life simulation game.
In the last lesson, you successfully created the main Head-Up Display (HUD) using UI Toolkit, giving the player a real-time view of their character's needs, mood, and money. This was a critical step in making the game state transparent and interactive.
Now, we'll continue to polish the player experience by adding a much-needed feature: a settings menu. Today's learning outcome is to build a settings menu with an autonomy toggle and volume controls. This will not only give players more control over their game but will also introduce you to handling audio in Unity and persisting player preferences across game sessions.
1. Building the Settings Panel UI
First, let's create the visual elements for our settings menu using the UI Builder, just as you did for the HUD. Your goal is to create a new panel that can be shown or hidden, containing the controls for our settings.
Your Tasks:
- Create a Settings Button: In your existing HUD's UXML file, add a
Buttonelement. Name itsettings-buttonand style it with a gear icon or simple text. - Create the Settings Panel: In the same UXML file, create a new top-level
VisualElementthat will act as the container for your menu. Name itsettings-panel.- By default, this panel should be hidden. In its USS style, set
display: none;. - Inside this panel, add the following:
- A
Labelfor the title, e.g., "Settings". - A
Toggleelement for character autonomy. Name itautonomy-toggle. Add a label next to it. - A
Sliderfor master volume. Name itvolume-slider. Add a label for it as well. - A
Buttonto close the panel. Name itclose-settings-button.
- A
- By default, this panel should be hidden. In its USS style, set
At this stage, you have the static UI structure. We will add the logic to show/hide the panel and make the controls functional in the next steps.

2. Implementing Volume Control with an Audio Mixer
Controlling game audio effectively requires more than just changing a single value. We'll use Unity's Audio Mixer to create a robust and scalable audio pipeline. This allows you to manage different audio types (music, sound effects, UI sounds) in separate channels.
The Logarithmic Nature of Sound
A crucial concept is that human hearing is logarithmic, not linear. A slider that goes from 0 to 1 linearly will sound like the volume drops to almost zero very quickly. To make the slider feel intuitive, we must convert its linear value to a logarithmic one before sending it to the audio mixer.
The following resource provides an excellent, concise explanation of this problem and its solution.
The Logarithmic Scale of Audio Volume
This forum post explains why a linear slider feels wrong for volume control and provides the exact mathematical formula to fix it. This is a key piece of knowledge for creating professional-feeling audio settings.
Read the post by user 'jlf'. Focus on the explanation of the logarithmic fader value and the C# code provided: masterMixer.SetFloat("musicVol", Mathf.Log(soundLevel) * 20);. This is the formula we will use.
Setting up the Audio Mixer
- In Unity, go to Window > Audio > Audio Mixer.
- Click the '+' next to "Mixers" to create a new Audio Mixer. Name it
MasterMixer. - In the Groups section, you'll see a "Master" group. This is all we need for now.
- Select the "Master" group. In the Inspector, find the "Volume" parameter under "Attenuation". Right-click on "Volume" and select "Expose 'Volume' to script".
- In the top-right of the Audio Mixer window, you'll see "Exposed Parameters (1)". Click it. Rename the exposed parameter from "MyExposedParam" to something memorable, like
MasterVolume. This is the string name you will use in your script.
Scripting the Volume Slider
Now, let's create a script to manage our settings. Create a new C# script called SettingsController.cs and attach it to the same GameObject as your HudController.
Here is the code to control the volume. We'll add to this script throughout the lesson.
// SettingsController.cs
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Audio;
public class SettingsController : MonoBehaviour
{
// Assign in Inspector
public UIDocument uiDocument;
public AudioMixer masterMixer;
private VisualElement settingsPanel;
private Slider volumeSlider;
private Toggle autonomyToggle;
void Start()
{
var root = uiDocument.rootVisualElement;
// Get references to the UI elements
settingsPanel = root.Q<VisualElement>("settings-panel");
Button settingsButton = root.Q<Button>("settings-button");
Button closeSettingsButton = root.Q<Button>("close-settings-button");
volumeSlider = root.Q<Slider>("volume-slider");
autonomyToggle = root.Q<Toggle>("autonomy-toggle");
// Register callbacks for button clicks
settingsButton.clicked += () => settingsPanel.style.display = DisplayStyle.Flex;
closeSettingsButton.clicked += () => settingsPanel.style.display = DisplayStyle.None;
// Register callback for the volume slider
volumeSlider.RegisterValueChangedCallback(OnVolumeChanged);
// TODO: Register callback for the autonomy toggle
LoadSettings();
}
private void OnVolumeChanged(ChangeEvent<float> evt)
{
float volumeValue = evt.newValue;
// The slider's range should be set from 0.0001f to 1f in the UI Builder
// to avoid Mathf.Log(0) which is -infinity.
masterMixer.SetFloat("MasterVolume", Mathf.Log10(volumeValue) * 20);
// Save the setting
PlayerPrefs.SetFloat("MasterVolume", volumeValue);
}
// We will implement this later
private void LoadSettings()
{
// TODO: Load saved settings
}
}
In the UI Builder, select your volume-slider and set its Low Value to 0.0001 and its High Value to 1. This is critical for the logarithmic conversion to work correctly.
3. Implementing the Autonomy Toggle
Next, we'll hook up the autonomy toggle. This requires sending a command to our Rust backend to tell the AI whether it should be active or not.
Rust Backend Change
First, let's define a new command and the data structure it expects. We'll modify our command handler in lib.rs.
1. Define the command payload struct (e.g., in src/models.rs):
// In src/models.rs or a new `commands.rs` file
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct SetAutonomyPayload {
pub enabled: bool,
}
2. Update your game state struct to include an autonomy flag:
// In your main game state struct
pub struct GameState {
// ... your other fields like characters, world, etc.
pub autonomy_enabled: bool,
}
Remember to initialize this in your GameState::new() function, probably to true.
3. Handle the new command in your command processor:
// In your command handler function in lib.rs
"set_autonomy" => {
let payload: Result<models::SetAutonomyPayload, _> = serde_json::from_str(&json_payload);
if let Ok(payload) = payload {
game_state.autonomy_enabled = payload.enabled;
// Return a success message
return Ok("{\"status\": \"success\"}".to_string());
}
// Handle error
return Err("Invalid payload for set_autonomy".to_string());
}
Now, in your game loop where you call the autonomous AI's update function (from Module 5), you'll wrap it in a check: if game_state.autonomy_enabled { ... }.
C# Frontend Change
Back in SettingsController.cs, we need to register a callback for the toggle and implement the method to send the command.
1. Add the callback registration in Start():
// In SettingsController.cs, inside the Start() method
autonomyToggle.RegisterValueChangedCallback(OnAutonomyChanged);
2. Implement the OnAutonomyChanged method:
// A struct to match the Rust payload
[System.Serializable]
private struct SetAutonomyPayload
{
public bool enabled;
}
private void OnAutonomyChanged(ChangeEvent<bool> evt)
{
bool isEnabled = evt.newValue;
var payload = new SetAutonomyPayload { enabled = isEnabled };
string jsonPayload = JsonUtility.ToJson(payload);
// Assumes you have a RustApi.SendCommand method that takes command name and payload
RustApi.SendCommand("set_autonomy", jsonPayload, (response) => {
Debug.Log("Autonomy set to " + isEnabled);
});
// Save the setting
PlayerPrefs.SetInt("AutonomyEnabled", isEnabled ? 1 : 0);
}
4. Saving and Loading Player Preferences
We've already added the PlayerPrefs.Set... calls to save the settings when they change. Now we need to load them when the game starts. Let's complete the LoadSettings method in SettingsController.cs.
// In SettingsController.cs
private void LoadSettings()
{
// Load Volume
float savedVolume = PlayerPrefs.GetFloat("MasterVolume", 1.0f); // Default to 1.0f
volumeSlider.value = savedVolume;
// Apply the setting immediately, without triggering the callback's save logic again
masterMixer.SetFloat("MasterVolume", Mathf.Log10(savedVolume) * 20);
// Load Autonomy
int autonomyInt = PlayerPrefs.GetInt("AutonomyEnabled", 1); // Default to enabled (1)
bool autonomyEnabled = autonomyInt == 1;
autonomyToggle.SetValueWithoutNotify(autonomyEnabled); // Prevents firing the callback
// Send initial autonomy state to Rust
var payload = new SetAutonomyPayload { enabled = autonomyEnabled };
string jsonPayload = JsonUtility.ToJson(payload);
RustApi.SendCommand("set_autonomy", jsonPayload, null);
}
Notice the use of SetValueWithoutNotify. This is important because it updates the UI to reflect the loaded state without triggering the OnAutonomyChanged callback, which would redundantly send the command and save the preference again.
Conclusion
Excellent work! You've now built a functional settings menu that gives the player meaningful control over their experience. This is a huge step in moving from a raw simulation to a polished game.
Key Takeaways from this Lesson:
- You learned how to use Unity's Audio Mixer to manage game audio and expose parameters to scripts.
- You understand the importance of logarithmic conversion for volume sliders and how to implement it.
- You connected a UI toggle to the Rust backend to control a core game mechanic (autonomy).
- You can now save and load player settings using PlayerPrefs, ensuring choices persist between sessions.
In our next and final lesson of the course, we'll complete our audio implementation by integrating background music and UI sound effects, bringing your game world to life with sound.