enhanced UDP audio sender/receiver scripts with metrics for packet tracking and improved NPC assignment logic
This commit is contained in:
BIN
Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset
(Stored with Git LFS)
Normal file
BIN
Unity-Master/Assets/Resources/OVROverlayCanvasSettings.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44c8d4b7ac986c847a36bd9a8d84f4b6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Unity-Master/Assets/Scenes/VR_Player1.unity
(Stored with Git LFS)
BIN
Unity-Master/Assets/Scenes/VR_Player1.unity
(Stored with Git LFS)
Binary file not shown.
BIN
Unity-Master/Assets/Scenes/VR_Player2.unity
(Stored with Git LFS)
BIN
Unity-Master/Assets/Scenes/VR_Player2.unity
(Stored with Git LFS)
Binary file not shown.
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
541
Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs
Normal file
541
Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user