Overriding Default GameState
The game_state function is a crucial component that provides AI agents with essential information about the current game environment. This section explains how to customize the GameState to expose additional context to your AI agents.
Understanding the Default GameState
By default, the SDK provides a minimal GameState implementation with the following fields:
[Serializable]
private class DummyGameState
{
[SerializeField] private bool _can_interact = true;
[SerializeField] private string _hint_key = SceneManager.GetActiveScene().name;
[SerializeField] private string current_scene = SceneManager.GetActiveScene().name;
}_can_interact: A boolean value that indicates whether the agent can interact with the game. Iffalse, the agent will poll until the environment is ready for interaction again._hint_key: A string value used to expose hints to the agent. By default, it is set to the current scene name.current_scene: A string value that stores the current scene name, that the agent can actually see.
Fields prefixed with an underscore (e.g., _can_interact) are private and used internally by the framework. They are NOT exposed directly to the agent.
The default implementation is registered as a volatile function, allowing for easy override:
[FlayerFunction("game_state", "returns minimal game state needed", volatileFunction: true)]
[Preserve]
public static string HandleGameState(PacketLogger logger)
{
var gameState = new DummyGameState();
return JsonUtility.ToJson(gameState);
}Creating a Custom GameState
To provide richer context to your AI agent, you can override the default implementation with a custom one that includes game-specific information.
Here’s a comprehensive example of a custom GameState implementation:
#if USE_NUNU_SDK
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Nunu.Flayer;
using Nunu.Network.Logging;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace NunuFunctions
{
[FlayerFunction("game_state", "returns the actual game state")]
[Preserve]
public static string HandleGameState(PacketLogger packetLogger)
{
// create a new empty json object
var gameState = new JObject();
// set can interact & hint key
gameState["can_interact"] = GameManager.Instance != null;
gameState["hint_key"] = $"{SceneManager.GetActiveScene().name}-{GameManager.Instance.GetLevelName()}-{MenuManager.Instance.GetMenuName() ?? "NO_MENU_OPEN"}";
// add custom fields that are exposed to the agent
gameState["player_data"] = GetPlayerData();
gameState["inventory"] = GetInventory();
return JsonConvert.SerializeObject(gameState);
}
}
#endifHint Key
The hint key is used to expose hints to the agent. By default, it is set to the current scene name. You can override this value to expose different hints to the agent.
Usually it makes sense to concat different state values to create a unique hint key. Some useful values to include are:
- The current scene name.
- Current level name
- In Combat/Out of Combat
- Menu Open/Close
- Dead or Alive
- Role/Class/Character Name
this way you can define hints that only are shown when for example the player is in combat or in a specific level.
For Instance MainHub_NON_COMBAT_INVENTORY_OPEN_WIZARD let’s you write a hint that only applies to the main hub, when the player is not in combat, has the inventory open and is playing as a wizard.
Because we are using regex matching for hint keys, you can create flexible patterns:
.*INVENTORY_OPEN_WIZARD.*to match all wizard inventories regardless of location or combat stateMainHub_.*(WIZARD|WARRIOR)to define hints for wizards and warriors, but only in the main hub.*COMBAT_.*to create hints that apply in any combat situation across all scenes
Additional Fields
Additional fields can be added to reduce repetitive lookups in the agent. For example, you can expose the player’s health, inventory, or other game-specific data to the agent.
They can also be used to expose parameters that are needed to execute some of the action flayer functions, like:
- If you have a
navigate_to(x, y, z)function, expose coordinates of important locations in the game state ("quest_marker": {"x": 120.5, "y": 10.0, "z": 45.8}) - For an
equip_item(item_id)function, include a list of available items with their IDs ("equipment_options": [{"id": "sword_01", "name": "Iron Sword"}, ...]) - If you have a
cast_spell(spell_id, target_id)function, provide both spell IDs and potential target IDs
By including this contextual information, you help the agent make more informed decisions.
Best Practices
To verify your GameState override is working correctly:
- Use the Flayer Debugger to inspect the returned JSON
- Check that all expected fields are present and correctly formatted
- Verify that the hint key changes appropriately as game conditions change
Example
#if USE_NUNU_SDK
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Nunu.Flayer;
using Nunu.Network.Logging;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace NunuFunctions
{
public static class CustomGameState
{
[FlayerFunction("game_state", "Returns detailed game state with player and environment information")]
[Preserve]
public static string HandleGameState(PacketLogger logger)
{
// Create a JSON object to hold our game state
var gameState = new JObject();
// Required framework fields
gameState["_can_interact"] = GameManager.Instance != null && !Player.Instance.IsInCutscene;
// Build a comprehensive hint key
string currentMenu = UIManager.Instance.CurrentMenuName ?? "NO_MENU";
string combatState = CombatSystem.Instance.InCombat ? "IN_COMBAT" : "PEACEFUL";
string playerState = Player.Instance.IsDead ? "DEAD" : "ALIVE";
gameState["_hint_key"] = $"{SceneManager.GetActiveScene().name}_{currentMenu}_{combatState}_{playerState}";
// Visible fields for the agent
gameState["current_scene"] = SceneManager.GetActiveScene().name;
gameState["player_health"] = Player.Instance.CurrentHealth;
gameState["player_max_health"] = Player.Instance.MaxHealth;
gameState["player_level"] = Player.Instance.Level;
gameState["current_objective"] = QuestManager.Instance.CurrentObjectiveDescription;
// Add nearby interactive objects
gameState["nearby_interactables"] = GetNearbyInteractables();
// Add inventory information
gameState["inventory"] = GetInventoryData();
// Log what we're returning
logger.Log("Providing updated game state to agent", sendToAI: true);
return JsonConvert.SerializeObject(gameState);
}
private static JArray GetNearbyInteractables()
{
var interactables = new JArray();
// Find all interactable objects within range
foreach (var interactable in InteractionSystem.GetNearbyInteractables(5.0f))
{
interactables.Add(new JObject
{
["id"] = interactable.Id,
["type"] = interactable.Type,
["name"] = interactable.DisplayName,
["distance"] = Vector3.Distance(Player.Instance.transform.position, interactable.transform.position)
});
}
return interactables;
}
private static JObject GetInventoryData()
{
var inventory = new JObject();
// Add equipped items
var equipped = new JObject();
foreach (var slot in InventorySystem.Instance.EquippedSlots)
{
if (slot.Value?.Item != null)
{
equipped[slot.Key] = slot.Value.Item.ItemId;
}
else
{
equipped[slot.Key] = null;
}
}
inventory["equipped"] = equipped;
// Add key inventory items
var items = new JArray();
foreach (var item in InventorySystem.Instance.GetImportantItems())
{
items.Add(new JObject
{
["id"] = item.ItemId,
["name"] = item.DisplayName,
["count"] = item.Count
});
}
inventory["key_items"] = items;
return inventory;
}
}
}
#endif