Hello! Let's dive into our next lesson.
In our last session, you did an excellent job creating a grid-based placement system. Players can now purchase objects and position them in the world, and our Rust backend correctly stores their location. However, these objects are currently just static decorations. It's time to bring them to life.
Today's lesson focuses on achieving a crucial goal for our simulation: enabling interactions with newly placed objects. By the end of this session, a character will be able to walk up to a bed you've placed, and an interaction like "Sleep" will successfully increase their "Energy" need. This step is vital for bridging the gap between "Build Mode" and "Live Mode," making the world you build truly functional.
We will cover:
- The "Smart Object" design pattern, a highly modular and extensible approach.
- Defining interactions as C# components on our object prefabs.
- Connecting the player's clicks to these interactions via the UI you built earlier.
- Executing interactions to trigger animations and update the character's needs in the Rust backend.
1. The "Smart Object" Philosophy
A key design choice in games like The Sims is that the objects themselves are "smart." Instead of a central AI that needs to know about every possible object and interaction, each object advertises what can be done with it. A refrigerator knows it offers "Get Snack," and a bed knows it offers "Sleep." The character AI simply looks at the available "advertisements" and makes a decision.
This approach is perfect for our game because it's incredibly scalable and mod-friendly. Adding a new object with unique interactions doesn't require rewriting the core AI; you just add the logic to the new object itself.
To understand this concept more clearly, please watch the beginning of the following video.
Unity AI Tutorial: Sims-Style AI (Part 1 - Smart Objects)
This video, 'Sims-Style AI (Part 1 - Smart Objects)' by Iain McManus, provides an excellent explanation of the core philosophy behind this design pattern.
Watch from 01:10 to 03:36. Focus on how the logic is placed on the objects in the world, not on the Sim, and why this makes the system easy to extend.
2. Defining Interactions as Components
To implement this pattern in Unity, we will define our interactions as C# components that we can attach directly to our object prefabs. This is an evolution of our earlier JSON-based definition from Module 6; the components will now hold this data on the Unity side, making them easier to work with in the editor.
We'll create two scripts:
BaseInteraction.cs: An abstract class that defines the common interface for all interactions.SimpleInteraction.cs: A concrete implementation for most of our basic, need-fulfilling actions.
The following video segment walks through creating these scripts. While you watch, think about how these components will store the information needed to execute an interaction: its name, its effect on needs, and any associated animations.
Unity AI Tutorial: Sims-Style AI (Part 1 - Smart Objects)
Let's continue with the same video to see how to structure the interaction and smart object scripts.
Watch from 05:22 to 18:27. Pay close attention to the creation of the abstract BaseInteraction class and the SimpleInteraction class that inherits from it. Notice the key properties like DisplayName, Duration, and the abstract methods like CanPerform and Perform.
Now, let's adapt this to our project.
Step 2a: Create the Interaction Scripts
Create two new C# scripts in your Unity project.
First, BaseInteraction.cs. This will be an abstract MonoBehaviour.
// BaseInteraction.cs
using UnityEngine;
public abstract class BaseInteraction : MonoBehaviour
{
[SerializeField] protected string displayName = "Interaction";
public string DisplayName => displayName;
// We will call this to execute the interaction's logic
public abstract void Perform(GameObject interactor);
// We can use this later to determine if an interaction is available (e.g., bed is not occupied)
public virtual bool CanPerform(GameObject interactor)
{
return true;
}
}
Next, SimpleInteraction.cs. This script will hold the specific data for a need-fulfilling interaction. Note that we define which need is affected and by how much, which is the data we'll eventually send to Rust.
// SimpleInteraction.cs
using UnityEngine;
public class SimpleInteraction : BaseInteraction
{
// Corresponds to the need name in Rust
[SerializeField] private string needToAffect;
[SerializeField] private float needChangeAmount;
// Name of the animation trigger to play in the Animator
[SerializeField] private string animationTriggerName;
// How long the interaction takes. For simplicity, we'll use a delay.
[SerializeField] private float interactionDuration = 3.0f;
public override void Perform(GameObject interactor)
{
Debug.Log($"{interactor.name} is performing '{displayName}'");
// Find the character's controller to play animation and send command
var characterController = interactor.GetComponent<PlayerCharacterController>();
if (characterController != null)
{
// You might need to move the character to an interaction point first.
// For now, we'll just play the animation and send the command.
// 1. Play animation
var animator = interactor.GetComponent<Animator>();
if (animator != null && !string.IsNullOrEmpty(animationTriggerName))
{
animator.SetTrigger(animationTriggerName);
}
// 2. Send command to Rust after a delay
characterController.UpdateNeedAfterDelay(needToAffect, needChangeAmount, interactionDuration);
}
}
}
In your PlayerCharacterController (or equivalent script), you'll need to add a method to handle the delayed need update. Using a Coroutine is a great way to do this.
// Add this to your character controller script (e.g., PlayerCharacterController.cs)
public void UpdateNeedAfterDelay(string need, float amount, float delay)
{
StartCoroutine(SendNeedUpdateCoroutine(need, amount, delay));
}
private IEnumerator SendNeedUpdateCoroutine(string need, float amount, float delay)
{
yield return new WaitForSeconds(delay);
// This reuses the command structure and FFI function from Module 6
var command = new InteractionCommand
{
Need = need,
Amount = amount
};
string commandJson = JsonUtility.ToJson(command);
// Call your FFI function to process this command in Rust
// FFI.process_interaction_command(commandJson);
Debug.Log($"Command sent to Rust: Update {need} by {amount}");
}
Step 2b: Create the SmartObject Script
Now, create the SmartObject.cs script. Its main job is to act as a discoverable entry point on our GameObjects.
// SmartObject.cs
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class SmartObject : MonoBehaviour
{
private BaseInteraction[] interactions;
void Awake()
{
// Find all interaction components on this GameObject
interactions = GetComponents<BaseInteraction>();
}
public List<BaseInteraction> GetAvailableInteractions(GameObject interactor)
{
// Later, you can filter this list based on who is interacting
// For now, we return all interactions that pass the CanPerform check.
return interactions.Where(i => i.CanPerform(interactor)).ToList();
}
}
3. Updating Your Object Prefabs
With the scripts ready, the next step is to add them to the prefabs of the objects you can buy and place. Let's configure a bed.
- Open your bed prefab for editing.
- Add the
SmartObject.cscomponent to the root of the prefab. - Add a
SimpleInteraction.cscomponent. - Configure the
SimpleInteractionin the Inspector:- Display Name:
Sleep - Need To Affect:
Energy(must match the name used in your RustNeedsstruct) - Need Change Amount:
75(or any value you like) - Animation Trigger Name:
StartSleeping(you'll need to set this up in your character's Animator Controller) - Interaction Duration:
8
- Display Name:
Now, every time a player places a bed from the catalog, the instantiated object will be inherently "smart" and carry its "Sleep" interaction with it.

