2using System.Collections;
3using System.Collections.Generic;
6using Microsoft.Extensions.Configuration;
70 [Header(
"GUI References")]
75 [Header(
"Configurations")]
79 [Header(
"Discussions")]
82 [Header(
"Voice Chat Settings")]
83 [SerializeField, Range(1, 100), Tooltip(
"How many rounds to play before the chat ends.")]
private int finishRound = 10;
92 private Dictionary<string, (
Actor, AudioSource)>
actors =
new Dictionary<
string, (
Actor, AudioSource)>();
225 if (kv.Value && kv.Key !=
null) { Destroy(kv.Key); }
267 Debug.Log(
"Starting audio thread...");
270 AudioSource playingAudioSource =
null;
271 while (!destroyCancellationToken.IsCancellationRequested) {
273 if (playingAudioSource !=
null && playingAudioSource.isPlaying) {
279 if (playingAudioSource !=
null && playingAudioSource.clip !=
null) {
285 playingAudioSource =
null;
288 bool hasOutput =
false;
297 playingAudioSource.clip = actorOutput.
audioClip;
299 playingAudioSource.Play();
302 Debug.Log($
"{actorOutput.persona} is {actorOutput.emotion.ToString()}");
305 for (
int i = 0; i < actorOutput.
reasonings.Count; ++i) {
321 Debug.Log(
"Shutting down audio thread...");
330 foreach (var item
in actors) {
331 Actor actor = item.Value.Item1;
332 if (actor == speaker)
continue;
342 foreach (var item
in actors) {
343 Actor actor = item.Value.Item1;
363 private void OnEnterIdle() { Debug.Log(
"VoiceChat: OnEnterIdle"); }
367 Debug.Log(
"VoiceChat: OnEnterPrepare");
371 if (
roundCounterText !=
null) { roundCounterText.text = $
"Round {roundCounter}"; }
380 fsm.ChangeState((
int)
State.Discuss);
390 private void OnEnterWait() { Debug.Log(
"VoiceChat: OnEnterWait"); }
407 Debug.Log(
"VoiceChat: OnEnterListen");
414 List<string> speakers =
actors.Keys.ToList();
419 Select(d => d.GetTriggerTopic()).ToList();
428 if (
idleTimerText !=
null) { idleTimerText.text = $
"Idle Time: {idleTimer.ToString("n2
")}s"; }
437 Debug.Log(
"Aborting idle discussion as director has already started responding.");
444 fsm.ChangeState((
int)
State.Discuss);
456 var (actor, audioSource) =
actors.GetValueOrDefault(
speakerQueue.Dequeue(), (
null,
null));
457 if (actor ==
null || audioSource ==
null) {
458 Debug.LogWarning(
"Actor or AudioSource is null!");
462 Actor.Response actorResponse = await actor.GetResponse(destroyCancellationToken);
468 fsm.ChangeState((
int)
State.Prepare);
472 Debug.Log(
"VoiceChat: OnEnterFreeTalk");
481 var (actor, audioSource) =
actors.GetValueOrDefault(dialogue.speaker.ToString(), (
null,
null));
482 if (actor ==
null || audioSource ==
null) {
483 Debug.LogWarning(
"Actor or AudioSource is null!");
487 Actor.Response actorResponse = await actor.InsertResponse(dialogue.message, dialogue.emotion, destroyCancellationToken);
493 fsm.ChangeState((
int)
State.Prepare);
500 foreach (
Persona speaker
in speakers) {
501 var (actor, audioSource) =
actors.GetValueOrDefault(speaker.ToString(), (
null,
null));
502 if (actor ==
null || audioSource ==
null) {
503 Debug.LogWarning(
"Actor or AudioSource is null!");
507 actor.AddSystemMesssage(discussion.GetDiscussionPrompt());
508 Actor.Response actorResponse = await actor.GetResponse(destroyCancellationToken);
514 fsm.ChangeState((
int)
State.Prepare);
518 Debug.Log(
"VoiceChat: OnEnterDiscussion");
522 switch (discussion) {
524 Debug.Log(
"VoiceChat: Starting scripted discussion...");
528 Debug.Log(
"VoiceChat: Starting generated discussion...");
532 throw new System.NotImplementedException();
538 Debug.Log(
"VoiceChat: OnEnterFinish");
546 if (
fsm.GetCurrentState() == (
int)
State.Idle) {
547 fsm.ChangeState((
int)
State.Prepare);
552 if (
fsm.GetCurrentState() == (
int)
State.Finish) {
567 if (
fsm.GetCurrentState() != (
int)
State.Listen) {
return; }
570 chatbox.AddMessage(
"User", response.userTranscript);
575 if (response.discussionTopic !=
null) {
582 fsm.ChangeState((
int)
State.Discuss);
589 if (response.speakerOrder !=
null) {
590 foreach (
string speaker
in response.speakerOrder) {
593 fsm.ChangeState((
int)
State.FreeTalk);
598 foreach (var key
in actors.Keys) {
600 fsm.ChangeState((
int)
State.FreeTalk);
601 Debug.Log(
"Director decision failed. Applying backup.");
607 string filePath = $
"{Application.streamingAssetsPath}/Configs/{OverrideFileName}";
608 IConfiguration config =
new ConfigurationBuilder().AddIniFile(filePath).Build();
609 IConfigurationSection section = config.GetSection(
"VoiceChat");
612 if (section ==
null) {
613 Debug.LogWarning($
"Section VoiceChat not found in {filePath}!");
618 Func<string, string> GetValue = (
string key) => {
return section[key] ==
null ? string.Empty : section[key].Trim(); };
619 string value =
string.Empty;
623 value = GetValue(
"finish_round");
624 if (!
string.IsNullOrEmpty(value) &&
int.TryParse(value, out parsedInt)) {
626 Debug.Log($
"Overrode VoiceChat's Finish Round to {finishRound}");
List< string > reasonings
The actors are the OpenAI Response model which chats with the user.
void AddUserMessage(string message)
void Initialise(UnityAction< Director.Response > onDirectorResponse, CancellationToken cancellationToken)
bool IsStatus(Status value)
async Awaitable< bool > SubmitUserTextInput(string message, CancellationToken cancellationToken)
async void ListenForNextUserInput(DirectorConfig config, List< string > speakers, List< string > topics, CancellationToken cancellationToken)
Status
Current status of the director.
Discussions are a way for the designers to create a way for the actors to interact beyond the standar...
TriggerMode
Ways that a discussion can be triggered.
bool HasAllTriggerModes(TriggerMode modes)
float GetTriggerIdleTime()
GeneratedDiscussions allow the designer to request the actors to generate N responses based on a prom...
List< Persona > GenerateRandomSpeakerOrder()
ScriptedDiscussions allow the designer to make the actors speak scripted lines.
List< Dialogue > GetDialogues()
Output of actors to be queued and played by the audio thread.
ActorOutput(string persona, string message, Emotion emotion, List< string > reasonings, AudioClip audioClip, AudioSource audioSource)
List< string > reasonings
Reference to an actor configuration, and the AudioSource it should play it's output audio from.
AudioSource audioSource
The audio source to output the speech of the actor.
ActorConfig actorConfig
The configuration of the actor.
EchoTrio.UI.SpriteSwitcher listeningIcon
The listening icon of the actor.
IEnumerator AudioThread()
Launch an audio thread via Coroutine to play any queued audio from the actors.
Queue< ActorOutput > actorOutputQueue
void QueueActorOutput(ActorOutput actorOutput)
Queue up an actor's output to be played by the audio thread.
EchoTrio.UI.Chatbox chatbox
void EndQueuingAudio()
Flag that audio is not queued.
GameInputActions gameInputActions
void PropogateActorMessage(Actor speaker, string message)
Send the message received from one actor, to all the other actors.
DirectorConfig directorConfig
List< Discussion > untriggeredDiscussions
const string OverrideFileName
async Awaitable< bool > SubmitUserTextInput(string message)
Submit the user text input. Used as an alternative to speaking into the microphone,...
Queue< Discussion > discussionQueue
void OnPushToTalkStarted(UnityEngine.InputSystem.InputAction.CallbackContext context)
void ToggleChatbox()
Toggle the chatbox to be active or inactive.
TMPro.TextMeshProUGUI roundCounterText
async void RunGeneratedDiscussion(GeneratedDiscussion discussion)
void ResetIdleTimer()
Reset the idle timer, usually invoked whenever there's any input by the user, such as typing somethin...
void PropogateUserMessage(string message)
Send the message received from the user to all the actors.
State
All the possible states of the system.
@ Wait
If user input is allowed this round, enter the Wait stage to wait for the actors to finish speaking,...
@ Finish
The chat has ended.
@ FreeTalk
Actors responding to user input as per normal AI conversations.
@ Discuss
Actors playing a scripted discussion, or generating a discussion on a topic.
@ Listen
Listen for user input.
@ Idle
Idle while waiting for player to start the game.
@ Prepare
Before we start each round, we enter the Prepare stage, and make a decision of which state to enter f...
void OnPushToTalkCancelled(UnityEngine.InputSystem.InputAction.CallbackContext context)
ActorReferences[] actorReferences
void BeginQueuingAudio()
Flag that audio is being queued.
void ToggleMicMute()
Toggle the director's microphone on or off.
Dictionary< AudioClip, bool > audioClipGarbageCollector
async void RunScriptedDiscussion(ScriptedDiscussion discussion)
Queue< string > speakerQueue
void OnDirectorResponse(Director.Response response)
FSM.FiniteStateMachine fsm
TMPro.TextMeshProUGUI idleTimerText
Dictionary< string,(Actor, AudioSource)> actors
Finite state machine class to handle state transitions and updates.
void UnsubscribeFromEvent(string eventName, UnityAction unityAction)
void TriggerEvent(string eventName)
void SubscribeToEvent(string eventName, UnityAction unityAction)
static GameEventSystem GetInstance()
Persona
Personas the actors will role-play.