Skip to main content

Welcome to a Grasp lesson

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

Lesson 32

Autonomy Toggle: Player vs. AI Control

Recommended resources

Hello! Welcome back to the final lesson of our "Needs System & Autonomous AI" module.

In the last session, we successfully created the UI to display our character's needs and mood, giving the player a window into the character's internal state. We now have a character who autonomously decides what to do based on their needs, and a player who can see those needs changing. The final piece of this puzzle is to give the player control over this autonomy.

Today, we'll implement a classic feature of life simulation games: an autonomy toggle. This will allow the player to switch between direct, manual control of their character and letting the AI we built take over. By the end of this lesson, you will have fulfilled the learning outcome: Implement an autonomy toggle in the UI to switch between player-controlled and AI-controlled modes.

Here's our plan:

  1. Establish an architectural pattern for separating player control from AI control.
  2. Update our Rust backend to track and switch the character's control state.
  3. Create a UI Toggle in Unity for the player to interact with.
  4. Write the C# logic to connect the UI toggle to both the Unity character controller and the Rust backend.

The Architectural Pattern: Swappable "Brains"

Before we write any code, let's think about the architecture. A naive approach might scatter if (isAI) { ... } else { ... } checks all over our codebase. This quickly becomes difficult to maintain. A much cleaner solution, common in game development, is to separate the decision-making logic into distinct, interchangeable components, often called "brains" or "controllers." One "brain" handles player input, while another handles AI logic.

To see this concept in action, let's watch a short video that demonstrates a great architecture for this exact problem.

Unity3D Architecture - Build your game so players can control NPCs

This video, "Unity3D Architecture - Build your game so players can control NPCs" from the Jason Weimann channel, demonstrates a system where control can be swapped between the player and multiple AI characters. We will use this as our conceptual model.

Please watch these sections: The Demo (00:18 - 00:54): See the end goal in actionβ€”switching control between different characters. The Character Script (01:37 - 02:45): Pay close attention to how the Character script itself doesn't contain control logic. It just holds a reference to a CharacterBrain and calls its Tick method. The Abstract CharacterBrain (02:45 - 04:19): This is the key. Understand that this abstract class defines a common interface (Tick method) that all 'brains' must implement. This is what makes them swappable.

While we won't implement this exact class hierarchy today to keep our lesson focused, the core principle is what matters: decoupling the character from the source of its commands. Our implementation will reflect this principle. Player commands will originate from a C# script that processes input, while AI commands will originate from our Rust backend. The toggle will switch which source is active.


1. Updating the Rust Backend

First, let's give our Rust Character a way to know whether it should be listening to the player or thinking for itself.

  1. Define a ControlState Enum: In your lib.rs file, create a C-compatible enum to represent the two possible control states.

    
    
    
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum ControlState {
        Player,
        AI,
    }
    

    We use #[repr(C)] so we can pass this enum across the FFI boundary. PartialEq and Eq allow us to compare enum variants (e.g., control_state == ControlState::AI).

  2. Add ControlState to the Character Struct: Add the new enum as a field in your Character struct. Let's make Player the default state.

    // In your Character struct definition
    pub struct Character {
        // ... existing fields like id, name, needs, mood_score ...
        pub control_state: ControlState,
    }
    
    // In your character creation function (e.g., create_character)
    // When creating a new Character instance:
    Character {
        // ... other fields initialized ...
        control_state: ControlState::Player, // Default to player control
    }
    
  3. Create an FFI Function to Change State: We need a way for Unity to tell Rust to change the control state. Add the following FFI function.

    
    
    
    #[no_mangle]
    pub extern "C" fn set_character_control_state(character_ptr: *mut Character, new_state: ControlState) {
        if character_ptr.is_null() {
            return;
        }
        let character = unsafe { &mut *character_ptr };
        character.control_state = new_state;
    }
    
  4. Conditionally Run the AI: Finally, modify your main game loop or tick function in Rust. It should only call the autonomous action selection logic if the character's control_state is AI.

    // In your main game update/tick function where you loop through characters
    // ...
    if character.control_state == ControlState::AI {
        // This is the function from the previous lesson that makes the AI choose an action
        character.choose_action(); 
        
        // You might have other logic here to execute the chosen action
    }
    // If the state is Player, Rust does nothing and waits for commands from Unity.
    // ...
    

After making these changes, recompile your Rust library with cargo build --release. Our backend is now ready.


2. Creating the Autonomy Toggle in Unity

Now, let's switch to the Unity Editor and create the UI element for the player.

How to setup a Toggle in Unity - and how to assign dynamic functionality

The video "How to setup a Toggle in Unity" by Christina Creates Games will guide us through creating the UI element and hooking it up to a script. We'll focus on creating the toggle and understanding its OnValueChanged event.

Follow these steps, using the video as a reference: Create the Toggle (00:10 - 01:30): Right-click your Canvas in the Hierarchy, go to UI -> Toggle. Customize its appearance and add a TextMeshPro - Text label next to it that says "Enable Autonomy". Understand OnValueChanged (01:30 - 02:29): This section explains the OnValueChanged event, which fires every time the toggle is clicked. It passes a boolean value indicating the new state (true if checked, false if not). Adding Listeners via Code (03:08 - 04:34): This is the best practice for what we're doing. We will programmatically add a 'listener' to the event, which is a method that gets called when the event fires. This keeps our logic cleanly in code.

As a supplement, the official Unity documentation provides a concise code example for this exact task.

UI.Toggle.onValueChanged - Scripting API

This is the official documentation for the onValueChanged event. It's a quick read and provides a clear, minimal code sample.

Read the description and review the C# code example. Notice how AddListener is used in the Start method to attach a function to the toggle's event.

