enhanced UDP audio sender/receiver scripts with metrics for packet tracking and improved NPC assignment logic

This commit is contained in:
tom.hempel
2025-10-23 03:08:51 +02:00
parent 6a99392e34
commit 73b921fc9b
13 changed files with 751 additions and 12 deletions

BIN
Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 44c8d4b7ac986c847a36bd9a8d84f4b6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.

View File

@ -28,6 +28,14 @@ namespace Convai.Scripts.Runtime.Multiplayer
// Events
public Action<bool> OnAudioReceiving;
// Metrics for debug UI
private int _totalPacketsReceived = 0;
private DateTime _lastPacketReceivedTime;
public int TotalPacketsReceived => _totalPacketsReceived;
public float TimeSinceLastReceive => _lastPacketReceivedTime != default ?
(float)(DateTime.UtcNow - _lastPacketReceivedTime).TotalSeconds : -1f;
public int ListenPort => listenPort;
// Network components
private UdpClient _udpListener;
private IPEndPoint _remoteEndPoint;
@ -107,6 +115,17 @@ namespace Convai.Scripts.Runtime.Multiplayer
_cancellationTokenSource = new CancellationTokenSource();
}
StartListening();
// Immediately try to assign an enabled NPC
if (useActiveNPC && targetNPC == null)
{
var currentActiveNPC = FindEnabledConvaiNPC();
if (currentActiveNPC != null)
{
targetNPC = currentActiveNPC;
ConvaiLogger.Info($"🔄 UDP Audio Receiver assigned target NPC on enable: {targetNPC.characterName} (on {targetNPC.gameObject.name})", ConvaiLogger.LogCategory.Character);
}
}
}
private void OnDestroy()
@ -135,6 +154,47 @@ namespace Convai.Scripts.Runtime.Multiplayer
{
StopTalkingSimulation();
}
// Continuously update target NPC if using active NPC mode
if (useActiveNPC)
{
var currentActiveNPC = FindEnabledConvaiNPC();
// Update whenever the active NPC changes (including null → NPC or NPC → different NPC)
if (currentActiveNPC != targetNPC)
{
targetNPC = currentActiveNPC;
if (targetNPC != null)
{
ConvaiLogger.Info($"🔄 UDP Audio Receiver updated target NPC to: {targetNPC.characterName} (on {targetNPC.gameObject.name})", ConvaiLogger.LogCategory.Character);
}
else
{
ConvaiLogger.Info($"🔄 UDP Audio Receiver cleared target NPC", ConvaiLogger.LogCategory.Character);
}
}
}
}
/// <summary>
/// Finds an enabled ConvaiNPC in the scene (doesn't rely on ConvaiNPCManager raycasting)
/// </summary>
private ConvaiNPC FindEnabledConvaiNPC()
{
// Find all ConvaiNPC components in the scene (including inactive GameObjects)
var allNPCs = FindObjectsOfType<ConvaiNPC>(true);
// Return the first one that's on an active GameObject
foreach (var npc in allNPCs)
{
if (npc.gameObject.activeInHierarchy && npc.enabled)
{
return npc;
}
}
return null;
}
private void InitializeNetwork()
@ -151,10 +211,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
private void InitializeConvai()
{
// Get target NPC
// Get target NPC by finding enabled NPCs in the scene
if (useActiveNPC)
{
targetNPC = ConvaiNPCManager.Instance?.GetActiveConvaiNPC();
targetNPC = FindEnabledConvaiNPC();
}
if (targetNPC == null)
@ -163,7 +223,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
}
else
{
ConvaiLogger.Info($"UDP Audio Receiver V2 initialized with NPC: {targetNPC.characterName}", ConvaiLogger.LogCategory.Character);
ConvaiLogger.Info($"UDP Audio Receiver V2 initialized with NPC: {targetNPC.characterName} (on {targetNPC.gameObject.name})", ConvaiLogger.LogCategory.Character);
}
}
@ -238,6 +298,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
var packet = packetData.Value;
_lastPacketTime = Time.time;
// Update metrics
_totalPacketsReceived++;
_lastPacketReceivedTime = DateTime.UtcNow;
if (enableDebugLogging)
{
if (packet.isEndSignal)
@ -299,10 +363,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
if (_isReceivingAudio) return;
MainThreadDispatcher.Instance.RunOnMainThread(() => {
// Update target NPC if using active NPC
// Update target NPC by finding enabled NPCs in the scene
if (useActiveNPC)
{
targetNPC = ConvaiNPCManager.Instance?.GetActiveConvaiNPC();
targetNPC = FindEnabledConvaiNPC();
}
if (targetNPC == null)
@ -324,7 +388,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
// This is the KEY! Simulate a talk key press to trigger normal Convai flow
ConvaiInputManager.Instance.talkKeyInteract?.Invoke(true);
ConvaiLogger.Info($"🎤 Started talking simulation for {targetNPC.characterName} (remote player audio)", ConvaiLogger.LogCategory.Character);
ConvaiLogger.Info($"🎤 Started talking simulation for {targetNPC.characterName} (on {targetNPC.gameObject.name}) (remote player audio)", ConvaiLogger.LogCategory.Character);
});
}

