From 73b921fc9b496dc38467ca39dc9754dd7d670e85 Mon Sep 17 00:00:00 2001 From: "tom.hempel" Date: Thu, 23 Oct 2025 03:08:51 +0200 Subject: [PATCH] enhanced UDP audio sender/receiver scripts with metrics for packet tracking and improved NPC assignment logic --- .../NetworkConfig.asset | 0 .../NetworkConfig.asset.meta | 0 .../Resources/OVROverlayCanvasSettings.asset | 3 + .../OVROverlayCanvasSettings.asset.meta | 8 + Unity-Master/Assets/Scenes/VR_Player1.unity | 4 +- Unity-Master/Assets/Scenes/VR_Player2.unity | 4 +- .../ConvaiSimpleUDPAudioReceiver.cs | 76 ++- .../Multiplayer/ConvaiSimpleUDPAudioSender.cs | 14 + .../Multiplayer/ConvaiUDPSpeechReceiver.cs | 12 + .../Multiplayer/ConvaiUDPSpeechSender.cs | 65 ++- .../Scripts/Multiplayer/NetworkDebugUI.cs | 541 ++++++++++++++++++ .../Multiplayer/NetworkDebugUI.cs.meta | 12 + .../Scripts/Multiplayer/UDPPeerDiscovery.cs | 24 + 13 files changed, 751 insertions(+), 12 deletions(-) rename Unity-Master/Assets/{Scripts => Resources}/NetworkConfig.asset (100%) rename Unity-Master/Assets/{Scripts => Resources}/NetworkConfig.asset.meta (100%) create mode 100644 Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset create mode 100644 Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset.meta create mode 100644 Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs create mode 100644 Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs.meta diff --git a/Unity-Master/Assets/Scripts/NetworkConfig.asset b/Unity-Master/Assets/Resources/NetworkConfig.asset similarity index 100% rename from Unity-Master/Assets/Scripts/NetworkConfig.asset rename to Unity-Master/Assets/Resources/NetworkConfig.asset diff --git a/Unity-Master/Assets/Scripts/NetworkConfig.asset.meta b/Unity-Master/Assets/Resources/NetworkConfig.asset.meta similarity index 100% rename from Unity-Master/Assets/Scripts/NetworkConfig.asset.meta rename to Unity-Master/Assets/Resources/NetworkConfig.asset.meta diff --git a/Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset b/Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset new file mode 100644 index 0000000..070114c --- /dev/null +++ b/Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:603f123f56ba400be5e87b4edbc4c4268ad6c3ef08d2c39d3a7459bb58d7eab2 +size 563 diff --git a/Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset.meta b/Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset.meta new file mode 100644 index 0000000..fe59c00 --- /dev/null +++ b/Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 44c8d4b7ac986c847a36bd9a8d84f4b6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity-Master/Assets/Scenes/VR_Player1.unity b/Unity-Master/Assets/Scenes/VR_Player1.unity index 1ce0563..d41a050 100644 --- a/Unity-Master/Assets/Scenes/VR_Player1.unity +++ b/Unity-Master/Assets/Scenes/VR_Player1.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73f7f8d41a48b26bc6f3c72b510df5b06b9e15c0dc24b41dc0f5af2b0fc51364 -size 4417549 +oid sha256:16e51a6e0ccc8302e2b4c5ceb564a85bd512a0407226c683aa2912c6d62349bd +size 4412088 diff --git a/Unity-Master/Assets/Scenes/VR_Player2.unity b/Unity-Master/Assets/Scenes/VR_Player2.unity index aae3d01..c5bea02 100644 --- a/Unity-Master/Assets/Scenes/VR_Player2.unity +++ b/Unity-Master/Assets/Scenes/VR_Player2.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c223da993f58a887ca3fd5e8076ab6152857a003ae0c3280aa9a74cd7c621168 -size 4496524 +oid sha256:6581dcfe7ee24059333b4498c2e0eb5af29230ed600649dad768113b84ed7331 +size 4491929 diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs index 14bce23..10b62d8 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs @@ -28,6 +28,14 @@ namespace Convai.Scripts.Runtime.Multiplayer // Events public Action 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); + } + } + } + } + + /// + /// Finds an enabled ConvaiNPC in the scene (doesn't rely on ConvaiNPCManager raycasting) + /// + private ConvaiNPC FindEnabledConvaiNPC() + { + // Find all ConvaiNPC components in the scene (including inactive GameObjects) + var allNPCs = FindObjectsOfType(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); }); } diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs index 9f41b27..418de1c 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs @@ -70,6 +70,16 @@ namespace Convai.Scripts.Runtime.Multiplayer public event Action 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); diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs index d47a4e7..193c3fc 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs @@ -54,6 +54,14 @@ namespace Convai.Scripts.Runtime.Multiplayer public Action OnTranscriptReceived; public Action 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); } diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs index 6496f74..f66c285 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs @@ -51,6 +51,16 @@ namespace Convai.Scripts.Runtime.Multiplayer public Action OnSpeechTransmission; public Action 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); + } + } + } + } + + /// + /// Finds an enabled ConvaiNPC in the scene (doesn't rely on ConvaiNPCManager raycasting) + /// + private ConvaiNPC FindEnabledConvaiNPC() + { + // Find all ConvaiNPC components in the scene (including inactive GameObjects) + var allNPCs = FindObjectsOfType(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(); 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) diff --git a/Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs b/Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs new file mode 100644 index 0000000..e91ae08 --- /dev/null +++ b/Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs @@ -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 +{ + /// + /// In-game debug UI for network diagnostics in VR builds + /// Shows peer discovery, voice/speech status, and packet counters + /// + 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(); + _speechSender = FindBestComponent(); + _audioReceiver = FindBestComponent(); + _speechReceiver = FindBestComponent(); + } + + /// + /// Finds a component, prioritizing enabled ones in active hierarchies + /// + private T FindBestComponent() where T : MonoBehaviour + { + var allComponents = FindObjectsOfType(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.renderMode = RenderMode.WorldSpace; + + CanvasScaler scaler = canvasObj.AddComponent(); + scaler.dynamicPixelsPerUnit = 10; + + // Create panel background + _panel = new GameObject("DebugPanel"); + _panel.transform.SetParent(canvasObj.transform, false); + + Image panelImage = _panel.AddComponent(); + panelImage.color = new Color(0, 0, 0, 0.85f); + + RectTransform panelRect = _panel.GetComponent(); + panelRect.sizeDelta = new Vector2(800, 900); + + // Create text + GameObject textObj = new GameObject("DebugText"); + textObj.transform.SetParent(_panel.transform, false); + + _debugText = textObj.AddComponent(); + _debugText.font = Resources.GetBuiltinResource("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(); + 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(); + 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("{LeftHand}/secondaryButton"); + _toggleAction.AddBinding("{LeftHand}/secondaryButton"); + _toggleAction.AddBinding("{LeftHand}/secondaryButton"); + _toggleAction.AddBinding("{LeftHand}/secondaryButton"); + + // Also bind keyboard for editor testing + _toggleAction.AddBinding("/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); + } + } +} + diff --git a/Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs.meta b/Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs.meta new file mode 100644 index 0000000..2388d9e --- /dev/null +++ b/Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: + diff --git a/Unity-Master/Assets/Scripts/Multiplayer/UDPPeerDiscovery.cs b/Unity-Master/Assets/Scripts/Multiplayer/UDPPeerDiscovery.cs index aa486f9..311d7e7 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/UDPPeerDiscovery.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/UDPPeerDiscovery.cs @@ -58,12 +58,19 @@ namespace Convai.Scripts.Runtime.Multiplayer Lost } + // Event log for debugging + private System.Collections.Generic.List _eventLog = new System.Collections.Generic.List(); + 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 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); + } + } } }