After following the guides, you should have a Toggle UI element in your scene, ready to be scripted.

Linking a UI Toggle to C# in Unity
Your setup will involve connecting a script to the Toggle's `OnValueChanged` event, similar to what is depicted here, but we will do it through code for better practice.

3. Implementing the Control Switch in C#

The final step is to write the C# script that listens to the UI Toggle and communicates the state change to both our Unity character controller and the Rust backend.

  1. Define C# ControlState Enum: In a script (e.g., CharacterData.cs or a new AutonomyManager.cs), define the C# version of our enum. The names and order must match the Rust version exactly.

    public enum ControlState
    {
        Player, // Value = 0
        AI      // Value = 1
    }
    
  2. Import FFI Function: In the same script, import the new function from our Rust library.

    using UnityEngine;
    using UnityEngine.UI;
    using System.Runtime.InteropServices;
    using System; // For IntPtr
    
    public class AutonomyManager : MonoBehaviour
    {
        private const string RustLib = "your_library_name"; // CHANGE THIS
    
        [DllImport(RustLib)]
        private static extern void set_character_control_state(IntPtr characterPtr, ControlState newState);
    
        // ... more code to come
    }
    
  3. Disable Player Input: We need to modify the C# script that handles player input (e.g., the point-and-click movement from Module 4). Add a public property to enable or disable its functionality.

    // In your character controller script (e.g., PlayerMovementController.cs)
    public class PlayerMovementController : MonoBehaviour
    {
        public bool IsAutonomous { get; set; } = false;
    
        void Update()
        {
            // Wrap all your player input handling logic in this check!
            if (!IsAutonomous)
            {
                // Your existing code for raycasting and moving the character on click goes here.
                // For example:
                if (Input.GetMouseButtonDown(0))
                {
                    // ... raycast to find a point on the ground ...
                    // ... send move command to Rust ...
                }
            }
        }
    }
    
  4. Create the Manager Script: Now, let's complete the AutonomyManager.cs script. This script will connect everything.

    public class AutonomyManager : MonoBehaviour
    {
        private const string RustLib = "your_library_name"; // CHANGE THIS
    
        [DllImport(RustLib)]
        private static extern void set_character_control_state(IntPtr characterPtr, ControlState newState);
    
        [Header("UI")]
        [SerializeField] private Toggle autonomyToggle;
    
        [Header("Character Control")]
        // Drag your player character GameObject here in the inspector
        [SerializeField] private PlayerMovementController playerController; 
        
        // We assume you have a way to get the character's pointer
        private IntPtr activeCharacterPtr;
    
        void Start()
        {
            if (autonomyToggle == null || playerController == null)
            {
                Debug.LogError("AutonomyManager is not configured correctly!");
                return;
            }
    
            // Get the character pointer (you likely have this from when the character was created/loaded)
            // For example: activeCharacterPtr = GameManager.Instance.GetPlayerCharacterPtr();
            
            // Add a listener that calls our method whenever the toggle value changes
            autonomyToggle.onValueChanged.AddListener(OnAutonomyChanged);
    
            // Initialize the toggle to match the character's initial state
            autonomyToggle.isOn = playerController.IsAutonomous;
        }
    
        private void OnAutonomyChanged(bool isAutonomous)
        {
            // 1. Update the C# controller to enable/disable player input
            playerController.IsAutonomous = isAutonomous;
    
            // 2. Determine the new state for Rust
            ControlState newState = isAutonomous ? ControlState.AI : ControlState.Player;
    
            // 3. Call the FFI function to update the Rust backend
            if (activeCharacterPtr != IntPtr.Zero)
            {
                set_character_control_state(activeCharacterPtr, newState);
            }
            
            Debug.Log($"Autonomy mode set to: {newState}");
        }
    
        // Make sure to remove the listener when the object is destroyed
        void OnDestroy()
        {
            if (autonomyToggle != null)
            {
                autonomyToggle.onValueChanged.RemoveListener(OnAutonomyChanged);
            }
        }
    }
    
  5. Wire it up in the Editor:

    • Create an empty GameObject in your scene named GameSystems or similar.
    • Attach the AutonomyManager.cs script to it.
    • Drag your UI Toggle from the Hierarchy into the Autonomy Toggle field in the Inspector.
    • Drag your player character GameObject (the one with the PlayerMovementController script) into the Player Controller field.
    • Ensure your game has a way to provide the activeCharacterPtr to the AutonomyManager.

Now, press Play. When you check the "Enable Autonomy" box, your character should stop responding to your clicks and start moving on its own to fulfill its needs. When you uncheck it, it should stop its autonomous behavior and once again respond to your commands.


Conclusion

Congratulations on completing the "Needs System & Autonomous AI" module! You've built a complete feedback loop: the character's needs decay over time, the AI decides how to fix them, the UI shows the player what's happening, and now the player has the ultimate control to enable or disable this autonomy.

Key Takeaways:

  • Separation of Concerns: We separated player control logic (in C#) from AI control logic (in Rust), using a state enum to determine which is active.
  • FFI for State Control: We created a simple Rust FFI function and a corresponding C# enum to allow the UI to control the character's core state in the backend.
  • UI Event Listeners: The Toggle.onValueChanged.AddListener() pattern is a powerful and clean way to react to UI events entirely through code, avoiding messy Inspector configurations.
  • Conditional Logic: By wrapping player input code in an if (!IsAutonomous) check and the Rust AI logic in an if (control_state == ControlState::AI) check, we effectively switch between the two control schemes.

This concludes Module 5. In our next lesson, we will begin Module 6: Player Interaction Framework. Now that the player can take control, we need to give them interesting things to do. We'll start by building a system to click on objects and characters in the world and see a dynamic menu of available interactions.