View File

@ -70,6 +70,16 @@ namespace Convai.Scripts.Runtime.Multiplayer
public event Action<bool> OnRecordingStateChanged;
// Metrics for debug UI
private int _totalPacketsSent = 0;
private DateTime _lastPacketSentTime;
public int TotalPacketsSent => _totalPacketsSent;
public float TimeSinceLastSend => _lastPacketSentTime != default ?
(float)(DateTime.UtcNow - _lastPacketSentTime).TotalSeconds : -1f;
public string CurrentTargetIP => targetIP;
public int CurrentTargetPort => targetPort;
public bool UsingDiscovery => NetworkConfig.Instance?.useAutoDiscovery ?? false;
[Header("Recording Storage")]
[SerializeField] private bool saveLocalAudio = true;
[SerializeField] private int localSampleRate = 16000;
@ -609,6 +619,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
// Send the packet
await _udpClient.SendAsync(packet, packet.Length, _targetEndPoint);
// Update metrics
_totalPacketsSent++;
_lastPacketSentTime = DateTime.UtcNow;
if (enableDebugLogging && _packetSequence % 10 == 0) // Log every 10th packet
{
ConvaiLogger.DebugLog($"Sent packet {_packetSequence} with {currentChunkSamples} samples", ConvaiLogger.LogCategory.Character);

View File

@ -54,6 +54,14 @@ namespace Convai.Scripts.Runtime.Multiplayer
public Action<string> OnTranscriptReceived;
public Action<AudioClip> OnAudioClipReceived;
// Metrics for debug UI
private int _totalClipsReceived = 0;
private DateTime _lastClipReceivedTime;
public int TotalClipsReceived => _totalClipsReceived;
public float TimeSinceLastReceive => _lastClipReceivedTime != default ?
(float)(DateTime.UtcNow - _lastClipReceivedTime).TotalSeconds : -1f;
public int ListenPort => listenPort;
// Data structures
private struct SpeechPacket
{
@ -446,6 +454,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
OnAudioClipReceived?.Invoke(clip);
// Update metrics
_totalClipsReceived++;
_lastClipReceivedTime = DateTime.UtcNow;
if (enableDebugLogging)
ConvaiLogger.DebugLog($"✅ Reconstructed audio clip {sequence}: {clip.length:F2}s, '{incomingClip.transcript}'", ConvaiLogger.LogCategory.Character);
}

View File

@ -51,6 +51,16 @@ namespace Convai.Scripts.Runtime.Multiplayer
public Action<bool> OnSpeechTransmission;
public Action<string> OnSpeechSent;
// Metrics for debug UI
private int _totalClipsSent = 0;
private DateTime _lastClipSentTime;
public int TotalClipsSent => _totalClipsSent;
public float TimeSinceLastSend => _lastClipSentTime != default ?
(float)(DateTime.UtcNow - _lastClipSentTime).TotalSeconds : -1f;
public string CurrentTargetIP => targetIP;
public int CurrentTargetPort => targetPort;
public bool UsingDiscovery => NetworkConfig.Instance?.useAutoDiscovery ?? false;
private void Start()
{
// Get network config from global instance
@ -92,6 +102,53 @@ namespace Convai.Scripts.Runtime.Multiplayer
CleanupNetwork();
}
private void Update()
{
// Continuously update source NPC if using active NPC mode
if (useActiveNPC)
{
var currentActiveNPC = FindEnabledConvaiNPC();
if (currentActiveNPC != sourceNPC)
{
// Cleanup old subscriptions
CleanupNPCSubscriptions();
// Update to new NPC
sourceNPC = currentActiveNPC;
SubscribeToNPCEvents();
if (sourceNPC != null)
{
ConvaiLogger.Info($"🔄 UDP Speech Sender updated source NPC to: {sourceNPC.characterName} (on {sourceNPC.gameObject.name})", ConvaiLogger.LogCategory.Character);
}
else
{
ConvaiLogger.Info($"🔄 UDP Speech Sender cleared source NPC", ConvaiLogger.LogCategory.Character);
}
}
}
}
/// <summary>
/// Finds an enabled ConvaiNPC in the scene (doesn't rely on ConvaiNPCManager raycasting)
/// </summary>
private ConvaiNPC FindEnabledConvaiNPC()
{
// Find all ConvaiNPC components in the scene (including inactive GameObjects)
var allNPCs = FindObjectsOfType<ConvaiNPC>(true);
// Return the first one that's on an active GameObject
foreach (var npc in allNPCs)
{
if (npc.gameObject.activeInHierarchy && npc.enabled)
{
return npc;
}
}
return null;
}
private void HandlePeerDiscovered(string peerIP)
{
targetIP = peerIP;
@ -128,7 +185,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
private void InitializeConvai()
{
// Prefer local ConvaiNPC on the same GameObject, then fall back to active NPC
// Prefer local ConvaiNPC on the same GameObject, then fall back to finding enabled NPC
var localNPC = GetComponent<ConvaiNPC>();
if (localNPC != null)
{
@ -136,7 +193,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
}
else if (useActiveNPC)
{
sourceNPC = ConvaiNPCManager.Instance?.GetActiveConvaiNPC();
sourceNPC = FindEnabledConvaiNPC();
}
SubscribeToNPCEvents();
@ -259,6 +316,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
// Only increment sequence after the entire clip is sent
_speechSequence++;
// Update metrics
_totalClipsSent++;
_lastClipSentTime = DateTime.UtcNow;
OnSpeechSent?.Invoke(transcript);
if (enableDebugLogging)

View File

@ -0,0 +1,541 @@
using System;
using System.Text;
using Convai.Scripts.Runtime.Multiplayer;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace Convai.Scripts.Runtime.Multiplayer
{
/// <summary>
/// In-game debug UI for network diagnostics in VR builds
/// Shows peer discovery, voice/speech status, and packet counters
/// </summary>
public class NetworkDebugUI : MonoBehaviour
{
[Header("UI Configuration")]
[SerializeField] private bool showOnStart = true;
[SerializeField] private KeyCode toggleKey = KeyCode.F1;
[SerializeField] private bool useVRInput = true;
[Header("UI Positioning (VR)")]
[SerializeField] private float distanceFromCamera = 2.5f;
[SerializeField] private float verticalOffset = 0.5f;
[SerializeField] private Vector3 rotationOffset = new Vector3(0, 0, 0);
[Header("Update Settings")]
[SerializeField] private float updateInterval = 0.5f; // Update UI twice per second
// UI Components
private Canvas _canvas;
private Text _debugText;
private GameObject _panel;
private Camera _mainCamera;
// Component references
private UDPPeerDiscovery _peerDiscovery;
private ConvaiSimpleUDPAudioSender _audioSender;
private ConvaiUDPSpeechSender _speechSender;
private ConvaiSimpleUDPAudioReceiver _audioReceiver;
private ConvaiUDPSpeechReceiver _speechReceiver;
// State
private bool _isVisible = true;
private float _lastUpdateTime;
private InputAction _toggleAction;
private void Start()
{
_mainCamera = Camera.main;
// Find components
FindNetworkComponents();
// Create UI
CreateDebugUI();
_isVisible = showOnStart;
_panel.SetActive(_isVisible);
// Setup VR input for toggle
if (useVRInput)
{
SetupVRToggleInput();
}
UpdateDebugInfo();
}
private void FindNetworkComponents()
{
_peerDiscovery = UDPPeerDiscovery.Instance;
// Always re-scan for the best (most active) component
// This is important for components on NPCs that get enabled/disabled
_audioSender = FindBestComponent<ConvaiSimpleUDPAudioSender>();
_speechSender = FindBestComponent<ConvaiUDPSpeechSender>();
_audioReceiver = FindBestComponent<ConvaiSimpleUDPAudioReceiver>();
_speechReceiver = FindBestComponent<ConvaiUDPSpeechReceiver>();
}
/// <summary>
/// Finds a component, prioritizing enabled ones in active hierarchies
/// </summary>
private T FindBestComponent<T>() where T : MonoBehaviour
{
var allComponents = FindObjectsOfType<T>(true); // Include inactive
if (allComponents.Length == 0)
return null;
// Priority 1: Enabled component in active hierarchy
foreach (var comp in allComponents)
{
if (comp.enabled && comp.gameObject.activeInHierarchy)
return comp;
}
// Priority 2: Component on active GameObject (even if component disabled)
foreach (var comp in allComponents)
{
if (comp.gameObject.activeInHierarchy)
return comp;
}
// Priority 3: Any component (even if GameObject is inactive)
return allComponents[0];
}
private void CreateDebugUI()
{
// Create canvas
GameObject canvasObj = new GameObject("NetworkDebugCanvas");
canvasObj.transform.SetParent(transform);
_canvas = canvasObj.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.WorldSpace;
CanvasScaler scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.dynamicPixelsPerUnit = 10;
// Create panel background
_panel = new GameObject("DebugPanel");
_panel.transform.SetParent(canvasObj.transform, false);
Image panelImage = _panel.AddComponent<Image>();
panelImage.color = new Color(0, 0, 0, 0.85f);
RectTransform panelRect = _panel.GetComponent<RectTransform>();
panelRect.sizeDelta = new Vector2(800, 900);
// Create text
GameObject textObj = new GameObject("DebugText");
textObj.transform.SetParent(_panel.transform, false);
_debugText = textObj.AddComponent<Text>();
_debugText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
_debugText.fontSize = 18;
_debugText.color = Color.white;
_debugText.alignment = TextAnchor.UpperLeft;
_debugText.horizontalOverflow = HorizontalWrapMode.Overflow;
_debugText.verticalOverflow = VerticalWrapMode.Overflow;
RectTransform textRect = textObj.GetComponent<RectTransform>();
textRect.anchorMin = new Vector2(0, 0);
textRect.anchorMax = new Vector2(1, 1);
textRect.offsetMin = new Vector2(20, 20);
textRect.offsetMax = new Vector2(-20, -20);
// Setup canvas transform for VR
RectTransform canvasRect = _canvas.GetComponent<RectTransform>();
canvasRect.sizeDelta = new Vector2(800, 900);
canvasRect.localScale = Vector3.one * 0.001f; // Scale down for VR viewing
}
private void SetupVRToggleInput()
{
try
{
// Create input action for Y/B button (left controller secondary button)
_toggleAction = new InputAction("ToggleDebug", InputActionType.Button);
// Bind to left controller secondary button (Y on Quest)
_toggleAction.AddBinding("<XRController>{LeftHand}/secondaryButton");
_toggleAction.AddBinding("<OculusTouchController>{LeftHand}/secondaryButton");
_toggleAction.AddBinding("<MetaTouchController>{LeftHand}/secondaryButton");
_toggleAction.AddBinding("<QuestProTouchController>{LeftHand}/secondaryButton");
// Also bind keyboard for editor testing
_toggleAction.AddBinding("<Keyboard>/f1");
_toggleAction.started += ctx => ToggleVisibility();
_toggleAction.Enable();
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to setup VR toggle input: {ex.Message}");
}
}
private void Update()
{
// Handle keyboard toggle
if (Input.GetKeyDown(toggleKey))
{
ToggleVisibility();
}
// Update UI text periodically
if (Time.time - _lastUpdateTime >= updateInterval)
{
UpdateDebugInfo();
_lastUpdateTime = Time.time;
}
// Position UI in front of camera
if (_isVisible && _mainCamera != null)
{
PositionUIInFrontOfCamera();
}
}
private void PositionUIInFrontOfCamera()
{
Vector3 cameraForward = _mainCamera.transform.forward;
Vector3 cameraRight = _mainCamera.transform.right;
Vector3 cameraUp = _mainCamera.transform.up;
// Position in front of camera
Vector3 targetPosition = _mainCamera.transform.position +
cameraForward * distanceFromCamera +
cameraUp * verticalOffset;
_canvas.transform.position = targetPosition;
// Rotate to face camera
_canvas.transform.rotation = Quaternion.LookRotation(cameraForward, cameraUp);
_canvas.transform.Rotate(rotationOffset);
}
private void ToggleVisibility()
{
_isVisible = !_isVisible;
_panel.SetActive(_isVisible);
}
private void UpdateDebugInfo()
{
if (_debugText == null) return;
// Re-check for components that might have been disabled at startup
FindNetworkComponents();
StringBuilder sb = new StringBuilder();
// Header
sb.AppendLine("═══ NETWORK DEBUG ═══");
sb.AppendLine($"Time: {DateTime.Now:HH:mm:ss}");
sb.AppendLine();
// Peer Discovery Status
AppendPeerDiscoveryStatus(sb);
sb.AppendLine();
// Audio Sender (Voice Input)
AppendAudioSenderStatus(sb);
sb.AppendLine();
// Speech Sender (NPC Response)
AppendSpeechSenderStatus(sb);
sb.AppendLine();
// Audio Receiver
AppendAudioReceiverStatus(sb);
sb.AppendLine();
// Speech Receiver
AppendSpeechReceiverStatus(sb);
sb.AppendLine();
// Event Log
AppendEventLog(sb);
_debugText.text = sb.ToString();
}
private void AppendPeerDiscoveryStatus(StringBuilder sb)
{
sb.AppendLine("🔍 PEER DISCOVERY");
if (_peerDiscovery != null)
{
string stateColor = GetConnectionStateColor(_peerDiscovery.CurrentState);
string stateIcon = GetConnectionStateIcon(_peerDiscovery.CurrentState);
sb.AppendLine($"State: {_peerDiscovery.CurrentState} {stateIcon}");
sb.AppendLine($"Local Player ID: {_peerDiscovery.LocalPlayerID}");
sb.AppendLine($"Peer Player ID: {(_peerDiscovery.PeerPlayerID > 0 ? _peerDiscovery.PeerPlayerID.ToString() : "None")}");
sb.AppendLine($"Peer IP: {(_peerDiscovery.IsConnected ? _peerDiscovery.PeerIP : "None")}");
if (_peerDiscovery.IsConnected)
{
float timeSince = _peerDiscovery.TimeSinceLastPeerPacket;
sb.AppendLine($"Last Packet: {timeSince:F1}s ago");
}
}
else
{
sb.AppendLine("❌ NOT FOUND - Add UDPPeerDiscovery component!");
}
}
private void AppendAudioSenderStatus(StringBuilder sb)
{
sb.AppendLine("🎤 VOICE INPUT SENDER");
if (_audioSender != null && _audioSender.gameObject != null)
{
// Check if component's GameObject is active AND enabled in hierarchy
bool isActive = _audioSender.enabled && _audioSender.gameObject.activeInHierarchy;
if (!isActive)
{
sb.AppendLine("⏸️ FOUND BUT DISABLED");
sb.AppendLine($"(GameObject: {_audioSender.gameObject.name})");
sb.AppendLine($"(Active: {_audioSender.gameObject.activeSelf}, InHierarchy: {_audioSender.gameObject.activeInHierarchy})");
}
else
{
sb.AppendLine($"GameObject: {_audioSender.gameObject.name} ✅");
sb.AppendLine($"Target: {_audioSender.CurrentTargetIP}:{_audioSender.CurrentTargetPort}");
sb.AppendLine($"Recording: {(_audioSender.IsRecording ? "YES " : "NO")}");
sb.AppendLine($"Packets Sent: {_audioSender.TotalPacketsSent}");
float timeSince = _audioSender.TimeSinceLastSend;
if (timeSince >= 0)
{
sb.AppendLine($"Last Send: {timeSince:F1}s ago");
}
else
{
sb.AppendLine($"Last Send: Never");
}
sb.AppendLine($"Using Discovery: {(_audioSender.UsingDiscovery ? "YES" : "NO")}");
}
}
else
{
sb.AppendLine("❌ NOT FOUND IN SCENE");
}
}
private void AppendSpeechSenderStatus(StringBuilder sb)
{
sb.AppendLine("🔊 SPEECH SENDER");
if (_speechSender != null && _speechSender.gameObject != null)
{
// Check if component's GameObject is active AND enabled in hierarchy
bool isActive = _speechSender.enabled && _speechSender.gameObject.activeInHierarchy;
if (!isActive)
{
sb.AppendLine("⏸️ FOUND BUT DISABLED");
sb.AppendLine($"(GameObject: {_speechSender.gameObject.name})");
sb.AppendLine($"(Active: {_speechSender.gameObject.activeSelf}, InHierarchy: {_speechSender.gameObject.activeInHierarchy})");
}
else
{
sb.AppendLine($"GameObject: {_speechSender.gameObject.name} ✅");
sb.AppendLine($"Target: {_speechSender.CurrentTargetIP}:{_speechSender.CurrentTargetPort}");
// Show source NPC
var sourceNPC = _speechSender.SourceNPC;
if (sourceNPC != null)
{
sb.AppendLine($"Source NPC: {sourceNPC.characterName} ✅");
}
else
{
sb.AppendLine($"Source NPC: None ⚠️");
}
sb.AppendLine($"Transmitting: {(_speechSender.IsSendingSpeech ? "YES " : "NO")}");
sb.AppendLine($"Clips Sent: {_speechSender.TotalClipsSent}");
float timeSince = _speechSender.TimeSinceLastSend;
if (timeSince >= 0)
{
sb.AppendLine($"Last Send: {timeSince:F1}s ago");
}
else
{
sb.AppendLine($"Last Send: Never");
}
sb.AppendLine($"Using Discovery: {(_speechSender.UsingDiscovery ? "YES" : "NO")}");
}
}
else
{
sb.AppendLine("❌ NOT FOUND IN SCENE");
}
}
private void AppendAudioReceiverStatus(StringBuilder sb)
{
sb.AppendLine("🎧 VOICE INPUT RECEIVER");
if (_audioReceiver != null && _audioReceiver.gameObject != null)
{
// Check if component's GameObject is active AND enabled in hierarchy
bool isActive = _audioReceiver.enabled && _audioReceiver.gameObject.activeInHierarchy;
if (!isActive)
{
sb.AppendLine("⏸️ FOUND BUT DISABLED");
sb.AppendLine($"(GameObject: {_audioReceiver.gameObject.name})");
sb.AppendLine($"(Active: {_audioReceiver.gameObject.activeSelf}, InHierarchy: {_audioReceiver.gameObject.activeInHierarchy})");
}
else
{
sb.AppendLine($"GameObject: {_audioReceiver.gameObject.name} ✅");
sb.AppendLine($"Listen Port: {_audioReceiver.ListenPort}");
// Show active NPC
var targetNPC = _audioReceiver.TargetNPC;
if (targetNPC != null)
{
sb.AppendLine($"Active NPC: {targetNPC.characterName} ✅");
}
else
{
sb.AppendLine($"Active NPC: None ⚠️");
}
sb.AppendLine($"Receiving: {(_audioReceiver.IsReceivingAudio ? "YES " : "NO")}");
sb.AppendLine($"Packets Received: {_audioReceiver.TotalPacketsReceived}");
float timeSince = _audioReceiver.TimeSinceLastReceive;
if (timeSince >= 0)
{
sb.AppendLine($"Last Receive: {timeSince:F1}s ago");
}
else
{
sb.AppendLine($"Last Receive: Never");
}
}
}
else
{
sb.AppendLine("❌ NOT FOUND IN SCENE");
}
}
private void AppendSpeechReceiverStatus(StringBuilder sb)
{
sb.AppendLine("🔉 SPEECH RECEIVER");
if (_speechReceiver != null && _speechReceiver.gameObject != null)
{
// Check if component's GameObject is active AND enabled in hierarchy
bool isActive = _speechReceiver.enabled && _speechReceiver.gameObject.activeInHierarchy;
if (!isActive)
{
sb.AppendLine("⏸️ FOUND BUT DISABLED");
sb.AppendLine($"(GameObject: {_speechReceiver.gameObject.name})");
sb.AppendLine($"(Active: {_speechReceiver.gameObject.activeSelf}, InHierarchy: {_speechReceiver.gameObject.activeInHierarchy})");
}
else
{
sb.AppendLine($"GameObject: {_speechReceiver.gameObject.name} ✅");
sb.AppendLine($"Listen Port: {_speechReceiver.ListenPort}");
sb.AppendLine($"Playing: {(_speechReceiver.IsPlayingSequence ? "YES " : "NO")}");
sb.AppendLine($"Clips Received: {_speechReceiver.TotalClipsReceived}");
sb.AppendLine($"Queued Clips: {_speechReceiver.QueuedClipCount}");
float timeSince = _speechReceiver.TimeSinceLastReceive;
if (timeSince >= 0)
{
sb.AppendLine($"Last Receive: {timeSince:F1}s ago");
}
else
{
sb.AppendLine($"Last Receive: Never");
}
}
}
else
{
sb.AppendLine("❌ NOT FOUND IN SCENE");
}
}
private void AppendEventLog(StringBuilder sb)
{
sb.AppendLine("📋 EVENT LOG");
if (_peerDiscovery != null && _peerDiscovery.EventLog.Count > 0)
{
int startIndex = Math.Max(0, _peerDiscovery.EventLog.Count - 8);
for (int i = startIndex; i < _peerDiscovery.EventLog.Count; i++)
{
sb.AppendLine(_peerDiscovery.EventLog[i]);
}
}
else
{
sb.AppendLine("No events yet");
}
}
private string GetConnectionStateColor(UDPPeerDiscovery.ConnectionState state)
{
return state switch
{
UDPPeerDiscovery.ConnectionState.Connected => "green",
UDPPeerDiscovery.ConnectionState.Discovering => "yellow",
UDPPeerDiscovery.ConnectionState.Lost => "red",
_ => "white"
};
}
private string GetConnectionStateIcon(UDPPeerDiscovery.ConnectionState state)
{
return state switch
{
UDPPeerDiscovery.ConnectionState.Connected => "✅",
UDPPeerDiscovery.ConnectionState.Discovering => "⏳",
UDPPeerDiscovery.ConnectionState.Lost => "❌",
UDPPeerDiscovery.ConnectionState.Disconnected => "⭕",
_ => "❓"
};
}
private void OnDestroy()
{
if (_toggleAction != null)
{
_toggleAction.Disable();
_toggleAction.Dispose();
}
}
// Public methods for external control
public void Show()
{
_isVisible = true;
_panel.SetActive(true);
}
public void Hide()
{
_isVisible = false;
_panel.SetActive(false);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -58,12 +58,19 @@ namespace Convai.Scripts.Runtime.Multiplayer
Lost
}
// Event log for debugging
private System.Collections.Generic.List<string> _eventLog = new System.Collections.Generic.List<string>();
private const int MAX_LOG_ENTRIES = 20;
// Public properties
public string PeerIP => _peerIP;
public byte LocalPlayerID => localPlayerID;
public byte PeerPlayerID => _peerPlayerID;
public ConnectionState CurrentState => _connectionState;
public bool IsConnected => _connectionState == ConnectionState.Connected;
public float TimeSinceLastPeerPacket => _connectionState == ConnectionState.Connected ?
(float)(DateTime.UtcNow - _lastPeerPacketTime).TotalSeconds : -1f;
public System.Collections.Generic.List<string> EventLog => _eventLog;
private void Awake()
{
@ -91,6 +98,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
}
_cancellationTokenSource = new CancellationTokenSource();
LogEvent($"🔍 Discovery started (Player {localPlayerID})");
StartDiscovery();
}
@ -401,6 +409,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
SetConnectionState(ConnectionState.Connected);
ConvaiLogger.Info($"✅ Peer discovered! Player {peerPlayerID} at {peerIP}", ConvaiLogger.LogCategory.Character);
LogEvent($"✅ Peer discovered! Player {peerPlayerID} at {peerIP}");
// Notify listeners
OnPeerDiscovered?.Invoke(peerIP);
@ -409,6 +418,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
private void HandlePeerLost()
{
ConvaiLogger.Warn($"⚠️ Peer connection lost (Player {_peerPlayerID} at {_peerIP})", ConvaiLogger.LogCategory.Character);
LogEvent($"⚠️ Peer connection lost (Player {_peerPlayerID})");
string lostPeerIP = _peerIP;
_peerIP = "";
@ -441,6 +451,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
public void RestartDiscovery()
{
ConvaiLogger.Info("Manually restarting peer discovery", ConvaiLogger.LogCategory.Character);
LogEvent("🔄 Manually restarting discovery");
_peerIP = "";
_peerPlayerID = 0;
SetConnectionState(ConnectionState.Discovering);
@ -455,6 +466,19 @@ namespace Convai.Scripts.Runtime.Multiplayer
ConvaiLogger.Info($"Peer Player ID: {_peerPlayerID}", ConvaiLogger.LogCategory.Character);
ConvaiLogger.Info($"Listen Port: {_listenPort}", ConvaiLogger.LogCategory.Character);
}
private void LogEvent(string message)
{
string timestamp = DateTime.Now.ToString("HH:mm:ss");
string logEntry = $"[{timestamp}] {message}";
_eventLog.Add(logEntry);
// Keep only last N entries
if (_eventLog.Count > MAX_LOG_ENTRIES)
{
_eventLog.RemoveAt(0);
}
}
}
}