Iplements and integrates GameManager and UIController.

This commit is contained in:
Jonas Luz Jr.
2025-11-23 16:40:06 -03:00
parent dd5aef15b1
commit 4c077514e3
13 changed files with 228 additions and 7 deletions
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Collections;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
namespace PPGIA.X540.Project3.API
{
internal class ApiClient
{
internal static byte[] EncodePayload(object payload)
{
var json = JsonUtility.ToJson(payload);
return Encoding.UTF8.GetBytes(json);
}
static IEnumerator WaitForTimeout(
UnityWebRequestAsyncOperation operation,
float timeoutInSeconds,
Action callbackIfTimeout = null)
{
float startTime = Time.realtimeSinceStartup;
while (!operation.isDone)
{
if (Time.realtimeSinceStartup - startTime > timeoutInSeconds)
{
callbackIfTimeout?.Invoke();
yield break;
}
yield return null;
}
}
static IEnumerator CallEndpointCoroutine(string url,
string method,
object payload,
float timeoutInSeconds,
Action<UnityWebRequest> callbackOnSuccess)
{
using (var request = new UnityWebRequest(url, method))
{
if (method == "POST" || method == "PUT")
{
request.SetRequestHeader("Content-Type", "application/json");
}
if (payload != null)
{
byte[] bodyRaw = EncodePayload(payload);
Debug.Log($"Payload size: {bodyRaw.Length} bytes - {payload}");
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
}
request.downloadHandler = new DownloadHandlerBuffer();
// Debug.Log($"Sending {method} request to {url}");
// Debug.Log(
// payload != null ?
// $"Payload: {JsonUtility.ToJson(payload)}" :
// "No payload.");
var op = request.SendWebRequest();
yield return WaitForTimeout(op, timeoutInSeconds, () =>
{
Debug.LogError("Request timed out.");
});
if (request.result == UnityWebRequest.Result.Success)
{
callbackOnSuccess?.Invoke(request);
}
else
{
var body = request.downloadHandler?.text ?? string.Empty;
var errorTrace = @$"API call failed: {request.error} (HTTP {request.responseCode})
Request Method: {method}
Request URL: {url}
Request Payload: {JsonUtility.ToJson(payload)}
Response Body: {body}";
Debug.LogError(errorTrace);
}
}
}
internal static IEnumerator CallEndpointWithGetCoroutine(
string url, float timeoutInSeconds,
Action<UnityWebRequest> callbackOnSuccess)
{
yield return CallEndpointCoroutine(
url, "GET", null, timeoutInSeconds, callbackOnSuccess);
}
internal static IEnumerator CallEndpointWithPostCoroutine(
string url, float timeoutInSeconds, object payload,
Action<UnityWebRequest> callbackOnSuccess)
{
yield return CallEndpointCoroutine(
url, "POST", payload, timeoutInSeconds, callbackOnSuccess);
}
internal static IEnumerator CallEndpointWithPutCoroutine(
string url, float timeoutInSeconds, object payload,
Action<UnityWebRequest> callbackOnSuccess)
{
yield return CallEndpointCoroutine(
url, "PUT", payload, timeoutInSeconds, callbackOnSuccess);
}
internal static IEnumerator CallEndpointWithDeleteCoroutine(
string url, float timeoutInSeconds,
Action<UnityWebRequest> callbackOnSuccess)
{
yield return CallEndpointCoroutine(
url, "DELETE", null, timeoutInSeconds, callbackOnSuccess);
}
internal static IEnumerator DownloadAudioCoroutine(
string url,
float timeoutInSeconds,
Action<AudioClip> callbackOnSuccess)
{
using (UnityWebRequest mmRequest =
UnityWebRequestMultimedia.GetAudioClip(url, AudioType.OGGVORBIS))
{
var op = mmRequest.SendWebRequest();
yield return WaitForTimeout(op, timeoutInSeconds, () =>
{
Debug.LogError("Request timed out.");
});
if (mmRequest.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"Error loading audio: {mmRequest.error}");
yield break;
}
AudioClip clip = DownloadHandlerAudioClip.GetContent(mmRequest);
if (clip == null)
{
Debug.LogError("AudioClip is null after download.");
yield break;
}
callbackOnSuccess?.Invoke(clip);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1a14c6225c9606e4baff928b379f19fb
@@ -0,0 +1,211 @@
using System;
using System.Collections;
using System.Linq;
using UnityEngine;
namespace PPGIA.X540.Project3.API
{
[RequireComponent(typeof(AudioSource))]
public class ApiClientManager : MonoBehaviour
{
#region -- Inspector Fields -------------------------------------------
[Header("API Base URL Settings")]
[SerializeField]
private string _apiBaseUrlDev = "http://127.0.0.1:8000";
[SerializeField]
private string _apiBaseUrlProd = "https://api.example.com";
[SerializeField]
private Environment _environment = Environment.Development;
[Header("API Endpoints")]
[SerializeField]
private string _sessionInitEndpoint = "/session/init";
[SerializeField]
private string _sessionCloseEndpoint = "/session/close";
[SerializeField]
private string _chatEndpoint = "/chat/";
[SerializeField]
private string _llmAgentEndpoint = "/agent/ask";
[SerializeField]
private string _ttsEndpoint = "/tts/synthesize";
[SerializeField]
private string _sttEndpoint = "/stt/upload";
[Header("API Settings & Workload")]
[SerializeField]
private string _clientId = "unity-client";
[SerializeField]
private float _timeoutInSeconds = 10f;
[SerializeField, Multiline, TextArea(3, 10)]
private string _query;
[Header("API State Information")]
[SerializeField]
private Session _session;
public bool IsSessionActive => _session != null;
#endregion ------------------------------------------------------------
#region -- Other Properties & Methods ---------------------------------
[SerializeField]
private AudioSource _audioSource;
// Property to get the appropriate API base URL
private string ApiBaseUrl =>
_environment == Environment.Development ?
_apiBaseUrlDev : _apiBaseUrlProd;
// Helper Method to build endpoint URLs
string EndpointUrl(params string[] parts) =>
ApiBaseUrl.TrimEnd('/') + '/' +
string.Join("/", parts.Select(p => p.Trim('/')));
#endregion ------------------------------------------------------------
void Awake()
{
_audioSource = GetComponent<AudioSource>();
}
#region -- API Calls --------------------------------------------------
[ContextMenu("API Tests/API Availability")]
public void TestApiAvailability()
{
var url = EndpointUrl("");
StartCoroutine(ApiClient.CallEndpointWithGetCoroutine(
url, _timeoutInSeconds, (request) =>
{
var body = request.downloadHandler?.text ?? string.Empty;
Debug.Log($"API call returned: {body}");
}));
}
[ContextMenu("Session/Initiate Session")]
public void InitiateSession(Action sessionStartedCallback = null)
{
StopAllCoroutines();
var url = EndpointUrl(_sessionInitEndpoint, _clientId);
StartCoroutine(ApiClient.CallEndpointWithPostCoroutine(
url, _timeoutInSeconds, null, (request) =>
{
var body = request.downloadHandler?.text ?? string.Empty;
var session = JsonUtility.FromJson<Session>(body);
_session = session;
sessionStartedCallback?.Invoke();
}));
}
[ContextMenu("Session/Close Session")]
public void CloseSession(Action sessionClosedCallback = null)
{
if (_session == null)
{
Debug.LogWarning("No active session to close.");
sessionClosedCallback?.Invoke();
return;
}
StopAllCoroutines();
var url = EndpointUrl(_sessionCloseEndpoint, _session.SessionId);
StartCoroutine(ApiClient.CallEndpointWithDeleteCoroutine(
url, _timeoutInSeconds, (request) =>
{
Debug.Log("Session closed successfully.");
_session = null;
sessionClosedCallback?.Invoke();
}));
}
[ContextMenu("Chat/Send Message")]
public void SendChatMessage(string message = null,
Action<string> responseReceivedCallback = null,
Action speechFinishedCallback = null)
{
// Ensure there is an active session
if (_session == null)
{
Debug.LogWarning("No active session. Please initiate a session first.");
return;
}
if (message != null) _query = message;
StopAllCoroutines();
// Build the endpoint URL and payload
var url = EndpointUrl(_chatEndpoint, _session.SessionId);
var payload = new ChatServicePayload { message = _query };
// Make the API call. Expect an audio response.
StartCoroutine(ApiClient.CallEndpointWithPostCoroutine(
url, _timeoutInSeconds, payload, (request) =>
{
var body = request.downloadHandler?.text ?? string.Empty;
var response = ApiModel.FromJson<ChatServiceResponse>(body);
var chatResponse = response?.Message;
responseReceivedCallback?.Invoke(chatResponse);
var audioUrl = response?.AudioUrl;
if (string.IsNullOrEmpty(audioUrl))
{
Debug.LogWarning("No audio URL in response.");
return;
}
Debug.Log($"Downloading audio from: {audioUrl}");
StartCoroutine(ApiClient.DownloadAudioCoroutine(
audioUrl, _timeoutInSeconds, (audioClip) =>
{
if (audioClip == null)
{
Debug.LogError("AudioClip is null after download.");
return;
}
StartCoroutine(PlayAudioAndNotifyCoroutine(
audioClip, speechFinishedCallback));
}));
}));
}
private IEnumerator PlayAudioAndNotifyCoroutine(
AudioClip audioClip, Action onComplete)
{
if (_audioSource == null || audioClip == null)
{
onComplete?.Invoke();
yield break;
}
_audioSource.PlayOneShot(audioClip);
yield return new WaitForSeconds(audioClip.length);
onComplete?.Invoke();
}
#endregion -- API Calls ------------------------------------------------
[ContextMenu("Debug/Play or Stop Test Audio")]
public void PlayTestAudio()
{
if (_audioSource == null) return;
if (_audioSource.isPlaying) _audioSource.Stop();
else _audioSource?.Play();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6df1f7169d6eca04abd7db0f712639ff
+56
View File
@@ -0,0 +1,56 @@
using System;
using UnityEngine;
namespace PPGIA.X540.Project3.API
{
[Serializable]
public class ApiModel
{
public static T FromJson<T>(string json) =>
JsonUtility.FromJson<T>(json);
public static string ToJson<T>(T obj) =>
JsonUtility.ToJson(obj);
}
[Serializable]
public class Session: ApiModel
{
public string session_id;
public string created_at;
// Optional convenience properties with C#-style names:
public string SessionId => session_id;
public DateTime CreatedAt => DateTime.Parse(created_at);
}
[Serializable]
public class ChatServicePayload : ApiModel
{
public string message;
}
public class ChatServiceResponse : ApiModel
{
public string session_id;
public string message;
public string audio_url;
public string audio_key;
public int expires_in;
// Optional convenience properties with C#-style names:
public string SessionId => session_id;
public string Message => message;
public string AudioUrl => audio_url;
public string AudioKey => audio_key;
public int ExpiresIn => expires_in;
}
internal enum Environment
{
Development,
Production
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 905309ff23223794faea2150bde7a080