4. Integrating with the Player's Controls
The final step is to update your click-to-select system (from Module 6) to use these new components. When the player clicks on an object, we'll check for a SmartObject component and display its available interactions in the radial menu.
Modify your script that handles player clicks (e.g., PlayerInputManager.cs):
// In your player input/click handler script
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
// Check if the clicked object is a SmartObject
var smartObject = hit.collider.GetComponent<SmartObject>();
if (smartObject != null)
{
// Get the player character GameObject
GameObject playerCharacter = GetPlayerCharacter();
// Get the list of available interactions
List<BaseInteraction> availableInteractions = smartObject.GetAvailableInteractions(playerCharacter);
// Populate your radial menu with these interactions
// For each interaction in the list, create a button.
// The button's OnClick event should call interaction.Perform(playerCharacter);
radialMenu.Show(availableInteractions, playerCharacter, hit.point);
}
}
}
}
Your RadialMenu script would need a Show method that dynamically creates buttons.
// A conceptual example for your RadialMenu.cs
public void Show(List<BaseInteraction> interactions, GameObject performer, Vector3 clickPosition)
{
// Clear any existing buttons
// ...
// For each interaction, create a button
foreach (var interaction in interactions)
{
var button = Instantiate(buttonPrefab, transform);
button.GetComponentInChildren<Text>().text = interaction.DisplayName;
button.onClick.AddListener(() => {
interaction.Perform(performer);
Hide(); // Hide the menu after selection
});
}
// Position the menu at the click position and make it visible
// ...
}
Conclusion
Excellent work! You have now fully connected the build/buy system with the character simulation. Objects placed by the player are no longer just scenery; they are functional items that directly influence the game's state. This creates a deeply satisfying gameplay loop where player creativity (building) directly impacts the simulation's outcomes (living).
Key Takeaways:
- You implemented the Smart Object pattern, where objects advertise their own interactions, creating a modular and extensible system ideal for modding.
- You defined interactions as C# components, allowing you to easily configure them on your object prefabs directly within the Unity Editor.
- You successfully integrated this system with your existing player controls and UI, allowing players to click on placed objects and choose from a context-sensitive menu of actions.
- Most importantly, you closed the loop by having these Unity-side interactions trigger state changes in the Rust backend, ensuring our core simulation remains the authoritative source of truth.
Preview of the Next Lesson:
Our game world is becoming more dynamic. We have characters with needs and objects that can fulfill them. But what happens when you close the game? Everything resets. In our next module, we will tackle one of the most critical features of any simulation game: implementing a robust save/load system. We will design a unified GameState struct in Rust and create the functions to serialize the entire state of the world—including every placed object and every character's status—into a single JSON file and load it back.