Initial commit of Unity SDK for GumYum NPC client

This commit is contained in:
Russell Ballestrini 2025-09-15 09:59:21 -04:00
commit db46a7bf9d
29 changed files with 3073 additions and 0 deletions

View file

@ -0,0 +1,215 @@
#if UNITY_5_3_OR_NEWER
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using GumYum.NPC;
using GumYum.NPC.Unity;
namespace GumYum.NPC.Unity.Examples {
/// <summary>
/// Example showing filtered NPC spawning and multiple NPCs
/// </summary>
public class NPCFilteredSpawning : MonoBehaviour {
[Header("API Configuration")]
[SerializeField]
private string apiKey = "your-api-key";
[SerializeField]
private string apiSecret = "your-api-secret";
[SerializeField]
private string universeId = "kingdom";
[SerializeField]
private int worldSeed = 12345;
[Header("Spawn Settings")]
[SerializeField]
private string[] professionFilter = { "warrior", "knight", "guard" };
[SerializeField]
private string[] locationFilter = { "barracks", "castle",
"training_grounds" };
[SerializeField]
private int[] personalityTypeFilter = { 1, 8,
6 }; // Reformer, Challenger, Loyalist
[SerializeField]
private string sexFilter = ""; // "male", "female", or "" for any
private GumYumUnityClient client;
private List<NPC> spawnedNPCs = new List<NPC>();
void Start() {
// Initialize the client
client = GumYumUnityClient.Instance;
client.SetCredentials(apiKey, apiSecret);
// Example 1: Spawn filtered NPCs
StartCoroutine(SpawnFilteredNPCs());
}
private IEnumerator SpawnFilteredNPCs() {
Debug.Log("Spawning filtered NPCs...");
// Build filter object
var filters = new Dictionary<string, object>();
if (professionFilter.Length > 0)
filters["profession"] = professionFilter;
if (locationFilter.Length > 0)
filters["location"] = locationFilter;
if (personalityTypeFilter.Length > 0)
filters["personality_type"] = personalityTypeFilter;
if (!string.IsNullOrEmpty(sexFilter))
filters["sex"] = new[] { sexFilter };
// Spawn 3 NPCs with filters
for (int i = 0; i < 3; i++) {
yield return client.SpawnFilteredNPCCoroutine(
universeId, worldSeed + i, filters, npc => {
if (npc != null) {
spawnedNPCs.Add(npc);
Debug.Log($"Spawned: {npc.GetInfo()}");
// Demonstrate mood and stress info
Debug.Log(
$" - Current mood: {npc.GetMood()}, Stress level: {npc.GetStressLevel()}");
// Example of saving NPC for later
PlayerPrefs.SetString($"SavedNPC_{npc.NpcId}", npc.ToJson());
} else {
Debug.LogError("Failed to spawn filtered NPC");
}
});
yield return new WaitForSeconds(0.5f); // Small delay between spawns
}
// Example 2: Chat with all spawned NPCs
yield return new WaitForSeconds(1f);
yield return ChatWithAllNPCs(
"Hello everyone! How are you all doing today?");
// Example 3: Demonstrate mood transitions
yield return new WaitForSeconds(2f);
yield return DemonstrateMoodTransitions();
}
private IEnumerator ChatWithAllNPCs(string message) {
Debug.Log($"\nBroadcasting message to all NPCs: \"{message}\"");
foreach (var npc in spawnedNPCs) {
var messages =
new List<ChatMessage> { new ChatMessage { Role = "user",
Content = message } };
yield return client.ChatCoroutine(npc, messages, response => {
if (response?.Choices?.Count > 0) {
var content = response.Choices[0].Message.Content;
Debug.Log($"{npc.Name}: {content}");
// Check for mood changes
if (npc.HasMoodChanged()) {
Debug.Log(
$" [Mood transition: {npc.GetPreviousMood()} → {npc.GetMood()}]");
Debug.Log($" [Reason: {npc.GetLastMoodChangeReason()}]");
Debug.Log(
$" [Confidence: {npc.GetLastMoodChangeConfidence():F2}]");
}
}
});
yield return new WaitForSeconds(0.5f);
}
}
private IEnumerator DemonstrateMoodTransitions() {
Debug.Log("\nDemonstrating mood transitions with different messages...");
// Messages designed to potentially trigger different moods
var moodTriggeringMessages = new[] {
"I have terrible news. The kingdom is under attack!",
"Great news! We've won the battle and everyone is safe!",
"I'm not sure what to do. Everything seems so uncertain.",
"You've done an amazing job! The king wants to reward you personally!"
};
foreach (var triggerMessage in moodTriggeringMessages) {
Debug.Log($"\nTesting message: \"{triggerMessage}\"");
// Send to first NPC only for this demo
if (spawnedNPCs.Count > 0) {
var npc = spawnedNPCs[0];
var previousMood = npc.GetMood();
var previousStress = npc.GetStressLevel();
var messages = new List<ChatMessage> {
new ChatMessage { Role = "user", Content = triggerMessage }
};
yield return client.ChatCoroutine(npc, messages, response => {
if (response?.Choices?.Count > 0) {
var content = response.Choices[0].Message.Content;
Debug.Log($"{npc.Name}: {content}");
// Analyze mood change
if (npc.GetMood() != previousMood) {
Debug.Log($" ✓ Mood changed: {previousMood} → {npc.GetMood()}");
} else {
Debug.Log($" - Mood unchanged: {npc.GetMood()}");
}
if (npc.GetStressLevel() != previousStress) {
Debug.Log(
$" ✓ Stress changed: {previousStress} → {npc.GetStressLevel()}");
}
}
});
yield return new WaitForSeconds(2f);
}
}
}
// Example: Load a previously saved NPC
public IEnumerator LoadSavedNPC(int npcId) {
string savedJson = PlayerPrefs.GetString($"SavedNPC_{npcId}", "");
if (!string.IsNullOrEmpty(savedJson)) {
var npc = NPC.FromJson(savedJson, client.Client);
Debug.Log($"Loaded saved NPC: {npc.GetInfo()}");
// You can continue chatting with the loaded NPC
yield return ChatWithNPC(npc, "Do you remember our last conversation?");
}
}
private IEnumerator ChatWithNPC(NPC npc, string message) {
var messages =
new List<ChatMessage> { new ChatMessage { Role = "user",
Content = message } };
yield return client.ChatCoroutine(npc, messages, response => {
if (response?.Choices?.Count > 0) {
Debug.Log($"{npc.Name}: {response.Choices[0].Message.Content}");
}
});
}
// Example: Get all NPCs of a specific mood
public List<NPC> GetNPCsByMood(string mood) {
return spawnedNPCs.FindAll(npc => npc.GetMood() == mood);
}
// Example: Get stressed NPCs (stress > 7)
public List<NPC> GetStressedNPCs() {
return spawnedNPCs.FindAll(npc => npc.GetStressLevel() > 7);
}
private void OnDestroy() {
// Clean up saved NPCs if needed
foreach (var npc in spawnedNPCs) {
PlayerPrefs.DeleteKey($"SavedNPC_{npc.NpcId}");
}
}
}
}
#endif

