Skip to main content

Welcome to a Grasp lesson

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

Lesson 35

Modeling Interactions in JSON

Recommended resources

Hello! Welcome to the third lesson in our Player Interaction Framework module.

In the last lesson, you built an impressive, fully animated radial menu that appears whenever the player selects an interactable object. It was a huge step for the game's user experience, but its options were still hard-coded. Today, we're going to change that.

This lesson focuses on a crucial concept in scalable game design: making our systems data-driven. Your goal is to define interactions as data-driven entries in JSON, specifying their type and associated need changes. By moving our interaction logic out of the code and into a simple text file, we make the game far easier to expand, balance, and—most importantly—mod.

1. The "Smart Objects" Philosophy

Many simulation games, including the one that inspired this project, The Sims, use a design pattern where the characters themselves aren't explicitly programmed to know what to do. Instead, the objects around them "advertise" the actions they afford.

Sims-like Interaction Menu Example
When a player clicks an object in The Sims, the object presents a list of possible interactions. Our radial menu will serve the same purpose, populated with data we define.

This "smart objects, dumb AI" approach is incredibly powerful. To understand why, let's look at a short analysis of the system.

The Genius AI Behind The Sims

This video from the Game Maker's Toolkit channel provides an excellent explanation of the AI behind The Sims. It breaks down the concept of objects 'advertising' what they can do to fulfill a Sim's needs.

Watch the segment from 02:22 to 03:02 to understand the core 'advertisement' concept. Then, watch from 06:52 to 07:44 to see why this data-driven approach is so scalable and perfect for a moddable game like ours.

As the video explains, putting the data on the objects makes the system incredibly extensible. We're going to implement this pattern. Our game objects will simply hold a list of interaction IDs they offer. The details of those interactions—what they're called and what they do—will be stored centrally in a JSON file.


2. Designing the Interaction JSON Schema

JSON (JavaScript Object Notation) is a lightweight, human-readable format for structuring data. It's perfect for configuration files and game data.

JSON Visualizer for Hierarchical Data
This visualizer shows how JSON uses key-value pairs to organize data into nested objects and arrays. We'll use this structure to define our interactions.

We need to decide what information defines an interaction in our game. A good starting point is:

  • id: A unique, machine-readable identifier (e.g., "interaction_eat_snack").
  • displayName: A human-readable string for the UI (e.g., "Grab a Snack").
  • effects: A list of changes to apply to a character's needs if the interaction is performed. Each effect will have:
    • need: The name of the need to change (e.g., "hunger").
    • value: The amount to change the need by.

Let's create our first data file. In your Unity project, create a folder named Resources inside your Assets folder if it doesn't already exist. Inside Resources, create a new text file and name it interactions.json.

Paste the following structure into interactions.json:

{
  "interactions": [
    {
      "id": "interaction_eat_snack_fridge",
      "displayName": "Grab a Snack",
      "effects": [
        { "need": "hunger", "value": 30 }
      ]
    },
    {
      "id": "interaction_nap_sofa",
      "displayName": "Take a Nap",
      "effects": [
        { "need": "energy", "value": 40 }
      ]
    },
    {
      "id": "interaction_watch_tv",
      "displayName": "Watch TV",
      "effects": [
        { "need": "fun", "value": 25 }
      ]
    },
    {
      "id": "interaction_use_toilet",
      "displayName": "Use",
      "effects": [
        { "need": "bladder", "value": 100 }
      ]
    }
  ]
}

This file now serves as a mini-database for all possible interactions in our game.


3. Implementing the Data Structures in C#

To use this JSON data in Unity, we need to create C# classes that match its structure. Unity's JsonUtility can then automatically deserialize the JSON text into instances of these classes.

Simple JSON to Store Game Data in Unity

For a practical example of how to structure game data in JSON and then represent it in C#, this article by Jonathan Yu clearly shows the relationship between a JSON file and the corresponding C# classes.

Read the first part of the article, down to the JSON block for 'items'. Notice how the C# Item class directly maps to the fields in the JSON object, and how the [Serializable] attribute is used. This is exactly the approach we will take.

Now, let's create our own C# scripts. Create a new script named InteractionData.cs and add the following code. This will define the classes that hold our loaded data.

using System; // Required for [Serializable]

// Note: These classes don't need to inherit from MonoBehaviour

[Serializable]
public class NeedEffect
{
    public string need; // e.g., "hunger", "energy"
    public float value;
}

[Serializable]
public class InteractionData
{
    public string id;
    public string displayName;
    public NeedEffect[] effects;
}

// This is a wrapper class because JsonUtility cannot deserialize a top-level array.
[Serializable]
public class InteractionDatabase
{
    public InteractionData[] interactions;
}

4. Loading and Accessing Interaction Data

With the data structures in place, we need a manager to load the interactions.json file at runtime and provide easy access to the data.

Create a new C# script called InteractionDataManager.cs. We'll make this a static class so we can access it from anywhere without needing an instance in the scene.

using UnityEngine;
using System.Collections.Generic;

public static class InteractionDataManager
{
    private static Dictionary<string, InteractionData> _interactionDict;

