Files
Bachelor-Arbeit-Luca-Haas/Assets/Convai/Scripts/Runtime/Features/NPC2NPC/ConvaiGroupNPCController.cs
2025-07-03 11:02:29 +02:00

227 lines
8.5 KiB
C#

using System;
using System.Collections;
using System.Threading.Tasks;
using Convai.Scripts;
using Convai.Scripts.Utils;
using Convai.Scripts.Utils.LipSync;
using Service;
using UnityEngine;
using Logger = Convai.Scripts.Utils.Logger;
/// <summary>
/// This class is responsible for handling out all the tasks related to NPC to NPC conversation for a NPC of a group
/// </summary>
public class ConvaiGroupNPCController : MonoBehaviour
{
#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<string> ShowSpeechBubble;
public event Action HideSpeechBubble;
public event Action<bool, ConvaiGroupNPCController> OnPlayerVicinityChanged;
#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
/// <summary>
/// Used to set Player GameObject Transform and lip-sync
/// </summary>
private void Awake()
{
if (playerTransform == null) playerTransform = Camera.main.transform;
ConvaiNPC = GetComponent<ConvaiNPC>();
CONVERSATION_DISTANCE_THRESHOLD = conversationDistanceThreshold == 0 ? Mathf.Infinity : conversationDistanceThreshold;
TryGetComponent(out _lipSync);
}
/// <summary>
/// Starts coroutine for player vicinity check and subscribe to necessary events
/// </summary>
private void Start()
{
_checkPlayerVicinityCoroutine = StartCoroutine(CheckPlayerVicinity());
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio))
{
convaiNPCAudio.OnAudioTranscriptAvailable += HandleAudioTranscriptAvailable;
}
}
/// <summary>
/// Unsubscribes to the events and stops the coroutine
/// </summary>
private void OnDestroy()
{
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio))
{
convaiNPCAudio.OnAudioTranscriptAvailable -= HandleAudioTranscriptAvailable;
}
if (_checkPlayerVicinityCoroutine != null)
{
StopCoroutine(_checkPlayerVicinityCoroutine);
}
}
/// <summary>
/// Shows speech bubble and adds the received text to final transcript
/// </summary>
/// <param name="transcript"></param>
private void HandleAudioTranscriptAvailable(string transcript)
{
if (IsInConversationWithAnotherNPC)
ShowSpeechBubble?.Invoke(transcript);
_finalResponseText += transcript;
}
/// <summary>
/// Attaches the speech bubble to the NPC game-object
/// </summary>
public void AttachSpeechBubble()
{
if (TryGetComponent(out ConvaiSpeechBubbleController _)) return;
gameObject.AddComponent<ConvaiSpeechBubbleController>().Initialize(speechBubblePrefab, this);
}
/// <summary>
/// Destroys the speech bubble game-object
/// </summary>
public void DetachSpeechBubble()
{
if (TryGetComponent(out ConvaiSpeechBubbleController convaiSpeechBubble)) Destroy(convaiSpeechBubble);
}
/// <summary>
/// Store the references of the client and subscribe to the necessary events
/// </summary>
/// <param name="client"></param>
public void InitializeNpc2NpcGrpcClient(NPC2NPCGRPCClient client)
{
_npc2NPCGrpcClient = client;
}
/// <summary>
/// Every 0.5 seconds updates if player is near or not and fire events according to the state
/// </summary>
/// <returns></returns>
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
Debug.Log($"Player is currently near {ConvaiNPC.characterName}: {isPlayerCurrentlyNear}");
}
previousPlayerPosition = currentPlayerPosition; // Update the player's previous position
// Check every half second
}
yield return new WaitForSeconds(0.5f);
}
}
/// <summary>
/// Sends the text to the other NPC in the group
/// </summary>
/// <param name="message"></param>
public async void SendTextDataNPC2NPC(string message)
{
if (_npc2NPCGrpcClient == null)
{
Logger.Warn("No GRPC client initialized for this NPC.", Logger.LogCategory.Character);
return;
}
try
{
CanRelayMessage = false;
await Task.Delay(500);
await _npc2NPCGrpcClient.SendTextData(
userText: message,
characterID: ConvaiNPC.characterID,
sessionID: ConvaiNPC.sessionID,
isLipSyncActive: _lipSync != null,
faceModel: FaceModel);
}
catch (Exception ex)
{
Logger.Warn($"Error sending message data for NPC2NPC: {ex.Message}", Logger.LogCategory.Character);
}
}
public void EndOfResponseReceived()
{
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio))
{
convaiNPCAudio.OnCharacterTalkingChanged += SendFinalTranscriptToOtherNPC;
}
}
private void SendFinalTranscriptToOtherNPC(bool isTalking)
{
if (IsInConversationWithAnotherNPC)
{
if (!isTalking)
{
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio))
{
convaiNPCAudio.OnCharacterTalkingChanged -= SendFinalTranscriptToOtherNPC;
}
ConversationManager.RelayMessage(_finalResponseText, this);
_finalResponseText = "";
HideSpeechBubble?.Invoke();
}
else
{
Logger.DebugLog($"{ConvaiNPC.characterName} is currently still talking. ", Logger.LogCategory.Character);
}
}
}
public bool IsPlayerNearMe()
{
bool result = Vector3.Distance(transform.position, playerTransform.position) < CONVERSATION_DISTANCE_THRESHOLD;
Logger.Info($"Player is near {CharacterName}: {result}", Logger.LogCategory.Character);
return result;
}
}