Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 78

Creating a Settings Menu

Recommended resources

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:

  1. Create a Settings Button: In your existing HUD's UXML file, add a Button element. Name it settings-button and style it with a gear icon or simple text.
  2. Create the Settings Panel: In the same UXML file, create a new top-level VisualElement that will act as the container for your menu. Name it settings-panel.
    • By default, this panel should be hidden. In its USS style, set display: none;.
    • Inside this panel, add the following:
      • A Label for the title, e.g., "Settings".
      • A Toggle element for character autonomy. Name it autonomy-toggle. Add a label next to it.
      • A Slider for master volume. Name it volume-slider. Add a label for it as well.
      • A Button to close the panel. Name it close-settings-button.

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.

Unity UI Toggle Component Properties
You'll use a Toggle component like this for the autonomy setting. We will be using its UI Toolkit equivalent, but the core concept of an 'on/off' state is the same.

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

  1. In Unity, go to Window > Audio > Audio Mixer.
  2. Click the '+' next to "Mixers" to create a new Audio Mixer. Name it MasterMixer.
  3. In the Groups section, you'll see a "Master" group. This is all we need for now.
  4. Select the "Master" group. In the Inspector, find the "Volume" parameter under "Attenuation". Right-click on "Volume" and select "Expose 'Volume' to script".
  5. 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.