Unity SDK

Add NPC conversations to your Unity game with the Foil Engine UPM package.

Installation

Install via Unity Package Manager using a git URL. In Unity, go to Window → Package Manager → + → Add package from git URL and enter:

https://github.com/py1218/foilengine-unity.git
💡Tip
Requires Unity 2021.3 or later. The SDK depends on Newtonsoft.Json (included with Unity 2020+).

Quick Start (Async/Await)

Unity 2023+ supports async/await natively:

using FoilEngine;
using FoilEngine.Models;

var client = new FoilEngineClient(
    "pk_live_...",
    llmApiKey: "sk-...",   // your LLM provider API key
    llmModel: "gpt-4o"    // any LiteLLM-supported model
);

// Discover your personas
Persona[] personas = await client.Personas.ListAsync();
Debug.Log(personas[0].Name); // "Grumpy Barista"

// Initialize a session
SessionInit session = await client.Chat.InitSessionAsync(
    personaId: personas[0].Id,
    userSessionId: "player-001",
    playerName: "Alex",
    playerGender: "non-binary"
);
Debug.Log(session.Message); // NPC's greeting

// Send a message
ChatResponse response = await client.Chat.SendMessageAsync(
    personaId: personas[0].Id,
    message: "What do you recommend?",
    userSessionId: "player-001"
);
Debug.Log(response.Message);      // NPC's reply
Debug.Log(response.CurrentState); // State machine state
Debug.Log(response.Score);        // Session score

Quick Start (Coroutines)

For Unity 2021–2022 or if you prefer coroutines:

using UnityEngine;
using FoilEngine;
using FoilEngine.Models;

public class NPCManager : MonoBehaviour
{
    private FoilEngineClient client;

    void Start()
    {
        client = new FoilEngineClient(
            "pk_live_...",
            llmApiKey: "sk-...",
            llmModel: "gpt-4o"
        );
        StartCoroutine(client.Personas.List(
            onSuccess: personas => {
                Debug.Log(personas[0].Name);
                StartCoroutine(InitChat(personas[0].Id));
            },
            onError: err => Debug.LogError(err.Message)
        ));
    }

    IEnumerator InitChat(string personaId)
    {
        yield return client.Chat.InitSession(
            personaId: personaId,
            userSessionId: "player-001",
            playerName: "Alex",
            playerGender: "non-binary",
            onSuccess: session => Debug.Log(session.Message),
            onError: err => Debug.LogError(err.Message)
        );
    }
}

Configuration

var client = new FoilEngineClient(
    apiKey: "pk_live_...",                          // required
    llmApiKey: "sk-...",                            // required for chat endpoints
    llmModel: "gpt-4o",                            // optional – default LLM model
    llmEvalModel: "gpt-4o-mini",                   // optional – model for evaluation
    llmResponseModel: "gpt-4o",                    // optional – model for NPC responses
    llmSummarizationModel: "gpt-4o-mini",          // optional – model for summaries
    llmEvalApiKey: "sk-ant-...",                    // optional – different provider for eval
    llmResponseApiKey: "sk-...",                    // optional – different provider for responses
    llmSummarizationApiKey: "sk-...",               // optional – different provider for summaries
    baseUrl: "http://localhost:8000",               // default: https://api.foilengine.io
    timeout: 30,                                    // default: 30
    maxRetries: 3,                                  // default: 3
    debug: true,                                    // optional – log requests to Unity Console
    cacheTtl: 60                                    // optional – cache Personas.ListAsync() for 60s (0 to disable)
);

Available Methods

Personas

AsyncCoroutineDescription
client.Personas.ListAsync()client.Personas.List(onSuccess, onError)List all published personas

Utilities

AsyncCoroutineDescription
client.ValidateLlmKeyAsync(model)(n/a)Validate your LLM API key

Machines

AsyncCoroutineDescription
client.Machines.ListAsync(personaId, userSessionId)client.Machines.List(personaId, userSessionId, onSuccess, onError)List available machines for a player

Chat

AsyncCoroutineDescription
InitSessionAsync(...)InitSession(..., onSuccess, onError)Start a new conversation
SendMessageAsync(...)SendMessage(..., onSuccess, onError)Send a message, get NPC reply
SendMessageStreamAsync(...)SendMessageStream(..., onEvent, onError)Stream NPC response via SSE (metadata first, then text chunks, then done)
GetSessionAsync(...)GetSession(..., onSuccess, onError)Check if a session exists
GetHistoryAsync(...)GetHistory(..., onSuccess, onError)Get full message history
ResetAsync(...)Reset(..., onSuccess, onError)Delete a session

Error Handling

Async/Await

using FoilEngine;

try
{
    var session = await client.Chat.GetSessionAsync(personaId, "player-001");
}
catch (NotFoundException)
{
    // No existing session — initialize one
    var session = await client.Chat.InitSessionAsync(personaId, "player-001", "Alex", "non-binary");
}
catch (RateLimitException e)
{
    Debug.Log($"Retry after {e.RetryAfter}s");
}
catch (AuthenticationException)
{
    Debug.LogError("Invalid API key");
}

Coroutines

StartCoroutine(client.Chat.GetSession(
    personaId: personaId,
    userSessionId: "player-001",
    onSuccess: session => Debug.Log(session.Exists),
    onError: err => {
        if (err is NotFoundException)
            StartCoroutine(InitNewSession());
        else
            Debug.LogError(err.Message);
    }
));
ExceptionStatusWhen
BadRequestException400Invalid UUID, missing fields
AuthenticationException401Missing or invalid API key
ForbiddenException403Not owner or persona unpublished
NotFoundException404Resource not found
RateLimitException429Rate limit exceeded
ServerException500Internal server error
💡Tip
The SDK automatically retries on 429 and 5xx errors with jittered exponential backoff (up to 3 retries). Uses UnityWebRequest for full platform compatibility (WebGL, mobile, console). Set debug: true to log requests to the Unity Console.