View file

@ -0,0 +1,228 @@
#if UNITY_5_3_OR_NEWER
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using GumYum.NPC;
using GumYum.NPC.Unity;
namespace GumYum.NPC.Unity.Examples {
/// <summary>
/// Simple example showing NPC spawning and dialogue
/// Attach this to a GameObject in your scene
/// </summary>
public class QuickstartChat : MonoBehaviour {
[Header("API Configuration")]
[SerializeField]
private string apiKey = "your-api-key";
[SerializeField]
private string apiSecret = "your-api-secret";
[SerializeField]
private string universeId = "blade-runner";
[SerializeField]
private int worldSeed = 42;
[Header("UI References")]
[SerializeField]
private Text npcNameText;
[SerializeField]
private Text npcInfoText;
[SerializeField]
private InputField userInput;
[SerializeField]
private Button sendButton;
[SerializeField]
private Text chatOutput;
[SerializeField]
private ScrollRect scrollRect;
[Header("Chat Settings")]
[SerializeField]
private float temperature = 0.8f;
[SerializeField]
private int maxTokens = 2000;
[SerializeField]
private bool useStreamingChat = false;
private GumYumUnityClient client;
private NPC currentNPC;
private bool isProcessing = false;
void Start() {
// Initialize the client
client = GumYumUnityClient.Instance;
client.SetCredentials(apiKey, apiSecret);
// Setup UI
sendButton.onClick.AddListener(OnSendButtonClicked);
userInput.onEndEdit.AddListener(OnInputEndEdit);
// Subscribe to events
client.OnCharacterSpawned += OnCharacterSpawned;
client.OnDialogueReceived += OnDialogueReceived;
client.OnDialogueChunkReceived += OnDialogueChunkReceived;
client.OnRequestFailed += OnRequestFailed;
// Start by spawning an NPC
StartCoroutine(SpawnRandomNPC());
}
private IEnumerator SpawnRandomNPC() {
UpdateChatOutput("Spawning a random NPC...");
yield return client.SpawnNPCCoroutine(universeId, worldSeed, null, npc => {
if (npc != null) {
currentNPC = npc;
UpdateNPCInfo();
UpdateChatOutput($"\n{npc.Name} has entered the chat!\n");
UpdateChatOutput(
$"[{npc.GetMood()} mood, stress level: {npc.GetStressLevel()}]\n\n");
} else {
UpdateChatOutput(
"\nFailed to spawn NPC. Check your API credentials.\n");
}
});
}
private void OnCharacterSpawned(NPC npc) {
Debug.Log($"Character spawned: {npc.GetInfo()}");
}
private void OnDialogueReceived(string response,
Dictionary<string, object> context,
Dictionary<string, object> moodTransition) {
if (!useStreamingChat) {
UpdateChatOutput($"\n{currentNPC.Name}: {response}\n");
if (currentNPC.HasMoodChanged()) {
UpdateChatOutput(
$"[Mood changed from {currentNPC.GetPreviousMood()} to {currentNPC.GetMood()}]\n");
}
}
}
private void OnDialogueChunkReceived(string chunk) {
if (useStreamingChat) {
UpdateChatOutput(chunk);
}
}
private void OnRequestFailed(string endpoint, string error) {
Debug.LogError($"Request failed - {endpoint}: {error}");
UpdateChatOutput($"\n[Error: {error}]\n");
isProcessing = false;
}
private void OnSendButtonClicked() {
if (!isProcessing && !string.IsNullOrEmpty(userInput.text) &&
currentNPC != null) {
SendMessage(userInput.text);
}
}
private void OnInputEndEdit(string value) {
if (Input.GetKeyDown(KeyCode.Return) ||
Input.GetKeyDown(KeyCode.KeypadEnter)) {
OnSendButtonClicked();
}
}
private void SendMessage(string message) {
isProcessing = true;
// Display user message
UpdateChatOutput($"\nYou: {message}\n");
// Clear input
userInput.text = "";
userInput.ActivateInputField();
// Send to NPC
if (useStreamingChat) {
StartCoroutine(StreamChatWithNPC(message));
} else {
StartCoroutine(ChatWithNPC(message));
}
}
private IEnumerator ChatWithNPC(string message) {
yield return client.SimpleChatCoroutine(currentNPC, message, response => {
isProcessing = false;
if (response == null) {
UpdateChatOutput("\n[Failed to get response]\n");
}
UpdateNPCInfo();
}, temperature, maxTokens);
}
private IEnumerator StreamChatWithNPC(string message) {
UpdateChatOutput($"\n{currentNPC.Name}: ");
var messages =
new List<ChatMessage> { new ChatMessage { Role = "user",
Content = message } };
bool streamComplete = false;
string fullResponse = "";
// Use streaming
var task = currentNPC.Chat.CompletionsAsync(messages, temperature,
maxTokens, true);
// Set up streaming event handlers
void OnChunk(object sender, string chunk) {
fullResponse += chunk;
UpdateChatOutput(chunk);
}
void OnStreamEnd(object sender, DialogueStreamEndedEventArgs e) {
streamComplete = true;
if (currentNPC.HasMoodChanged()) {
UpdateChatOutput(
$"\n[Mood changed from {currentNPC.GetPreviousMood()} to {currentNPC.GetMood()}]\n");
}
}
currentNPC.DialogueChunkReceived += OnChunk;
currentNPC.DialogueStreamEnded += OnStreamEnd;
// Wait for stream to complete
yield return new WaitUntil(() => streamComplete || task.IsCompleted);
// Clean up event handlers
currentNPC.DialogueChunkReceived -= OnChunk;
currentNPC.DialogueStreamEnded -= OnStreamEnd;
UpdateChatOutput("\n");
UpdateNPCInfo();
isProcessing = false;
}
private void UpdateNPCInfo() {
if (currentNPC != null) {
npcNameText.text = currentNPC.Name;
npcInfoText.text =
$"{currentNPC.Profession} | Type {currentNPC.PersonalityType} | {currentNPC.GetLocation()}\n" +
$"Mood: {currentNPC.GetMood()} | Stress: {currentNPC.GetStressLevel()}";
}
}
private void UpdateChatOutput(string text) {
chatOutput.text += text;
// Auto-scroll to bottom
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 0f;
}
private void OnDestroy() {
if (client != null) {
client.OnCharacterSpawned -= OnCharacterSpawned;
client.OnDialogueReceived -= OnDialogueReceived;
client.OnDialogueChunkReceived -= OnDialogueChunkReceived;
client.OnRequestFailed -= OnRequestFailed;
}
}
}
}
#endif