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 scoreQuick 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
| Async | Coroutine | Description |
|---|---|---|
client.Personas.ListAsync() | client.Personas.List(onSuccess, onError) | List all published personas |
Utilities
| Async | Coroutine | Description |
|---|---|---|
client.ValidateLlmKeyAsync(model) | (n/a) | Validate your LLM API key |
Machines
| Async | Coroutine | Description |
|---|---|---|
client.Machines.ListAsync(personaId, userSessionId) | client.Machines.List(personaId, userSessionId, onSuccess, onError) | List available machines for a player |
Chat
| Async | Coroutine | Description |
|---|---|---|
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);
}
));| Exception | Status | When |
|---|---|---|
BadRequestException | 400 | Invalid UUID, missing fields |
AuthenticationException | 401 | Missing or invalid API key |
ForbiddenException | 403 | Not owner or persona unpublished |
NotFoundException | 404 | Resource not found |
RateLimitException | 429 | Rate limit exceeded |
ServerException | 500 | Internal 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}");| Event | Data | When |
|---|---|---|
OnStateChange | FromState, ToState, Decision | State machine transitions to a new state |
OnScoreChange | OldScore, NewScore, Delta | Session score changes |
OnMachineCompleted | Outcome, FinalScore, SessionId | Conversation reaches a terminal state |
OnMachinesUnlocked | Machines (MachineKey, Name) | New machines become available after completion |
OnSessionEnded | Outcome, FinalScore, SessionId | Session 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);
}
}