Streaming Responses

Stream NPC responses token-by-token for a typing effect. Metadata (state, score, decision) arrives before any text, so you can update your game UI immediately.

Async/Await

await client.Chat.SendMessageStreamAsync(
    personaId: personaId,
    message: "Tell me about your potions.",
    userSessionId: "player-001",
    onEvent: (evt) =>
    {
        switch (evt.EventType)
        {
            case "metadata":
                Debug.Log($"State: {evt.Metadata.CurrentState}, Score: {evt.Metadata.Score}");
                break;
            case "text_delta":
                // Append to your dialogue UI text
                dialogueText.text += evt.Text;
                break;
            case "done":
                Debug.Log($"Final: {evt.Response.Message}");
                break;
        }
    }
);

Coroutines

StartCoroutine(client.Chat.SendMessageStream(
    personaId: personaId,
    message: "Tell me about your potions.",
    userSessionId: "player-001",
    onEvent: (evt) =>
    {
        if (evt.EventType == "text_delta")
            dialogueText.text += evt.Text;
    },
    onError: err => Debug.LogError(err.Message)
));
Warning
The coroutine version of SendMessageStream buffers the full response before parsing SSE events due to Unity coroutine limitations. For true progressive streaming, use SendMessageStreamAsync (Unity 2023+).

Events

Subscribe to C# events to react to game-relevant state changes. Events fire automatically after each SendMessageAsync() or SendMessageStreamAsync() call.

client.OnStateChange += e =>
    Debug.Log($"State: {e.FromState} -> {e.ToState}");

client.OnScoreChange += e =>
    Debug.Log($"Score: {e.OldScore} -> {e.NewScore} (delta: {e.Delta})");

client.OnMachineCompleted += e =>
    Debug.Log($"Done! Outcome: {e.Outcome}");

client.OnMachinesUnlocked += e =>
    Debug.Log($"Unlocked {e.Machines.Length} new machines");

client.OnSessionEnded += e =>
    Debug.Log($"Session ended: {e.Outcome}");
EventDataWhen
OnStateChangeFromState, ToState, DecisionState machine transitions to a new state
OnScoreChangeOldScore, NewScore, DeltaSession score changes
OnMachineCompletedOutcome, FinalScore, SessionIdConversation reaches a terminal state
OnMachinesUnlockedMachines (MachineKey, Name)New machines become available after completion
OnSessionEndedOutcome, FinalScore, SessionIdSession receives an outcome (ACCEPT, REJECT, KICK_OUT)

Multi-Machine Example

// Discover available machines for a player
MachineInfo[] machines = await client.Machines.ListAsync(personaId, "player-001");

foreach (var m in machines)
{
    string status = m.HasSession ? "active" : "available";
    string linked = m.IsLinked ? " (linked)" : "";
    Debug.Log($"  {m.Name}: {status}{linked}");
}

// Start a specific machine
SessionInit session = await client.Chat.InitSessionAsync(
    personaId: personaId,
    userSessionId: "player-001",
    playerName: "Alex",
    playerGender: "non-binary",
    machineId: machines[1].MachineId  // target a specific scenario
);

Complete Example

A full MonoBehaviour that initializes a session, sends messages with streaming, and handles events:

using UnityEngine;
using UnityEngine.UI;
using FoilEngine;
using FoilEngine.Models;

public class NPCDialogue : MonoBehaviour
{
    [Header("Configuration")]
    [SerializeField] private string apiKey = "pk_live_...";
    [SerializeField] private string llmApiKey = "sk-...";
    [SerializeField] private string llmModel = "gpt-4o";

    [Header("UI")]
    [SerializeField] private Text dialogueText;
    [SerializeField] private Text stateText;

    private FoilEngineClient client;
    private string personaId;
    private string sessionId = "player-001";

    async void Start()
    {
        client = new FoilEngineClient(
            apiKey, llmApiKey: llmApiKey, llmModel: llmModel
        );

        // Subscribe to events
        client.OnStateChange += e =>
            Debug.Log($"State: {e.FromState} -> {e.ToState}");
        client.OnScoreChange += e =>
            Debug.Log($"Score: {e.OldScore} -> {e.NewScore}");
        client.OnMachineCompleted += e =>
            Debug.Log($"Done! Outcome: {e.Outcome}");

        // Discover personas
        Persona[] personas = await client.Personas.ListAsync();
        personaId = personas[0].Id;

        // Initialize session
        SessionInit session = await client.Chat.InitSessionAsync(
            personaId: personaId,
            userSessionId: sessionId,
            playerName: "Alex",
            playerGender: "non-binary"
        );
        dialogueText.text = session.Message;
    }

    public async void SendMessage(string message)
    {
        dialogueText.text = "";

        // Stream the response for a typing effect
        await client.Chat.SendMessageStreamAsync(
            personaId: personaId,
            message: message,
            userSessionId: sessionId,
            onEvent: (evt) =>
            {
                switch (evt.EventType)
                {
                    case "metadata":
                        stateText.text = $"State: {evt.Metadata.CurrentState}";
                        break;
                    case "text_delta":
                        dialogueText.text += evt.Text;
                        break;
                }
            }
        );
    }

    void OnDestroy()
    {
        // Clean up: reset session when leaving scene
        if (client != null && personaId != null)
            _ = client.Chat.ResetAsync(personaId, sessionId);
    }
}