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:
- Establish an architectural pattern for separating player control from AI control.
- Update our Rust backend to track and switch the character's control state.
- Create a UI Toggle in Unity for the player to interact with.
- 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.
-
Define a
ControlStateEnum: In yourlib.rsfile, 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.PartialEqandEqallow us to compare enum variants (e.g.,control_state == ControlState::AI). -
Add
ControlStateto theCharacterStruct: Add the new enum as a field in yourCharacterstruct. Let's makePlayerthe 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 } -
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; } -
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_stateisAI.// 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.

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.
-
Define C#
ControlStateEnum: In a script (e.g.,CharacterData.csor a newAutonomyManager.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 } -
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 } -
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 ... } } } } -
Create the Manager Script: Now, let's complete the
AutonomyManager.csscript. 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); } } } -
Wire it up in the Editor:
- Create an empty GameObject in your scene named
GameSystemsor similar. - Attach the
AutonomyManager.csscript to it. - Drag your UI Toggle from the Hierarchy into the
Autonomy Togglefield in the Inspector. - Drag your player character GameObject (the one with the
PlayerMovementControllerscript) into thePlayer Controllerfield. - Ensure your game has a way to provide the
activeCharacterPtrto theAutonomyManager.
- Create an empty GameObject in your scene named
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 anif (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.