    // We call this at the start of the game to load all interactions
    public static void LoadInteractions()
    {
        _interactionDict = new Dictionary<string, InteractionData>();
        TextAsset jsonFile = Resources.Load<TextAsset>("interactions");
        
        if (jsonFile == null)
        {
            Debug.LogError("Cannot find 'interactions.json' in Resources folder!");
            return;
        }

        InteractionDatabase db = JsonUtility.FromJson<InteractionDatabase>(jsonFile.text);

        foreach (var interaction in db.interactions)
        {
            if (!_interactionDict.ContainsKey(interaction.id))
            {
                _interactionDict.Add(interaction.id, interaction);
            }
        }
        Debug.Log($"Loaded {_interactionDict.Count} interactions into the database.");
    }

    // This lets us easily retrieve an interaction's data by its ID
    public static InteractionData GetInteraction(string id)
    {
        _interactionDict.TryGetValue(id, out InteractionData data);
        if (data == null)
        {
            Debug.LogWarning($"Interaction with ID '{id}' not found in database.");
        }
        return data;
    }
}

To ensure our data is loaded when the game starts, open your SelectionManager.cs script and add a call to LoadInteractions() in the Start() method.

// In SelectionManager.cs
void Start()
{
    _playerMovement = FindObjectOfType<PlayerMovementController>();
    _radialMenu = FindObjectOfType<RadialMenu>(true);
    
    // Add this line to load the data
    InteractionDataManager.LoadInteractions();
}

5. Linking Objects to Interactions

Now we need to tell our game objects which interactions they "advertise".

  1. Create a new C# script called InteractableObject.cs.

    using UnityEngine;
    using System.Collections.Generic;
    
    public class InteractableObject : MonoBehaviour
    {
        public List<string> availableInteractionIDs;
    }
    
  2. Select an interactable object in your scene (e.g., a Fridge model).

  3. Add the InteractableObject component to it.

  4. In the Inspector, look for the "Available Interaction IDs" list. Set its Size to 1.

  5. For Element 0, enter the ID from your JSON file: interaction_eat_snack_fridge.

  6. Repeat this for other objects, like a sofa (interaction_nap_sofa) and a TV (interaction_watch_tv).


6. Populating the Radial Menu Dynamically

This is the final step where everything comes together. We will modify the SelectionManager to read the interaction IDs from a selected object and use them to build the radial menu.

First, you'll need to make a small change to your RadialMenu.cs script from the last lesson. We need a way to clear old options and a way to add new ones. Your AddEntry method from the tutorial might need to be adjusted to accept a string for the label and a Callback delegate.

Now, let's update SelectionManager.cs. The logic where you call _radialMenu.Open() needs to be expanded.

// In SelectionManager.cs -> Update() -> inside the Raycast check for interactableLayer

// ...
if (Physics.Raycast(ray, out hit, 100f, interactableLayer))
{
    _currentSelection = hit.transform;
    Debug.Log("Selected: " + _currentSelection.name);

    // Get the InteractableObject component from the hit object
    var interactable = _currentSelection.GetComponent<InteractableObject>();

    if (interactable != null && interactable.availableInteractionIDs.Count > 0)
    {
        // First, clear any old options from the menu
        _radialMenu.ClearEntries(); // You will need to implement this method in RadialMenu.cs to destroy old entry objects.

        // Populate the menu with new options based on the object's data
        foreach (string id in interactable.availableInteractionIDs)
        {
            InteractionData data = InteractionDataManager.GetInteraction(id);
            if (data != null)
            {
                // For now, the callback just prints to the console.
                // In the next lesson, this will send a command to Rust.
                System.Action callback = () => { 
                    Debug.Log($"Executing interaction: {data.id}"); 
                };

                // Assumes your RadialMenu has a method like this.
                // The 'null' is a placeholder for an icon texture.
                _radialMenu.AddEntry(data.displayName, null, callback);
            }
        }
        
        // Open the fully populated menu at the mouse position
        _radialMenu.Open(Input.mousePosition);
    }
}
//...

Run your project now. When you click on the fridge, the radial menu should appear with the option "Grab a Snack". Clicking the sofa should show "Take a Nap". You have successfully decoupled your UI from your interaction data!


Conclusion

Excellent work! You have fundamentally improved your game's architecture. By moving interaction definitions into a JSON file, you've created a system that is robust, scalable, and ready for modding.

Key Takeaways:

  • Data-Driven Design: The "advertisement" model, where objects declare what they can do, is a powerful pattern for simulation games.
  • JSON for Game Data: JSON is an ideal format for defining game entities like items, skills, or interactions in a way that both humans and machines can easily read.
  • Decoupling Data and Logic: Your C# code is now concerned with how to show a menu, not what to show. The "what" is defined entirely in your interactions.json file. You can add or change interactions without recompiling your code.

In our next lesson, Send an interaction command to Rust to update the character's needs and mood upon completion, we will finally close the loop. Clicking an option in the radial menu will send a command across the FFI to your Rust backend, which will use the effects data to update the character's needs.