initial upload
This commit is contained in:
@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Convai.Scripts.Runtime.Core;
|
||||
using Convai.Scripts.Runtime.LoggerSystem;
|
||||
using Grpc.Core;
|
||||
using Service;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Convai.Scripts.Runtime.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the conversation between two Convai powered NPC groups
|
||||
/// </summary>
|
||||
public class NPC2NPCConversationManager : MonoBehaviour
|
||||
{
|
||||
private const string GRPC_API_ENDPOINT = "stream.convai.com";
|
||||
public static NPC2NPCConversationManager Instance;
|
||||
public List<NPCGroup> npcGroups;
|
||||
private readonly List<NPCGroup> _groupsWhereConversationEnded = new();
|
||||
private string _apiKey = string.Empty;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
Instance = this;
|
||||
else
|
||||
Destroy(gameObject);
|
||||
LoadApiKey();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
foreach (NPCGroup group in npcGroups)
|
||||
group.Initialize(HandlePlayerVicinityChanged);
|
||||
|
||||
StartConversationWithAllNPCs();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the event of player vicinity change.
|
||||
/// </summary>
|
||||
/// <param name="isPlayerNear">Indicates if the player is near.</param>
|
||||
/// <param name="npc">The NPC for which the vicinity changed.</param>
|
||||
private void HandlePlayerVicinityChanged(bool isPlayerNear, ConvaiGroupNPCController npc)
|
||||
{
|
||||
if (isPlayerNear)
|
||||
ResumeConversation(npc);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Processes the message from the sender NPC.
|
||||
/// </summary>
|
||||
/// <param name="sender">The NPC sending the message.</param>
|
||||
/// <param name="topic">The topic of the conversation.</param>
|
||||
/// <param name="message">The message to be processed.</param>
|
||||
/// <returns>The processed message.</returns>
|
||||
private string ProcessMessage(ConvaiGroupNPCController sender, string topic, string message)
|
||||
{
|
||||
string processedMessage = $"{sender.CharacterName} said \"{message}\" to you. Reply to it. ";
|
||||
|
||||
processedMessage += Random.Range(0, 2) == 0
|
||||
? $"Talk about something related to {message}. "
|
||||
: $"Talk about something other than \"{message}\" but related to {topic}. Gently change the conversation topic. ";
|
||||
|
||||
return processedMessage + "Definitely, reply to the message. Dont address speaker. Keep the reply short. Do not repeat the same message, or keep asking same question.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Relays the message from the sender NPC.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be relayed.</param>
|
||||
/// <param name="sender">The NPC sending the message.</param>
|
||||
/// <param name="performSwitch"></param>
|
||||
public void RelayMessage(string message, ConvaiGroupNPCController sender)
|
||||
{
|
||||
NPCGroup npcGroup = npcGroups.Find(c => c.BelongToGroup(sender));
|
||||
if (npcGroup == null)
|
||||
{
|
||||
ConvaiLogger.Warn("Conversation not found for the sender.", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
npcGroup.messageToRelay = message;
|
||||
if (!npcGroup.CurrentSpeaker.IsPlayerNearMe()) return;
|
||||
StartCoroutine(RelayMessageCoroutine(message, sender, npcGroup));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine to relay the message from the sender NPC.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to be relayed.</param>
|
||||
/// <param name="npcGroup"> The NPC group to relay the message to. </param>
|
||||
/// <returns>An IEnumerator to be used in a coroutine.</returns>
|
||||
private IEnumerator RelayMessageCoroutine(string message, ConvaiGroupNPCController sender, NPCGroup npcGroup)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
ConvaiGroupNPCController receiver = npcGroup.GroupNPC1 == sender ? npcGroup.GroupNPC2 : npcGroup.GroupNPC1;
|
||||
while (receiver.ConvaiNPC.IsCharacterTalking)
|
||||
{
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
}
|
||||
try
|
||||
{
|
||||
ConvaiLogger.DebugLog($"Relaying message from {sender.CharacterName} to {receiver.CharacterName}: {message}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
string processedMessage = ProcessMessage(receiver, npcGroup.topic, message);
|
||||
receiver.SendTextDataNPC2NPC(processedMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConvaiLogger.Warn($"Failed to relay message: {e.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the speaker in the conversation.
|
||||
/// </summary>
|
||||
/// <param name="currentSpeaker">The current speaker NPC.</param>
|
||||
/// <returns>The new speaker NPC.</returns>
|
||||
public void SwitchSpeaker(ConvaiGroupNPCController currentSpeaker)
|
||||
{
|
||||
NPCGroup group = npcGroups.Find(g => g.CurrentSpeaker == currentSpeaker);
|
||||
if (group != null)
|
||||
{
|
||||
group.CurrentSpeaker = currentSpeaker == group.GroupNPC1 ? group.GroupNPC2 : group.GroupNPC1;
|
||||
ConvaiLogger.DebugLog($"Switching NPC2NPC Speaker to {group.CurrentSpeaker}", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
ConvaiLogger.Warn("Failed to switch speaker. Current speaker not found in any group.", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private void LoadApiKey()
|
||||
{
|
||||
ConvaiAPIKeySetup.GetAPIKey(out _apiKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a single NPC for conversation.
|
||||
/// </summary>
|
||||
/// <param name="npc">The NPC to initialize.</param>
|
||||
/// <param name="grpcClient">The GRPC client to use for the NPC.</param>
|
||||
private void InitializeNPC(ConvaiGroupNPCController npc, NPC2NPCGRPCClient grpcClient)
|
||||
{
|
||||
if (npc == null)
|
||||
{
|
||||
ConvaiLogger.Warn("The given NPC is null.", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
npc.ConversationManager = this;
|
||||
npc.InitializeNpc2NpcGrpcClient(grpcClient);
|
||||
npc.AttachSpeechBubble();
|
||||
npc.IsInConversationWithAnotherNPC = true;
|
||||
npc.ConvaiNPC.isCharacterActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the conversation for the given NPC.
|
||||
/// </summary>
|
||||
/// <param name="npcGroup">The NPC to start the conversation for.</param>
|
||||
private void InitializeNPCGroup(NPCGroup npcGroup)
|
||||
{
|
||||
if (npcGroup == null)
|
||||
{
|
||||
ConvaiLogger.Warn("The given NPC is not part of any group.", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
ConvaiGroupNPCController npc1 = npcGroup.GroupNPC1;
|
||||
ConvaiGroupNPCController npc2 = npcGroup.GroupNPC2;
|
||||
|
||||
if (npc1.IsInConversationWithAnotherNPC || npc2.IsInConversationWithAnotherNPC)
|
||||
{
|
||||
ConvaiLogger.Warn($"{npc1.CharacterName} or {npc2.CharacterName} is already in a conversation.", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
NPC2NPCGRPCClient grpcClient = CreateAndInitializeGRPCClient(npcGroup);
|
||||
|
||||
// Initialize both NPCs
|
||||
InitializeNPC(npc1, grpcClient);
|
||||
InitializeNPC(npc2, grpcClient);
|
||||
|
||||
npcGroup.CurrentSpeaker = Random.Range(0, 10) % 2 == 0 ? npc1 : npc2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and initializes a new GRPC client for the given NPC group.
|
||||
/// </summary>
|
||||
/// <param name="group">The NPC group to create the GRPC client for.</param>
|
||||
/// <returns>The initialized GRPC client.</returns>
|
||||
private NPC2NPCGRPCClient CreateAndInitializeGRPCClient(NPCGroup group)
|
||||
{
|
||||
GameObject grpcClientGameObject = new($"GRPCClient_{group.GroupNPC1.CharacterID}_{group.GroupNPC2.CharacterID}")
|
||||
{
|
||||
transform = { parent = transform }
|
||||
};
|
||||
|
||||
NPC2NPCGRPCClient grpcClient = grpcClientGameObject.AddComponent<NPC2NPCGRPCClient>();
|
||||
ConvaiService.ConvaiServiceClient serviceClient = CreateNewConvaiServiceClient();
|
||||
grpcClient.Initialize(_apiKey, serviceClient, group);
|
||||
return grpcClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ConvaiServiceClient.
|
||||
/// </summary>
|
||||
/// <returns> The new ConvaiServiceClient. </returns>
|
||||
private ConvaiService.ConvaiServiceClient CreateNewConvaiServiceClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
SslCredentials credentials = new();
|
||||
List<ChannelOption> options = new()
|
||||
{
|
||||
new ChannelOption(ChannelOptions.MaxReceiveMessageLength, 16 * 1024 * 1024)
|
||||
};
|
||||
Channel channel = new(GRPC_API_ENDPOINT, credentials, options);
|
||||
return new ConvaiService.ConvaiServiceClient(channel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Failed to create ConvaiServiceClient: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes the conversation for the given NPC.
|
||||
/// </summary>
|
||||
/// <param name="sender"> The NPC to resume the conversation for. </param>
|
||||
private void ResumeConversation(ConvaiGroupNPCController sender)
|
||||
{
|
||||
NPCGroup npcGroup = npcGroups.Find(g => g.BelongToGroup(sender));
|
||||
if (npcGroup.IsAnyoneTalking()) return;
|
||||
|
||||
if (_groupsWhereConversationEnded.Contains(npcGroup))
|
||||
{
|
||||
InitializeNPCGroup(npcGroup);
|
||||
_groupsWhereConversationEnded.Remove(npcGroup);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(npcGroup.messageToRelay))
|
||||
{
|
||||
string message = $"Talk about {npcGroup.topic}.";
|
||||
npcGroup.CurrentSpeaker.SendTextDataNPC2NPC(message);
|
||||
npcGroup.messageToRelay = message;
|
||||
ConvaiLogger.DebugLog($"Starting conversation for the first time between {npcGroup.GroupNPC1.CharacterName} and {npcGroup.GroupNPC2.CharacterName}",
|
||||
ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
else
|
||||
{
|
||||
RelayMessage(npcGroup.messageToRelay, npcGroup.CurrentSpeaker);
|
||||
ConvaiLogger.DebugLog($"Resuming conversation between {npcGroup.GroupNPC1.CharacterName} and {npcGroup.GroupNPC2.CharacterName}",
|
||||
ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the conversation for the given NPC.
|
||||
/// </summary>
|
||||
/// <param name="npc"> The NPC to end the conversation for. </param>
|
||||
public void EndConversation(ConvaiGroupNPCController npc)
|
||||
{
|
||||
NPCGroup group = npcGroups.Find(g => g.BelongToGroup(npc));
|
||||
ConvaiLogger.DebugLog($"Ending conversation between {group.GroupNPC1.CharacterName} and {group.GroupNPC2.CharacterName}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
void EndConversationForNPC(ConvaiGroupNPCController groupNPC)
|
||||
{
|
||||
groupNPC.IsInConversationWithAnotherNPC = false;
|
||||
groupNPC.ConvaiNPC.InterruptCharacterSpeech();
|
||||
groupNPC.GetComponent<ConvaiGroupNPCController>().DetachSpeechBubble();
|
||||
}
|
||||
|
||||
EndConversationForNPC(group.GroupNPC1);
|
||||
EndConversationForNPC(group.GroupNPC2);
|
||||
_groupsWhereConversationEnded.Add(group);
|
||||
ConvaiNPCManager.Instance.SetActiveConvaiNPC(npc.ConvaiNPC);
|
||||
|
||||
Destroy(transform.Find($"GRPCClient_{group.GroupNPC1.CharacterID}_{group.GroupNPC2.CharacterID}").gameObject);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Starts the conversation with all NPCs.
|
||||
/// </summary>
|
||||
private void StartConversationWithAllNPCs()
|
||||
{
|
||||
IEnumerable filteredList = npcGroups
|
||||
.Where(npcGroup => npcGroup.BothNPCAreNotNull())
|
||||
.Where(npcGroup => npcGroup.BothNPCAreNotActiveNPC());
|
||||
|
||||
foreach (NPCGroup npcGroup in filteredList)
|
||||
InitializeNPCGroup(npcGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user