Hello! Welcome to the next lesson in our Player Interaction Framework module.
In the previous lesson, we made a significant architectural improvement by creating a data-driven system for interactions. Your radial menu is now dynamically populated from a central interactions.json file, which is a fantastic step towards making our game scalable and moddable.
However, the menu currently shows every interaction an object offers, regardless of the situation. This can lead to illogical choices, like trying to sit in a chair that's already occupied. Today, we'll fix that.
Our goal is to filter the radial menu to show only contextually relevant interactions. This will make the game feel smarter and more polished by ensuring that the player is only ever presented with valid options.
1. The Concept: Interaction Prerequisites
Right now, our InteractableObject script simply lists all possible interactions. To make the system contextual, we need a way to test if an interaction is valid before we show it to the player. Think of it like a disabled button in a user interface—the functionality exists, but it's not available under the current conditions.
Our system will perform a check for each potential interaction. For a "Sit" interaction on a chair, the check would be: "Is this chair currently empty?" If yes, we show the "Sit" option. If no, we hide it.

To implement this cleanly, we'll adopt a design pattern where each interactable object is responsible for its own state and validation logic. The selection manager won't need to know why an interaction is invalid; it will simply ask the object, "Is this action valid right now?" and trust the object's response.
This diagram illustrates a professional-grade architecture for such a system.

We will create a base class for all interactable objects and use polymorphism to allow each specific type of object (a chair, a fridge, a TV) to provide its own custom validation rules.
2. Evolving the InteractableObject Script
Our current InteractableObject script is just a simple data container. To support contextual logic, we need to upgrade it into a more robust system using inheritance.
First, rename InteractableObject.cs to BaseInteractable.cs. This will serve as the foundation for all objects the player can interact with.
Update the code in BaseInteractable.cs to the following:
using UnityEngine;
using System.Collections.Generic;
public class BaseInteractable : MonoBehaviour
{
// This list still holds all theoretically possible interactions for this object.
public List<string> availableInteractionIDs;
/// <summary>
/// Checks if a specific interaction is currently valid.
/// This is a 'virtual' method, meaning child classes can override it with their own logic.
/// By default, all interactions are considered valid.
/// </summary>
/// <param name="interactionID">The ID of the interaction to test.</param>
/// <returns>True if the interaction is valid, false otherwise.</returns>
public virtual bool IsInteractionValid(string interactionID)
{
// Base implementation assumes the interaction is always valid.
return true;
}
}
Now, let's create a specific interactable for a chair. Create a new C# script named ChairInteractable.cs:
using UnityEngine;
public class ChairInteractable : BaseInteractable
{
// This private field holds the state of this specific chair.
private bool isOccupied = false;
// This 'override' replaces the base class's method with custom logic for chairs.
public override bool IsInteractionValid(string interactionID)
{
// We only care about the "Sit" interaction.
if (interactionID == "interaction_sit")
{
// Only allow sitting if the chair is not occupied.
return !isOccupied;
}
// For any other interaction IDs this object might have,
// we fall back to the default behavior.
return base.IsInteractionValid(interactionID);
}
// Public method to allow other systems to change the chair's state.
public void SetOccupied(bool occupied)
{
isOccupied = occupied;
Debug.Log($"Chair '{gameObject.name}' occupied state set to: {occupied}");
}
}
To apply this:
- Find a chair object in your scene.
- Remove the old
InteractableObject(or nowBaseInteractable) component. - Add the new
ChairInteractablecomponent. - In the
ChairInteractablecomponent's "Available Interaction IDs" list, add the ID for sitting. Let's create a new, more generic one. Open yourinteractions.jsonfile and add:
{
"id": "interaction_sit",
"displayName": "Sit",
"effects": [] // No effects for now
}
- Set the interaction ID in the
ChairInteractablecomponent tointeraction_sit.
3. Updating the SelectionManager to Filter Interactions
Now we modify the SelectionManager to use our new validation logic. The change is small but powerful. Instead of blindly adding every interaction from the list, we'll first ask the object if the interaction is valid.
Open SelectionManager.cs and find the section in your Update() method where you populate the radial menu.
// In SelectionManager.cs -> Update() -> inside the Raycast check
// ...
if (Physics.Raycast(ray, out hit, 100f, interactableLayer))
{
_currentSelection = hit.transform;
// IMPORTANT: We now get the BaseInteractable component.
// This works for ChairInteractable too, because it inherits from BaseInteractable.
var interactable = _currentSelection.GetComponent<BaseInteractable>();
if (interactable != null && interactable.availableInteractionIDs.Count > 0)
{
_radialMenu.ClearEntries();
foreach (string id in interactable.availableInteractionIDs)
{
// THIS IS THE NEW VALIDATION STEP!
if (interactable.IsInteractionValid(id))
{
InteractionData data = InteractionDataManager.GetInteraction(id);
if (data != null)
{
// This callback logic will be expanded in the next lesson.
// For now, let's make it change the chair's state to test our system.
System.Action callback = () => {
Debug.Log($"Executing interaction: {data.id}");
// If this is a chair, tell it it's now occupied.
if (interactable is ChairInteractable chair)
{
chair.SetOccupied(true);
}
};
_radialMenu.AddEntry(data.displayName, null, callback);
}
}
}
// Only open the menu if it has any valid entries.
if (_radialMenu.EntryCount > 0)
{
_radialMenu.Open(Input.mousePosition);
}
else
{
// If there are no valid interactions, make sure the menu is closed.
_radialMenu.Close();
}
}
}
//...
Note: You may need to add a public property EntryCount to your RadialMenu.cs script that returns the number of currently active entries.
4. Testing the Contextual Logic
Run the scene and test the complete flow:
- Click on the chair. The radial menu should appear with the "Sit" option.
- Click "Sit". The callback will fire, printing to the console and calling
chair.SetOccupied(true). - Click on the same chair again. Because
isOccupiedis nowtrue, theIsInteractionValidcheck will fail. The radial menu should not open, or it will be empty. The system correctly recognizes that the context has changed.
To make this fully interactive, you could add an "Stand" interaction that sets isOccupied to false. This reinforces the pattern: each action can modify the state of objects, which in turn affects which future actions are available.
This object-oriented approach is incredibly powerful. You can now create FridgeInteractable that checks if there is food, or a TVInteractable that shows a "Turn Off" interaction only if isTurnedOn is true, all without adding a single if statement to your SelectionManager.
Conclusion
Congratulations! You have just implemented a core feature of intelligent simulation games. Your interaction system is no longer just a simple list; it's a dynamic, context-aware system that responds to the state of the game world.
Key Takeaways:
- Contextual Filtering: By adding a validation step before displaying an interaction, we prevent the player from performing invalid actions.
- Polymorphism for Scalability: Using a
virtualmethod (IsInteractionValid) in a base class (BaseInteractable) allows each type of object to define its own specific rules in a clean, encapsulated way. - Decoupled Logic: The
SelectionManagerremains simple. It doesn't need to know about chairs, fridges, or TVs; it just needs to know aboutBaseInteractableobjects and trust them to manage their own state.
In our final lesson for this module, Send an interaction command to Rust to update the character's needs and mood upon completion, we will take the interactionID from the selected menu option and send it across the FFI to the Rust backend. This will finally connect the player's choice in the UI to a meaningful change in the character's simulation state.