using System; using System.Collections; using System.Threading.Tasks; using Convai.Scripts.Runtime.Core; using Convai.Scripts.Runtime.Features.LipSync; using Convai.Scripts.Runtime.LoggerSystem; using Service; using UnityEngine; namespace Convai.Scripts.Runtime.Features { /// /// This class is responsible for handling out all the tasks related to NPC to NPC conversation for a NPC of a group /// public class ConvaiGroupNPCController : MonoBehaviour { /// /// Used to set Player GameObject Transform and lip-sync /// private void Awake() { if (playerTransform == null) playerTransform = Camera.main.transform; ConvaiNPC = GetComponent(); CONVERSATION_DISTANCE_THRESHOLD = conversationDistanceThreshold == 0 ? Mathf.Infinity : conversationDistanceThreshold; TryGetComponent(out _lipSync); } /// /// Starts coroutine for player vicinity check and subscribe to necessary events /// private void Start() { _npcGroup = NPC2NPCConversationManager.Instance.npcGroups.Find(c => c.BelongToGroup(this)); otherNPC = _npcGroup.GroupNPC1 == this ? _npcGroup.GroupNPC2 : _npcGroup.GroupNPC1; _checkPlayerVicinityCoroutine = StartCoroutine(CheckPlayerVicinity()); if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnAudioTranscriptAvailable += HandleAudioTranscriptAvailable; } /// /// Unsubscribes to the events and stops the coroutine /// private void OnDestroy() { if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnAudioTranscriptAvailable -= HandleAudioTranscriptAvailable; if(_npc2NPCGrpcClient!=null) _npc2NPCGrpcClient.OnTranscriptAvailable -= HandleTranscriptAvailable; if (_checkPlayerVicinityCoroutine != null) StopCoroutine(_checkPlayerVicinityCoroutine); } /// /// Shows speech bubble and adds the received text to final transcript /// /// private void HandleAudioTranscriptAvailable(string transcript) { if (IsInConversationWithAnotherNPC) ShowSpeechBubble?.Invoke(transcript); } private void HandleTranscriptAvailable(string transcript, ConvaiGroupNPCController npcController) { if (npcController != this) return; _finalResponseText += transcript; } /// /// Attaches the speech bubble to the NPC game-object /// public void AttachSpeechBubble() { if (TryGetComponent(out ConvaiSpeechBubbleController _)) return; gameObject.AddComponent().Initialize(speechBubblePrefab, this); } /// /// Destroys the speech bubble game-object /// public void DetachSpeechBubble() { if (TryGetComponent(out ConvaiSpeechBubbleController convaiSpeechBubble)) Destroy(convaiSpeechBubble); } /// /// Store the references of the client and subscribe to the necessary events /// /// public void InitializeNpc2NpcGrpcClient(NPC2NPCGRPCClient client) { _npc2NPCGrpcClient = client; _npc2NPCGrpcClient.OnTranscriptAvailable += HandleTranscriptAvailable; } /// /// Every 0.5 seconds updates if player is near or not and fire events according to the state /// /// private IEnumerator CheckPlayerVicinity() { bool previousState = false; Vector3 previousPlayerPosition = Vector3.zero; yield return new WaitForSeconds(0.1f); while (true) { Vector3 currentPlayerPosition = playerTransform.transform.position; // Check if the player has moved more than a certain threshold distance if (Vector3.Distance(previousPlayerPosition, currentPlayerPosition) > PLAYER_MOVE_THRESHOLD) { // Calculate the distance between the NPC and the player float distanceToPlayer = Vector3.Distance(transform.position, currentPlayerPosition); // Check if the player is within the threshold distance bool isPlayerCurrentlyNear = distanceToPlayer <= CONVERSATION_DISTANCE_THRESHOLD; // If the player's current vicinity state is different from the previous state, raise the event if (isPlayerCurrentlyNear != previousState && !ConvaiNPC.isCharacterActive) { OnPlayerVicinityChanged?.Invoke(isPlayerCurrentlyNear, this); previousState = isPlayerCurrentlyNear; // Update the previous state ConvaiLogger.Info($"Player is currently near {ConvaiNPC.characterName}: {isPlayerCurrentlyNear}", ConvaiLogger.LogCategory.Character); } previousPlayerPosition = currentPlayerPosition; // Update the player's previous position // Check every half second } yield return new WaitForSeconds(0.5f); } } /// /// Sends the text to the other NPC in the group /// /// public async void SendTextDataNPC2NPC(string message) { if (_npc2NPCGrpcClient == null) { ConvaiLogger.Warn("No GRPC client initialized for this NPC.", ConvaiLogger.LogCategory.Character); return; } try { CanRelayMessage = false; await Task.Delay(500); await _npc2NPCGrpcClient.SendTextData( message, ConvaiNPC.characterID, ConvaiNPC.sessionID, _lipSync != null, FaceModel, this); } catch (Exception ex) { ConvaiLogger.Warn($"Error sending message data for NPC2NPC: {ex.Message}", ConvaiLogger.LogCategory.Character); } } public void EndOfResponseReceived() { if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnCharacterTalkingChanged += SendFinalTranscriptToOtherNPC; ConversationManager.RelayMessage(_finalResponseText, this); _finalResponseText = ""; } private void SendFinalTranscriptToOtherNPC(bool isTalking) { if (IsInConversationWithAnotherNPC) { if (!isTalking) { ConversationManager.SwitchSpeaker(this); if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnCharacterTalkingChanged -= SendFinalTranscriptToOtherNPC; HideSpeechBubble?.Invoke(); } else { ConvaiLogger.DebugLog($"{ConvaiNPC.characterName} is currently still talking. ", ConvaiLogger.LogCategory.Character); } } } public bool IsPlayerNearMe() { bool result = Vector3.Distance(transform.position, playerTransform.position) < CONVERSATION_DISTANCE_THRESHOLD; ConvaiLogger.Info($"Player is near {CharacterName}: {result}", ConvaiLogger.LogCategory.Character); return result; } public bool IsOtherNPCTalking() { return otherNPC.ConvaiNPC.IsCharacterTalking; } #region Serialized Fields [Tooltip("The prefab for the speech bubble to display above the NPC. [Optional]")] [SerializeField] private NPCSpeechBubble speechBubblePrefab; [Tooltip("Attach the Main Player Transform here so that distance check can be performed")] [SerializeField] private Transform playerTransform; // The distance from the NPC to the player when the NPC will start talking [Tooltip("The distance from the NPC to the player when the NPC will start talking. Set to 0 to disable this feature. [Optional]")] [SerializeField] [Range(0f, 100f)] private float conversationDistanceThreshold = 5.0f; #endregion #region Private Attributes private const float PLAYER_MOVE_THRESHOLD = 0.5f; private float CONVERSATION_DISTANCE_THRESHOLD; private string _finalResponseText = ""; private NPC2NPCGRPCClient _npc2NPCGrpcClient; private ConvaiLipSync _lipSync; private Coroutine _checkPlayerVicinityCoroutine; private FaceModel FaceModel => _lipSync == null ? FaceModel.OvrModelName : _lipSync.faceModel; #endregion #region Events public event Action ShowSpeechBubble; public event Action HideSpeechBubble; public event Action OnPlayerVicinityChanged; private NPCGroup _npcGroup; private ConvaiGroupNPCController otherNPC; #endregion #region Public Attributes public bool CanRelayMessage { get; set; } = true; public NPC2NPCConversationManager ConversationManager { get; set; } public bool IsInConversationWithAnotherNPC { get; set; } public string CharacterName => ConvaiNPC == null ? string.Empty : ConvaiNPC.characterName; public string CharacterID => ConvaiNPC == null ? string.Empty : ConvaiNPC.characterID; public ConvaiNPC ConvaiNPC { get; private set; } #endregion } }