36 KiB
GumYum NPC SDK for Unity
Welcome to the GumYum NPC SDK for Unity! This SDK allows you to integrate AI-powered NPCs with deterministic spawning and dynamic dialogue into your Unity games.
Features
- API Key Authentication: Simple setup with API keys from the GumYum dashboard
- Deterministic NPC Spawning: Spawn the same NPCs with the same seed
- Filtered Spawning: Spawn NPCs based on profession, location, personality type, and more
- OpenAI-Compatible Chat: Use familiar chat completion APIs
- Mood System: NPCs have dynamic moods that change based on conversations
- Streaming Support: Real-time streaming of NPC responses
- Unity Coroutine Support: Seamless integration with Unity's async patterns
- Conversation History: Maintain context across multiple interactions
Quick Start
1. Installation
- Import the GumYum NPC package from the Unity Asset Store
- The package will be installed in
Assets/GumYumNPC/
2. Get API Credentials
- Sign up at https://npc.gumyum.com
- Navigate to your dashboard
- Create a new API key pair
- Copy your public and secret keys
3. Basic Setup
Add the GumYumUnityClient to your scene:
using GumYum.NPC.Unity;
public class GameManager : MonoBehaviour
{
private GumYumUnityClient client;
void Start()
{
// Get the singleton instance
client = GumYumUnityClient.Instance;
// Set your API credentials
client.SetCredentials("your-public-key", "your-secret-key");
// Start spawning NPCs!
StartCoroutine(SpawnMyFirstNPC());
}
IEnumerator SpawnMyFirstNPC()
{
// Spawn a random NPC from the "blade-runner" universe
yield return client.SpawnNPCCoroutine("blade-runner", 42, null, npc =>
{
if (npc != null)
{
Debug.Log($"Spawned {npc.Name} the {npc.Profession}!");
}
});
}
}
4. Chat with NPCs
IEnumerator ChatWithNPC(NPC npc, string message)
{
yield return client.SimpleChatCoroutine(npc, message, response =>
{
if (response != null)
{
Debug.Log($"{npc.Name}: {response}");
}
});
}
Examples
The SDK includes several example scenes in the Samples~ folder:
- Basic Chat: Simple NPC spawning and chat interface
- Advanced Features: Filtered spawning, mood transitions, and streaming
To import samples:
- Open Package Manager (Window > Package Manager)
- Find GumYum NPC SDK
- Click on "Samples" and import the examples you want
Advanced Usage
Filtered NPC Spawning
// Spawn a female warrior or knight from the barracks
var filters = new Dictionary<string, object>
{
["profession"] = new[] { "warrior", "knight" },
["location"] = new[] { "barracks" },
["sex"] = new[] { "female" }
};
yield return client.SpawnFilteredNPCCoroutine("kingdom", 12345, filters, npc =>
{
if (npc != null)
{
Debug.Log($"Spawned filtered NPC: {npc.GetInfo()}");
}
});
Mood Transitions
NPCs have dynamic moods that change based on conversations:
// Check mood before conversation
string previousMood = npc.GetMood();
// Have a conversation...
yield return ChatWithNPC(npc, "I have terrible news!");
// Check if mood changed
if (npc.HasMoodChanged())
{
Debug.Log($"Mood changed from {npc.GetPreviousMood()} to {npc.GetMood()}");
Debug.Log($"Reason: {npc.GetLastMoodChangeReason()}");
}
Streaming Responses
For real-time chat experiences:
var messages = new List<ChatMessage>
{
new ChatMessage { Role = "user", Content = "Tell me a story" }
};
// Enable streaming in the chat call
yield return client.ChatCoroutine(npc, messages, response =>
{
// Final response received
}, temperature: 0.8, maxTokens: 2000);
// Listen to streaming events
npc.DialogueChunkReceived += (sender, chunk) =>
{
// Update UI with each chunk as it arrives
chatText.text += chunk;
};
Async/Await Support
The SDK also supports modern C# async patterns:
async Task SpawnAndChatAsync()
{
var npc = await client.NPCs.SpawnAsync("blade-runner", 42);
var messages = new List<ChatMessage>
{
new ChatMessage { Role = "user", Content = "Hello!" }
};
var response = await npc.Chat.CompletionsAsync(messages);
Debug.Log(response.Choices[0].Message.Content);
}
API Reference
GumYumUnityClient
SetCredentials(apiKey, apiSecret): Set API credentialsSpawnNPCCoroutine(universeId, seed, npcId, callback): Spawn an NPCSpawnFilteredNPCCoroutine(universeId, seed, filters, callback): Spawn filtered NPCChatCoroutine(npc, messages, callback, temperature, maxTokens): Chat with NPCSimpleChatCoroutine(npc, message, callback, temperature, maxTokens): Simple chat helper
NPC Class
Name,Profession,PersonalityType: NPC propertiesGetMood(),GetStressLevel(): Current stateChat.CompletionsAsync(messages): Chat with the NPCChatWithHistoryAsync(message): Chat with conversation historySaveToServerAsync(customName): Save NPC for later retrieval
Best Practices
- Singleton Usage: Use
GumYumUnityClient.Instancefor a single client across your game - Error Handling: Always check if NPCs are null after spawning
- API Keys: Never commit API keys to source control. Use environment variables or Unity's secret management
- Coroutines: Use coroutines for better Unity integration, especially for UI updates
- Mood Awareness: Design conversations considering NPC mood states
Troubleshooting
NPCs not spawning
- Check your API credentials are correct
- Ensure you have an active internet connection
- Check the Unity console for error messages
Mood not changing
- Not all messages trigger mood changes
- Mood transitions depend on NPC personality and context
- Check
HasMoodChanged()after conversations
Performance issues
- Use appropriate max_tokens limits
- Consider implementing response caching
- Use streaming for long responses
Support
- Documentation: https://docs.gumyum.com/sdk/unity
- Discord: https://discord.gg/gumyum
- Email: support@gumyum.com
License
This SDK is licensed under the MIT License. See LICENSE file for details.
Enterprise Documentation
The following sections provide in-depth coverage for production game development.
Persistence & Save Systems
NPCs automatically track conversation history. Save and load NPCs with their complete state:
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using GumYum.NPC;
public class NPCPersistenceManager : MonoBehaviour
{
private Dictionary<int, NPC> activeNPCs = new Dictionary<int, NPC>();
private Client client;
void Start()
{
// Initialize client
client = new Client()
{
ApiKey = "your-key",
ApiSecret = "your-secret"
};
}
// AUTOMATIC HISTORY TRACKING
async void DemonstrateHistoryTracking()
{
// Spawn an NPC
var npc = await client.NPCs.SpawnAsync("kingdom", 42, 123456789L);
// Every chat interaction is automatically saved to npc.ChatHistory
// Method 1: Manual message list with automatic tracking
var response1 = await npc.Chat.CompletionsAsync(new List<ChatMessage>
{
new ChatMessage { Role = "user", Content = "Hello there!" }
});
// npc.ChatHistory now contains both user message and NPC response
// Method 2: Use ChatWithHistoryAsync() for convenience
// This automatically includes all previous messages
var response2 = await npc.ChatWithHistoryAsync("What goods do you sell?");
var response3 = await npc.ChatWithHistoryAsync("How much for a healing potion?");
// Check conversation history
Debug.Log($"Total messages: {npc.ChatHistory.Count}");
foreach (var msg in npc.ChatHistory)
{
string role = msg.Role == "user" ? "Player" : npc.Name;
Debug.Log($"{role}: {msg.Content}");
}
// SAVE NPC WITH FULL CONVERSATION
SaveNPC(npc);
}
// SAVE SYSTEM INTEGRATION
void SaveNPC(NPC npc)
{
// Convert to dictionary (useful for custom storage)
var npcData = npc.ToDict();
// Contains: npc_id, name, profession, personality_type, universe_id,
// seed, spawned data, cached status, and full chat_history
// Convert to JSON string
string jsonStr = npc.ToJson();
// Save to PlayerPrefs (simple approach)
PlayerPrefs.SetString($"NPC_{npc.NpcId}", jsonStr);
PlayerPrefs.Save();
// Or save to file (persistent data path)
string filePath = Path.Combine(Application.persistentDataPath, $"npc_{npc.NpcId}.json");
File.WriteAllText(filePath, jsonStr);
Debug.Log($"Saved {npc.Name} with {npc.ChatHistory.Count} messages");
}
// LOAD AND CONTINUE CONVERSATIONS
NPC LoadNPC(int npcId)
{
// Load from PlayerPrefs
string jsonStr = PlayerPrefs.GetString($"NPC_{npcId}", "");
if (string.IsNullOrEmpty(jsonStr))
{
// Try loading from file
string filePath = Path.Combine(Application.persistentDataPath, $"npc_{npcId}.json");
if (File.Exists(filePath))
{
jsonStr = File.ReadAllText(filePath);
}
}
if (!string.IsNullOrEmpty(jsonStr))
{
// Restore NPC with full conversation history
var loadedNPC = NPC.FromJson(jsonStr, client);
activeNPCs[npcId] = loadedNPC;
Debug.Log($"Loaded {loadedNPC.Name} with {loadedNPC.ChatHistory.Count} messages");
return loadedNPC;
}
return null;
}
// CONTINUE CONVERSATION AFTER LOADING
async void ContinueConversation(int npcId)
{
var npc = LoadNPC(npcId);
if (npc != null)
{
// Continue from where we left off
var response = await npc.ChatWithHistoryAsync("Do you remember our last conversation?");
Debug.Log($"{npc.Name}: {response.Choices[0].Message.Content}");
// NPC will reference previous conversation naturally
}
}
// UNITY SAVE GAME EXAMPLE
[System.Serializable]
public class GameSaveData
{
public string saveSlot;
public float timestamp;
public PlayerData player;
public Dictionary<string, string> npcs = new Dictionary<string, string>();
}
[System.Serializable]
public class PlayerData
{
public int level;
public Vector3 position;
public int health;
}
public void SaveGame(string slotName)
{
var saveData = new GameSaveData
{
saveSlot = slotName,
timestamp = Time.time,
player = new PlayerData
{
level = 10,
position = transform.position,
health = 100
}
};
// Save each active NPC with their conversation
foreach (var kvp in activeNPCs)
{
saveData.npcs[kvp.Key.ToString()] = kvp.Value.ToJson();
}
// Serialize game save
string saveJson = JsonUtility.ToJson(saveData, true);
string savePath = Path.Combine(Application.persistentDataPath, $"save_{slotName}.json");
File.WriteAllText(savePath, saveJson);
Debug.Log($"Saved game to slot: {slotName}");
Debug.Log($"- {activeNPCs.Count} NPCs with conversations");
}
public void LoadGame(string slotName)
{
string savePath = Path.Combine(Application.persistentDataPath, $"save_{slotName}.json");
if (!File.Exists(savePath))
{
Debug.LogError($"Save file not found: {slotName}");
return;
}
string saveJson = File.ReadAllText(savePath);
var saveData = JsonUtility.FromJson<GameSaveData>(saveJson);
// Restore player state
transform.position = saveData.player.position;
// ... restore other player data
// Restore NPCs with their conversations
activeNPCs.Clear();
foreach (var kvp in saveData.npcs)
{
int npcId = int.Parse(kvp.Key);
var npc = NPC.FromJson(kvp.Value, client);
activeNPCs[npcId] = npc;
Debug.Log($"Loaded {npc.Name} with {npc.ChatHistory.Count} messages");
}
Debug.Log($"Loaded game from slot: {slotName}");
}
// SCRIPTABLEOBJECT APPROACH (Unity-specific)
[CreateAssetMenu(fileName = "NPCData", menuName = "GumYum/NPC Data")]
public class NPCDataAsset : ScriptableObject
{
public string npcJson;
public int messageCount;
public string lastUpdated;
public void SaveNPC(NPC npc)
{
npcJson = npc.ToJson();
messageCount = npc.ChatHistory.Count;
lastUpdated = System.DateTime.Now.ToString();
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
public NPC LoadNPC(Client client)
{
if (!string.IsNullOrEmpty(npcJson))
{
return NPC.FromJson(npcJson, client);
}
return null;
}
}
}
Modern Async/Await Patterns
Use C# async/await for cleaner code and better performance:
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using UnityEngine;
using GumYum.NPC;
// Modern C# async/await patterns for Unity 2021.3+
public class AsyncNPCManager : MonoBehaviour
{
private Client client;
private Dictionary<int, NPC> activeNPCs = new Dictionary<int, NPC>();
async void Start()
{
// Initialize client
client = new Client()
{
ApiKey = "your-key",
ApiSecret = "your-secret",
DebugMode = true
};
try
{
// Spawn NPCs concurrently
await SpawnMultipleNPCsAsync();
// Chat with all NPCs
await ChatWithAllNPCsAsync("Hello everyone!");
}
catch (Exception e)
{
Debug.LogError($"Failed to initialize NPCs: {e.Message}");
}
}
// CONCURRENT NPC SPAWNING
async Task SpawnMultipleNPCsAsync()
{
var npcIds = new[] { 1001, 1002, 1003, 2001, 2002 };
var spawnTasks = new List<Task<NPC>>();
// Start all spawn tasks
foreach (long id in npcIds)
{
var task = client.NPCs.SpawnAsync("kingdom", 42, id);
spawnTasks.Add(task);
}
// Wait for all to complete
var npcs = await Task.WhenAll(spawnTasks);
// Store spawned NPCs
foreach (var npc in npcs)
{
activeNPCs[npc.NpcId] = npc;
Debug.Log($"Spawned: {npc.Name} - {npc.Profession}");
}
}
// CHAT WITH MULTIPLE NPCS
async Task ChatWithAllNPCsAsync(string message)
{
var chatTasks = new List<Task<ChatCompletionResponse>>();
// Start chat tasks for all NPCs
foreach (var npc in activeNPCs.Values)
{
var task = npc.ChatWithHistoryAsync(message);
chatTasks.Add(task);
}
// Wait for all responses
var responses = await Task.WhenAll(chatTasks);
// Process responses
int i = 0;
foreach (var npc in activeNPCs.Values)
{
var response = responses[i++];
Debug.Log($"{npc.Name}: {response.Choices[0].Message.Content}");
if (npc.HasMoodChanged())
{
Debug.Log($" Mood: {npc.GetPreviousMood()} → {npc.GetMood()}");
}
}
}
// FILTERED SPAWNING WITH RETRY
async Task<NPC> SpawnFilteredWithRetryAsync(
string universeId,
int seed,
object filters,
int maxRetries = 3)
{
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
var npc = await client.NPCs.SpawnFilteredAsync(universeId, seed, filters);
return npc;
}
catch (Exception e)
{
Debug.LogWarning($"Spawn attempt {attempt + 1} failed: {e.Message}");
if (attempt == maxRetries - 1)
throw;
// Wait before retry with exponential backoff
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
}
}
return null;
}
// ERROR HANDLING PATTERNS
async Task SafeNPCInteractionAsync()
{
try
{
// Try to spawn in non-existent universe
var npc = await client.NPCs.SpawnAsync("unknown-universe", 12345, 1L);
}
catch (System.Net.Http.HttpRequestException httpEx)
{
// Handle API errors
Debug.LogError($"API Error: {httpEx.Message}");
// Show user-friendly message
ShowErrorUI("Failed to connect to NPC service. Please check your connection.");
}
catch (TaskCanceledException)
{
// Handle timeout
Debug.LogError("Request timed out");
ShowErrorUI("The request took too long. Please try again.");
}
catch (Exception ex)
{
// Handle unexpected errors
Debug.LogError($"Unexpected error: {ex.Message}");
ShowErrorUI("Something went wrong. Please try again later.");
}
}
// CANCELLATION SUPPORT
private System.Threading.CancellationTokenSource cts;
async Task CancellableOperationAsync()
{
cts = new System.Threading.CancellationTokenSource();
try
{
// Set a timeout
cts.CancelAfter(TimeSpan.FromSeconds(30));
// Pass cancellation token to async operations
var npc = await client.NPCs.SpawnAsync(
"kingdom", 42, null,
cancellationToken: cts.Token
);
var response = await npc.ChatWithHistoryAsync(
"Tell me a long story",
cancellationToken: cts.Token
);
}
catch (OperationCanceledException)
{
Debug.Log("Operation was cancelled");
}
}
void OnDestroy()
{
// Cancel any pending operations
cts?.Cancel();
cts?.Dispose();
// Dispose of the client
client?.Dispose();
}
void ShowErrorUI(string message)
{
// Your UI error display logic
Debug.LogError($"UI Error: {message}");
}
}
// UNITY-SPECIFIC ASYNC HELPERS
public static class UnityAsyncHelpers
{
// Convert async operation to Unity Coroutine
public static IEnumerator AsCoroutine(this Task task)
{
while (!task.IsCompleted)
{
yield return null;
}
if (task.IsFaulted)
{
throw task.Exception.GetBaseException();
}
}
// Await with timeout
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout)
{
using (var cts = new System.Threading.CancellationTokenSource())
{
var delayTask = Task.Delay(timeout, cts.Token);
var completedTask = await Task.WhenAny(task, delayTask);
if (completedTask == delayTask)
{
throw new TimeoutException("Operation timed out");
}
cts.Cancel(); // Cancel the delay task
return await task;
}
}
}
Unity Editor Integration
Custom inspectors and editor windows for easier development:
// Custom Unity Editor tools for GumYum NPCs
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using GumYum.NPC;
using GumYum.NPC.Unity;
// CUSTOM INSPECTOR FOR NPCS
[CustomEditor(typeof(NPCBehaviour))]
public class NPCInspector : Editor
{
private NPCBehaviour npcBehaviour;
void OnEnable()
{
npcBehaviour = (NPCBehaviour)target;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space();
EditorGUILayout.LabelField("NPC Information", EditorStyles.boldLabel);
if (Application.isPlaying && npcBehaviour.CurrentNPC != null)
{
var npc = npcBehaviour.CurrentNPC;
EditorGUILayout.LabelField("Name:", npc.Name);
EditorGUILayout.LabelField("Profession:", npc.Profession);
EditorGUILayout.LabelField("Personality Type:", npc.PersonalityType.ToString());
EditorGUILayout.Space();
EditorGUILayout.LabelField("Current State", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Mood:", npc.GetMood());
EditorGUILayout.LabelField("Stress Level:", npc.GetStressLevel().ToString());
EditorGUILayout.LabelField("Location:", npc.GetLocation());
if (npc.HasMoodChanged())
{
EditorGUILayout.HelpBox(
$"Mood changed: {npc.GetPreviousMood()} → {npc.GetMood()}\n" +
$"Reason: {npc.GetLastMoodChangeReason()}",
MessageType.Info
);
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Chat History", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Messages: {npc.ChatHistory.Count}");
if (GUILayout.Button("Save NPC to Asset"))
{
SaveNPCAsset(npc);
}
if (GUILayout.Button("Export Chat History"))
{
ExportChatHistory(npc);
}
}
else if (Application.isPlaying)
{
EditorGUILayout.HelpBox("No NPC spawned yet", MessageType.Info);
}
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("Enter Play Mode to spawn NPCs", MessageType.Info);
}
}
void SaveNPCAsset(NPC npc)
{
var asset = CreateInstance<NPCDataAsset>();
asset.SaveNPC(npc);
string path = EditorUtility.SaveFilePanelInProject(
"Save NPC Data",
$"NPC_{npc.Name}_{npc.NpcId}",
"asset",
"Save NPC data as ScriptableObject asset"
);
if (!string.IsNullOrEmpty(path))
{
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
EditorUtility.DisplayDialog("Success", "NPC data saved!", "OK");
}
}
void ExportChatHistory(NPC npc)
{
string path = EditorUtility.SaveFilePanel(
"Export Chat History",
"",
$"{npc.Name}_chat_history",
"txt"
);
if (!string.IsNullOrEmpty(path))
{
var content = new System.Text.StringBuilder();
content.AppendLine($"Chat History with {npc.Name}");
content.AppendLine($"Universe: {npc.UniverseId}");
content.AppendLine($"NPC ID: {npc.NpcId}");
content.AppendLine("=" + new string('=', 50));
content.AppendLine();
foreach (var msg in npc.ChatHistory)
{
string speaker = msg.Role == "user" ? "Player" : npc.Name;
content.AppendLine($"{speaker}: {msg.Content}");
content.AppendLine();
}
System.IO.File.WriteAllText(path, content.ToString());
EditorUtility.DisplayDialog("Success", "Chat history exported!", "OK");
}
}
}
// GUMYUM SETTINGS WINDOW
public class GumYumSettingsWindow : EditorWindow
{
private string apiKey = "";
private string apiSecret = "";
private bool showSecret = false;
private Vector2 scrollPos;
[MenuItem("Window/GumYum NPC/Settings")]
public static void ShowWindow()
{
var window = GetWindow<GumYumSettingsWindow>("GumYum NPC Settings");
window.minSize = new Vector2(400, 300);
}
void OnEnable()
{
// Load saved credentials
apiKey = EditorPrefs.GetString("GumYum_APIKey", "");
apiSecret = EditorPrefs.GetString("GumYum_APISecret", "");
}
void OnGUI()
{
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
GUILayout.Label("GumYum NPC SDK Settings", EditorStyles.boldLabel);
GUILayout.Space(10);
// API Credentials Section
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("API Credentials", EditorStyles.boldLabel);
GUILayout.Label("Get your API keys from https://npc.gumyum.com", EditorStyles.miniLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("API Key:", GUILayout.Width(80));
apiKey = EditorGUILayout.TextField(apiKey);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("API Secret:", GUILayout.Width(80));
if (showSecret)
{
apiSecret = EditorGUILayout.TextField(apiSecret);
}
else
{
apiSecret = EditorGUILayout.PasswordField(apiSecret);
}
showSecret = GUILayout.Toggle(showSecret, "Show", GUILayout.Width(50));
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
if (GUILayout.Button("Save Settings"))
{
SaveSettings();
}
EditorGUILayout.EndVertical();
GUILayout.Space(10);
// Test Connection
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Test Connection", EditorStyles.boldLabel);
if (GUILayout.Button("Test API Connection"))
{
TestConnection();
}
EditorGUILayout.EndVertical();
GUILayout.Space(10);
// Quick Actions
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("Quick Actions", EditorStyles.boldLabel);
if (GUILayout.Button("Create Chat Demo Scene"))
{
CreateChatDemoScene();
}
if (GUILayout.Button("Open Documentation"))
{
Application.OpenURL("https://docs.gumyum.com/sdk/unity");
}
if (GUILayout.Button("Get API Keys"))
{
Application.OpenURL("https://npc.gumyum.com");
}
EditorGUILayout.EndVertical();
GUILayout.Space(10);
// Info box
EditorGUILayout.HelpBox(
"These credentials are saved in Editor preferences and are not included in builds. " +
"For production, set credentials at runtime or use environment variables.",
MessageType.Info
);
EditorGUILayout.EndScrollView();
}
void SaveSettings()
{
EditorPrefs.SetString("GumYum_APIKey", apiKey);
EditorPrefs.SetString("GumYum_APISecret", apiSecret);
EditorUtility.DisplayDialog("Success", "Settings saved!", "OK");
// If in play mode, update the singleton
if (Application.isPlaying && GumYumUnityClient.Instance != null)
{
GumYumUnityClient.Instance.SetCredentials(apiKey, apiSecret);
}
}
async void TestConnection()
{
if (string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(apiSecret))
{
EditorUtility.DisplayDialog("Error", "Please enter API credentials first", "OK");
return;
}
EditorUtility.DisplayProgressBar("Testing Connection", "Connecting to GumYum API...", 0.5f);
try
{
var client = new Client()
{
ApiKey = apiKey,
ApiSecret = apiSecret
};
// Try to list public universes
var universes = await client.Universes.ListPublicAsync();
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog(
"Success",
$"Connection successful!\nFound {universes.Count} public universes.",
"OK"
);
}
catch (Exception e)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog(
"Connection Failed",
$"Failed to connect: {e.Message}",
"OK"
);
}
}
void CreateChatDemoScene()
{
// Implementation to create a demo scene with UI setup
EditorUtility.DisplayDialog(
"Demo Scene",
"Check the Samples folder for example scenes!",
"OK"
);
}
}
// Example MonoBehaviour for NPCs
public class NPCBehaviour : MonoBehaviour
{
[HideInInspector]
public NPC CurrentNPC { get; private set; }
public async void SpawnNPC(string universeId, int seed, long? npcId = null)
{
var client = GumYumUnityClient.Instance.Client;
CurrentNPC = await client.NPCs.SpawnAsync(universeId, seed, npcId);
}
}
// ScriptableObject for NPC data persistence
[CreateAssetMenu(fileName = "NPCData", menuName = "GumYum/NPC Data")]
public class NPCDataAsset : ScriptableObject
{
[TextArea(10, 20)]
public string npcJson;
public int messageCount;
public string lastUpdated;
public void SaveNPC(NPC npc)
{
npcJson = npc.ToJson();
messageCount = npc.ChatHistory.Count;
lastUpdated = System.DateTime.Now.ToString();
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
public NPC LoadNPC(Client client)
{
return !string.IsNullOrEmpty(npcJson) ? NPC.FromJson(npcJson, client) : null;
}
}
Advanced Streaming Implementation
Real-time streaming with full event handling:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using GumYum.NPC;
using GumYum.NPC.Unity;
public class StreamingChatExample : MonoBehaviour
{
[Header("UI")]
public Text dialogueText;
public float typewriterSpeed = 0.02f;
private GumYumUnityClient client;
private NPC currentNPC;
private Coroutine typewriterCoroutine;
void Start()
{
client = GumYumUnityClient.Instance;
// Subscribe to streaming events
client.OnDialogueChunkReceived += OnChunkReceived;
client.OnDialogueStreamStarted += OnStreamStarted;
client.OnDialogueStreamEnded += OnStreamEnded;
}
// STREAMING WITH COROUTINES
IEnumerator StreamingChatCoroutine(NPC npc, string userMessage)
{
var messages = new List<ChatMessage>
{
new ChatMessage { Role = "user", Content = userMessage }
};
// Track streaming state
bool isStreaming = true;
string fullResponse = "";
// Set up event handlers
void OnChunk(object sender, string chunk)
{
fullResponse += chunk;
AppendToDialogue(chunk);
}
void OnStreamEnd(object sender, DialogueStreamEndedEventArgs e)
{
isStreaming = false;
// Check mood changes
if (npc.HasMoodChanged())
{
Debug.Log($"Mood changed: {npc.GetPreviousMood()} → {npc.GetMood()}");
Debug.Log($"Reason: {npc.GetLastMoodChangeReason()}");
}
}
// Subscribe to NPC's streaming events
npc.DialogueChunkReceived += OnChunk;
npc.DialogueStreamEnded += OnStreamEnd;
// Start streaming request
var streamTask = npc.Chat.CompletionsAsync(
messages,
temperature: 0.8,
maxTokens: 2000,
stream: true // Enable streaming
);
// Show typing indicator
ShowTypingIndicator(npc.Name);
// Wait for stream to complete
yield return new WaitUntil(() => !isStreaming || streamTask.IsCompleted);
// Clean up
npc.DialogueChunkReceived -= OnChunk;
npc.DialogueStreamEnded -= OnStreamEnd;
HideTypingIndicator();
// Continue conversation with history
yield return new WaitForSeconds(1f);
// Use chat_with_history for follow-up
var followUp = npc.ChatWithHistoryAsync(
"Can you elaborate on that?",
stream: true
);
// Handle the follow-up stream...
}
// STREAMING WITH ASYNC/AWAIT
async void StreamingChatAsync(NPC npc, string userMessage)
{
try
{
// Build on conversation history
var response = await npc.ChatWithHistoryAsync(
userMessage,
temperature: 0.8,
maxTokens: 2000,
stream: true
);
// Response contains the full text after streaming
Debug.Log($"Full response: {response.Choices[0].Message.Content}");
// Check for mood transitions
if (response.MoodTransition != null && response.MoodTransition.Count > 0)
{
var mood = response.MoodTransition;
Debug.Log($"Mood analysis: {mood}");
}
}
catch (System.Exception e)
{
Debug.LogError($"Streaming failed: {e.Message}");
}
}
// UI HELPERS
void AppendToDialogue(string chunk)
{
if (typewriterCoroutine != null)
StopCoroutine(typewriterCoroutine);
typewriterCoroutine = StartCoroutine(TypewriterEffect(chunk));
}
IEnumerator TypewriterEffect(string text)
{
foreach (char c in text)
{
dialogueText.text += c;
yield return new WaitForSeconds(typewriterSpeed);
}
}
void ShowTypingIndicator(string npcName)
{
dialogueText.text += $"\n<i>{npcName} is typing...</i>";
}
void HideTypingIndicator()
{
// Remove typing indicator
string text = dialogueText.text;
int lastNewline = text.LastIndexOf('\n');
if (lastNewline > 0)
{
dialogueText.text = text.Substring(0, lastNewline);
}
}
// EVENT HANDLERS
void OnChunkReceived(string chunk)
{
// Global handler for any streaming chunk
// Useful for sound effects or animations
}
void OnStreamStarted()
{
// Play typing sound
// Show chat bubble animation
}
void OnStreamEnded(
string fullResponse,
Dictionary<string, object> context,
Dictionary<string, object> moodTransition)
{
// Stream complete
// Update UI state
// Process mood changes
}
void OnDestroy()
{
// Unsubscribe from events
if (client != null)
{
client.OnDialogueChunkReceived -= OnChunkReceived;
client.OnDialogueStreamStarted -= OnStreamStarted;
client.OnDialogueStreamEnded -= OnStreamEnded;
}
}
}