Compare commits
16 Commits
6a99392e34
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 793a00efe7 | |||
| 272cbfdca8 | |||
| 761f6b1bfe | |||
| 639ac894ce | |||
| fae9ddc9af | |||
| 521c426da7 | |||
| dd50987678 | |||
| bac096c4e3 | |||
| 868180e3ec | |||
| f3b4a4ddb0 | |||
| 3f72973ff5 | |||
| 4ddb89d011 | |||
| 1274d6277d | |||
| 4d77a4753a | |||
| fd7e08679f | |||
| 73b921fc9b |
BIN
Master-P1.apk
BIN
Master-P1.apk
Binary file not shown.
BIN
Master-P2.apk
BIN
Master-P2.apk
Binary file not shown.
BIN
Unity-Master/Assets/Resources/NetworkConfig.asset
(Stored with Git LFS)
Normal file
BIN
Unity-Master/Assets/Resources/NetworkConfig.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
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.
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Runtime.Core;
|
||||
@ -28,8 +27,15 @@ 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;
|
||||
private bool _isListening = false;
|
||||
private int listenPort;
|
||||
@ -39,24 +45,19 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
private bool _isReceivingAudio = false;
|
||||
private int _expectedSequence = 0;
|
||||
private const uint MAGIC_NUMBER = 0xC0A1; // Simple magic number for packet validation
|
||||
private const uint ACK_MAGIC = 0xC0A2; // ACK magic to confirm START control
|
||||
|
||||
// Timing for auto-stop
|
||||
private float _lastPacketTime;
|
||||
private const float AUTO_STOP_DELAY = 1.0f; // Stop listening after 1 second of no packets
|
||||
|
||||
|
||||
// Packet structure (matching ConvaiSimpleUDPAudioSender)
|
||||
// SIMPLIFIED Packet structure (matching ConvaiSimpleUDPAudioSender)
|
||||
private struct AudioPacketData
|
||||
{
|
||||
public uint magicNumber;
|
||||
public int sequence;
|
||||
public int sampleCount;
|
||||
public int microphonePosition;
|
||||
public bool isEndSignal;
|
||||
public bool isStartSignal;
|
||||
public short[] audioSamples;
|
||||
public long timestamp;
|
||||
}
|
||||
|
||||
[Header("Recording Storage")]
|
||||
@ -106,7 +107,18 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
StartListening();
|
||||
StartCoroutine(WaitAndSubscribe());
|
||||
|
||||
// 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 +147,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 +204,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 +216,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,20 +227,34 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
try
|
||||
{
|
||||
_udpListener = new UdpClient(listenPort);
|
||||
// Subscribe to shared listener
|
||||
SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
|
||||
_isListening = true;
|
||||
|
||||
ConvaiLogger.Info($"Simple UDP Audio Receiver V2 listening on port {listenPort}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Start listening for incoming packets
|
||||
_ = ListenForAudioPackets(_cancellationTokenSource.Token);
|
||||
ConvaiLogger.Info($"✅ Audio Receiver subscribed to shared listener, listening for magic 0x{MAGIC_NUMBER:X}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Failed to start UDP listener: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Error($"❌ FAILED to subscribe Audio Receiver: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Error($"Stack trace: {ex.StackTrace}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator WaitAndSubscribe()
|
||||
{
|
||||
float timeout = 3f;
|
||||
while (SharedUDPListener.Instance == null && timeout > 0f)
|
||||
{
|
||||
timeout -= Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
if (SharedUDPListener.Instance == null)
|
||||
{
|
||||
ConvaiLogger.Error("SharedUDPListener not ready after wait.", ConvaiLogger.LogCategory.Character);
|
||||
yield break;
|
||||
}
|
||||
StartListening();
|
||||
}
|
||||
|
||||
public void StopListening()
|
||||
{
|
||||
@ -195,9 +262,12 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
return;
|
||||
|
||||
_isListening = false;
|
||||
_udpListener?.Close();
|
||||
_udpListener?.Dispose();
|
||||
_udpListener = null;
|
||||
|
||||
// Unsubscribe from shared listener
|
||||
if (SharedUDPListener.Instance != null)
|
||||
{
|
||||
SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
|
||||
}
|
||||
|
||||
// Stop any ongoing simulation
|
||||
StopTalkingSimulation();
|
||||
@ -205,29 +275,22 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
ConvaiLogger.Info("Stopped UDP Audio Receiver V2", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private async Task ListenForAudioPackets(CancellationToken cancellationToken)
|
||||
private void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_isListening && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = await _udpListener.ReceiveAsync();
|
||||
_remoteEndPoint = result.RemoteEndPoint;
|
||||
|
||||
await ProcessReceivedPacket(result.Buffer, result.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Normal when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Error in UDP listener: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
// Check if this is an audio packet (by magic number)
|
||||
if (data.Length < 12) return;
|
||||
|
||||
uint magic = BitConverter.ToUInt32(data, 0);
|
||||
if (magic != MAGIC_NUMBER) return;
|
||||
|
||||
// Update remote endpoint
|
||||
_remoteEndPoint = senderEndPoint;
|
||||
|
||||
// Process audio packet
|
||||
_ = ProcessReceivedPacket(data, senderEndPoint);
|
||||
}
|
||||
|
||||
private async Task ProcessReceivedPacket(byte[] data, IPEndPoint sender)
|
||||
private Task ProcessReceivedPacket(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -238,28 +301,21 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
var packet = packetData.Value;
|
||||
_lastPacketTime = Time.time;
|
||||
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
if (packet.isEndSignal)
|
||||
ConvaiLogger.DebugLog($"Received end signal from {sender}", ConvaiLogger.LogCategory.Character);
|
||||
else
|
||||
ConvaiLogger.DebugLog($"Received audio packet {packet.sequence} with {packet.sampleCount} samples", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
// Update metrics
|
||||
_totalPacketsReceived++;
|
||||
_lastPacketReceivedTime = DateTime.UtcNow;
|
||||
|
||||
// Handle START control: acknowledge and begin simulation
|
||||
if (packet.isStartSignal)
|
||||
// SIMPLIFIED: Check for end signal (sampleCount == -1)
|
||||
bool isEndSignal = (packet.sampleCount == -1);
|
||||
|
||||
if (enableDebugLogging && packet.sequence % 50 == 0)
|
||||
{
|
||||
SendStartAck(sender);
|
||||
if (!_isReceivingAudio)
|
||||
{
|
||||
StartTalkingSimulation();
|
||||
}
|
||||
OnAudioReceiving?.Invoke(true);
|
||||
return;
|
||||
ConvaiLogger.DebugLog($"📥 Received audio packet #{packet.sequence} (magic: 0x{packet.magicNumber:X}) from {sender}, {packet.sampleCount} samples", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
if (packet.isEndSignal)
|
||||
if (isEndSignal)
|
||||
{
|
||||
ConvaiLogger.Info($"📥 Received END signal from {sender}", ConvaiLogger.LogCategory.Character);
|
||||
StopTalkingSimulation();
|
||||
OnAudioReceiving?.Invoke(false);
|
||||
}
|
||||
@ -268,6 +324,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
// If this is the first packet, start the talking simulation
|
||||
if (packet.sequence == 0 && !_isReceivingAudio)
|
||||
{
|
||||
ConvaiLogger.Info($"📥 Received FIRST audio packet from {sender}, starting simulation", ConvaiLogger.LogCategory.Character);
|
||||
StartTalkingSimulation();
|
||||
}
|
||||
|
||||
@ -292,6 +349,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
ConvaiLogger.Error($"Error processing received packet: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void StartTalkingSimulation()
|
||||
@ -299,10 +358,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 +383,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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -349,7 +408,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private AudioPacketData? ParseSimpleAudioPacket(byte[] data)
|
||||
{
|
||||
if (data.Length < 17) // Minimum header size to match sender
|
||||
if (data.Length < 12) // SIMPLIFIED: Minimum header size (4+4+4)
|
||||
return null;
|
||||
|
||||
try
|
||||
@ -361,27 +420,22 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
offset += 4;
|
||||
|
||||
if (magic != MAGIC_NUMBER)
|
||||
{
|
||||
if (enableDebugLogging)
|
||||
ConvaiLogger.Warn($"❌ Invalid magic number: expected 0x{MAGIC_NUMBER:X}, got 0x{magic:X}", ConvaiLogger.LogCategory.Character);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read header (matching sender's 17-byte format)
|
||||
// SIMPLIFIED header (12 bytes total)
|
||||
int sequence = BitConverter.ToInt32(data, offset);
|
||||
offset += 4;
|
||||
|
||||
int sampleCount = BitConverter.ToInt32(data, offset);
|
||||
offset += 4;
|
||||
|
||||
int microphonePosition = BitConverter.ToInt32(data, offset);
|
||||
offset += 4;
|
||||
|
||||
byte flags = data[offset];
|
||||
offset += 1;
|
||||
|
||||
bool isEndSignal = (flags == 1);
|
||||
bool isStartSignal = (flags == 2);
|
||||
|
||||
// Read audio data
|
||||
// Read audio data (if not end signal)
|
||||
short[] audioSamples = null;
|
||||
if (!isEndSignal && !isStartSignal && sampleCount > 0)
|
||||
if (sampleCount > 0)
|
||||
{
|
||||
int audioDataSize = sampleCount * sizeof(short);
|
||||
if (data.Length >= offset + audioDataSize)
|
||||
@ -389,6 +443,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
audioSamples = new short[sampleCount];
|
||||
Buffer.BlockCopy(data, offset, audioSamples, 0, audioDataSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvaiLogger.Warn($"⚠️ Packet too small: expected {offset + audioDataSize} bytes, got {data.Length}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
return new AudioPacketData
|
||||
@ -396,11 +454,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
magicNumber = magic,
|
||||
sequence = sequence,
|
||||
sampleCount = sampleCount,
|
||||
microphonePosition = microphonePosition,
|
||||
isEndSignal = isEndSignal,
|
||||
isStartSignal = isStartSignal,
|
||||
audioSamples = audioSamples,
|
||||
timestamp = 0 // Not provided in sender format
|
||||
audioSamples = audioSamples
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -410,26 +464,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
private void SendStartAck(IPEndPoint sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_udpListener == null || sender == null)
|
||||
return;
|
||||
|
||||
byte[] ack = new byte[8];
|
||||
BitConverter.GetBytes(ACK_MAGIC).CopyTo(ack, 0);
|
||||
BitConverter.GetBytes(-1).CopyTo(ack, 4);
|
||||
_udpListener.SendAsync(ack, ack.Length, sender);
|
||||
|
||||
if (enableDebugLogging)
|
||||
ConvaiLogger.DebugLog($"Sent START ACK to {sender}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Warn($"Failed to send START ACK: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private void BufferAudioPacket(int sequence, short[] samples)
|
||||
{
|
||||
|
||||
@ -48,28 +48,34 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
private AudioClip _audioClip;
|
||||
private bool _isRecording = false;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private CancellationTokenSource _ackCancellationTokenSource;
|
||||
|
||||
private int _lastMicrophonePosition = 0;
|
||||
private float[] _audioBuffer;
|
||||
private string _selectedMicrophone;
|
||||
private int _packetSequence = 0;
|
||||
private volatile bool _startAckReceived = false;
|
||||
private bool _xrAButtonPrevPressed = false;
|
||||
private InputAction _xrTalkAction;
|
||||
private InputAction _xrTestAction;
|
||||
private bool _usingExternalTalkAction = false;
|
||||
private InputAction _externalTalkAction;
|
||||
|
||||
// Protocol constants
|
||||
// Protocol constants - SIMPLIFIED
|
||||
private const uint AUDIO_MAGIC = 0xC0A1;
|
||||
private const uint ACK_MAGIC = 0xC0A2;
|
||||
private const byte FLAG_AUDIO = 0;
|
||||
private const byte FLAG_END = 1;
|
||||
private const byte FLAG_START = 2;
|
||||
|
||||
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;
|
||||
@ -108,10 +114,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
InitializeAudio();
|
||||
_persistentDataPath = Application.persistentDataPath;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_ackCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Start ACK listener
|
||||
_ = ListenForAcks(_ackCancellationTokenSource.Token);
|
||||
|
||||
// Setup Input System action for XR A/primary button
|
||||
if (useInputSystemXR)
|
||||
@ -148,8 +150,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
StopRecording();
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_ackCancellationTokenSource?.Cancel();
|
||||
_ackCancellationTokenSource?.Dispose();
|
||||
if (_usingExternalTalkAction)
|
||||
TeardownExternalTalkInputAction();
|
||||
else
|
||||
@ -167,13 +167,9 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private void HandlePeerLost()
|
||||
{
|
||||
var cfg = NetworkConfig.Instance;
|
||||
if (cfg != null)
|
||||
{
|
||||
targetIP = cfg.fallbackBroadcastIP;
|
||||
_targetEndPoint = new IPEndPoint(IPAddress.Parse(targetIP), targetPort);
|
||||
ConvaiLogger.Warn($"🎤 Audio sender falling back to broadcast: {targetIP}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
// Don't change targetIP - keep sending to the last known peer IP
|
||||
// The peer might come back online and we'll automatically reconnect
|
||||
ConvaiLogger.Warn($"🎤 Audio sender: Peer connection lost, but continuing to send to {targetIP}:{targetPort}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private void InitializeNetwork()
|
||||
@ -470,20 +466,16 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
_isRecording = true;
|
||||
_lastMicrophonePosition = 0;
|
||||
_packetSequence = 0;
|
||||
_startAckReceived = false;
|
||||
_localSessionStart = DateTime.UtcNow;
|
||||
lock (_localAudioLock)
|
||||
{
|
||||
_localSamples.Clear();
|
||||
}
|
||||
|
||||
ConvaiLogger.Info("Started recording for UDP transmission (Simple)", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($"🎤 Started recording for UDP transmission to {targetIP}:{targetPort}", ConvaiLogger.LogCategory.Character);
|
||||
OnRecordingStateChanged?.Invoke(true);
|
||||
|
||||
// Send START control and wait briefly for ACK to ensure receiver is ready
|
||||
_ = SendStartOfRecordingSignalAndAwaitAck();
|
||||
|
||||
// Start continuous audio processing
|
||||
// Start continuous audio processing immediately (SIMPLIFIED - no ACK waiting)
|
||||
_ = ProcessAudioContinuously(_cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -609,6 +601,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);
|
||||
@ -630,15 +626,13 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private byte[] CreateSimpleAudioPacket(float[] audioData, int startIndex, int sampleCount)
|
||||
{
|
||||
// Simple packet structure:
|
||||
// SIMPLIFIED packet structure:
|
||||
// 4 bytes: Magic number (0xC0A1)
|
||||
// 4 bytes: Packet sequence number
|
||||
// 4 bytes: Sample count in this packet
|
||||
// 4 bytes: Start position in stream
|
||||
// 1 byte: Flags (0 = normal audio, 1 = end of recording)
|
||||
// N bytes: Audio data (converted to shorts)
|
||||
|
||||
int headerSize = 17; // 4 + 4 + 4 + 4 + 1
|
||||
int headerSize = 12; // 4 + 4 + 4 (SIMPLIFIED from 17)
|
||||
int audioDataSize = sampleCount * sizeof(short);
|
||||
byte[] packet = new byte[headerSize + audioDataSize];
|
||||
|
||||
@ -656,15 +650,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
BitConverter.GetBytes(sampleCount).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
|
||||
// Start position
|
||||
BitConverter.GetBytes(_lastMicrophonePosition + startIndex).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
|
||||
// Flags (0 for normal audio)
|
||||
packet[offset] = FLAG_AUDIO;
|
||||
offset += 1;
|
||||
|
||||
// Convert audio samples to bytes (same as Convai approach)
|
||||
// Convert audio samples to bytes
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
float sample = audioData[startIndex + i];
|
||||
@ -675,6 +661,11 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
if (enableDebugLogging && _packetSequence % 50 == 0)
|
||||
{
|
||||
ConvaiLogger.DebugLog($"📤 Sent audio packet #{_packetSequence} (magic: 0x{AUDIO_MAGIC:X}) to {targetIP}:{targetPort}, {sampleCount} samples", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
@ -682,8 +673,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create end packet
|
||||
byte[] packet = new byte[17]; // Header only, no audio data
|
||||
// SIMPLIFIED end packet
|
||||
byte[] packet = new byte[12]; // Header only, no audio data
|
||||
int offset = 0;
|
||||
|
||||
// Magic number
|
||||
@ -694,18 +685,12 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
BitConverter.GetBytes(_packetSequence).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
|
||||
// Sample count (0 for end signal)
|
||||
BitConverter.GetBytes(0).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
|
||||
// Start position
|
||||
BitConverter.GetBytes(_lastMicrophonePosition).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
|
||||
// Flags (1 for end of recording)
|
||||
packet[offset] = FLAG_END;
|
||||
// Sample count (-1 for end signal)
|
||||
BitConverter.GetBytes(-1).CopyTo(packet, offset);
|
||||
|
||||
_udpClient.SendAsync(packet, packet.Length, _targetEndPoint);
|
||||
|
||||
ConvaiLogger.Info($"📤 Sent END signal (magic: 0x{AUDIO_MAGIC:X}) to {targetIP}:{targetPort}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -807,90 +792,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendStartOfRecordingSignalAndAwaitAck()
|
||||
{
|
||||
try
|
||||
{
|
||||
const int maxAttempts = 3;
|
||||
const int ackTimeoutMs = 250;
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts && !_startAckReceived; attempt++)
|
||||
{
|
||||
// Build START control packet (no audio, special flag)
|
||||
byte[] packet = new byte[17];
|
||||
int offset = 0;
|
||||
BitConverter.GetBytes(AUDIO_MAGIC).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
// Use -1 as the special sequence for START control
|
||||
BitConverter.GetBytes(-1).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
BitConverter.GetBytes(0).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
BitConverter.GetBytes(_lastMicrophonePosition).CopyTo(packet, offset);
|
||||
offset += 4;
|
||||
packet[offset] = FLAG_START;
|
||||
|
||||
await _udpClient.SendAsync(packet, packet.Length, _targetEndPoint);
|
||||
|
||||
// Wait for ACK
|
||||
int waited = 0;
|
||||
while (!_startAckReceived && waited < ackTimeoutMs)
|
||||
{
|
||||
await Task.Delay(10);
|
||||
waited += 10;
|
||||
}
|
||||
|
||||
if (_startAckReceived)
|
||||
{
|
||||
if (enableDebugLogging)
|
||||
ConvaiLogger.DebugLog("Received START ACK from receiver", ConvaiLogger.LogCategory.Character);
|
||||
break;
|
||||
}
|
||||
else if (enableDebugLogging)
|
||||
{
|
||||
ConvaiLogger.Warn($"No START ACK (attempt {attempt}/{maxAttempts}), retrying...", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Warn($"Error during START ACK process: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ListenForAcks(CancellationToken token)
|
||||
{
|
||||
// Use the same UDP client; acks will be sent back to our ephemeral local port
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _udpClient.ReceiveAsync();
|
||||
var data = result.Buffer;
|
||||
if (data == null || data.Length < 8)
|
||||
continue;
|
||||
|
||||
uint magic = BitConverter.ToUInt32(data, 0);
|
||||
if (magic != ACK_MAGIC)
|
||||
continue;
|
||||
|
||||
int seq = BitConverter.ToInt32(data, 4);
|
||||
if (seq == -1)
|
||||
{
|
||||
_startAckReceived = true;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore and keep listening
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods for external control
|
||||
public void SetTargetEndpoint(string ip, int port)
|
||||
{
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Runtime.LoggerSystem;
|
||||
@ -11,8 +10,18 @@ using UnityEngine;
|
||||
namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// UDP Speech Receiver - Receives high-quality Convai speech with proper buffering
|
||||
/// This version reconstructs the original AudioClip objects for seamless playback
|
||||
/// UDP Speech Receiver - Receives and plays NPC speech audio from remote player
|
||||
///
|
||||
/// FLOW (Player 1 → Player 2):
|
||||
/// 1. Player 2 speaks to Player 1's NPC
|
||||
/// 2. Player 1's NPC responds with speech
|
||||
/// 3. Player 1's ConvaiUDPSpeechSender transmits the audio
|
||||
/// 4. THIS COMPONENT receives the audio packets
|
||||
/// 5. Reconstructs AudioClips from the packets
|
||||
/// 6. Plays them back on local AudioSource
|
||||
///
|
||||
/// This component should be on a NetworkManager or similar persistent object.
|
||||
/// It receives speech from the remote player's NPC.
|
||||
/// </summary>
|
||||
public class ConvaiUDPSpeechReceiver : MonoBehaviour
|
||||
{
|
||||
@ -29,7 +38,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
[SerializeField] private bool showTranscripts = true;
|
||||
|
||||
// Network components
|
||||
private UdpClient _udpListener;
|
||||
private IPEndPoint _remoteEndPoint;
|
||||
private bool _isListening = false;
|
||||
private int listenPort;
|
||||
@ -54,6 +62,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
|
||||
{
|
||||
@ -128,7 +144,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
StartListening();
|
||||
StartCoroutine(WaitAndSubscribe());
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@ -193,19 +209,34 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
try
|
||||
{
|
||||
_udpListener = new UdpClient(listenPort);
|
||||
|
||||
// Subscribe to shared listener
|
||||
SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
|
||||
_isListening = true;
|
||||
|
||||
ConvaiLogger.Info($"UDP Speech Receiver listening on port {listenPort}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Start listening for incoming packets
|
||||
_ = ListenForSpeechPackets(_cancellationTokenSource.Token);
|
||||
ConvaiLogger.Info($"✅ Speech Receiver subscribed to shared listener, listening for magic 0x{MAGIC_NUMBER:X}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Failed to start UDP speech receiver: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Error($"❌ FAILED to subscribe Speech Receiver: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator WaitAndSubscribe()
|
||||
{
|
||||
float timeout = 3f;
|
||||
while (SharedUDPListener.Instance == null && timeout > 0f)
|
||||
{
|
||||
timeout -= Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
if (SharedUDPListener.Instance == null)
|
||||
{
|
||||
ConvaiLogger.Error("SharedUDPListener not ready after wait.", ConvaiLogger.LogCategory.Character);
|
||||
yield break;
|
||||
}
|
||||
StartListening();
|
||||
}
|
||||
|
||||
public void StopListening()
|
||||
{
|
||||
@ -213,9 +244,12 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
return;
|
||||
|
||||
_isListening = false;
|
||||
_udpListener?.Close();
|
||||
_udpListener?.Dispose();
|
||||
_udpListener = null;
|
||||
|
||||
// Unsubscribe from shared listener
|
||||
if (SharedUDPListener.Instance != null)
|
||||
{
|
||||
SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
|
||||
}
|
||||
|
||||
// Stop any ongoing playback
|
||||
StopSpeechPlayback();
|
||||
@ -223,29 +257,22 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
ConvaiLogger.Info("Stopped UDP Speech Receiver", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private async Task ListenForSpeechPackets(CancellationToken cancellationToken)
|
||||
private void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_isListening && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = await _udpListener.ReceiveAsync();
|
||||
_remoteEndPoint = result.RemoteEndPoint;
|
||||
|
||||
await ProcessReceivedPacket(result.Buffer, result.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Normal when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Error in UDP speech listener: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
// Check if this is a speech packet (by magic number)
|
||||
if (data.Length < 4) return;
|
||||
|
||||
uint magic = BitConverter.ToUInt32(data, 0);
|
||||
if (magic != MAGIC_NUMBER) return;
|
||||
|
||||
// Update remote endpoint
|
||||
_remoteEndPoint = senderEndPoint;
|
||||
|
||||
// Process speech packet
|
||||
_ = ProcessReceivedPacket(data, senderEndPoint);
|
||||
}
|
||||
|
||||
private async Task ProcessReceivedPacket(byte[] data, IPEndPoint sender)
|
||||
private Task ProcessReceivedPacket(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -313,6 +340,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
ConvaiLogger.Error($"Error processing speech packet: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void HandleAudioStartPacket(SpeechPacket packet)
|
||||
@ -446,6 +475,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);
|
||||
}
|
||||
|
||||
@ -12,8 +12,18 @@ using System.Collections;
|
||||
namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// UDP Speech Sender - Simple and reliable approach using events
|
||||
/// Hooks into AudioManager events to capture when clips are about to be played
|
||||
/// UDP Speech Sender - Captures and transmits NPC speech audio to remote player
|
||||
///
|
||||
/// FLOW (Player 1 → Player 2):
|
||||
/// 1. Player 2 speaks (via ConvaiSimpleUDPAudioSender on their device)
|
||||
/// 2. Player 1 receives voice input (via ConvaiSimpleUDPAudioReceiver)
|
||||
/// 3. Player 1's NPC generates response speech (Convai API)
|
||||
/// 4. THIS COMPONENT monitors Player 1's NPC AudioSource
|
||||
/// 5. When new AudioClips appear, transmit them to Player 2
|
||||
/// 6. Player 2's ConvaiUDPSpeechReceiver plays the audio
|
||||
///
|
||||
/// This component should be on a NetworkManager or similar persistent object.
|
||||
/// It will find and monitor ConvaiNPC components on Avatar objects in the scene.
|
||||
/// </summary>
|
||||
public class ConvaiUDPSpeechSender : MonoBehaviour
|
||||
{
|
||||
@ -26,7 +36,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
[Header("Audio Settings")]
|
||||
[SerializeField] private int maxSamplesPerPacket = 8192;
|
||||
[SerializeField] private bool sendTranscripts = true;
|
||||
|
||||
// Network components
|
||||
private UdpClient _udpClient;
|
||||
@ -51,6 +60,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 +111,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;
|
||||
@ -101,13 +167,9 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private void HandlePeerLost()
|
||||
{
|
||||
var cfg = NetworkConfig.Instance;
|
||||
if (cfg != null)
|
||||
{
|
||||
targetIP = cfg.fallbackBroadcastIP;
|
||||
_targetEndPoint = new IPEndPoint(IPAddress.Parse(targetIP), targetPort);
|
||||
ConvaiLogger.Warn($"🔊 Speech sender falling back to broadcast: {targetIP}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
// Don't change targetIP - keep sending to the last known peer IP
|
||||
// The peer might come back online and we'll automatically reconnect
|
||||
ConvaiLogger.Warn($"🔊 Speech sender: Peer connection lost, but continuing to send to {targetIP}:{targetPort}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private void InitializeNetwork()
|
||||
@ -128,15 +190,24 @@ 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)
|
||||
{
|
||||
sourceNPC = localNPC;
|
||||
ConvaiLogger.Info($"Speech Sender: Using local NPC {sourceNPC.characterName}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
else if (useActiveNPC)
|
||||
{
|
||||
sourceNPC = ConvaiNPCManager.Instance?.GetActiveConvaiNPC();
|
||||
sourceNPC = FindEnabledConvaiNPC();
|
||||
if (sourceNPC != null)
|
||||
{
|
||||
ConvaiLogger.Info($"Speech Sender: Found NPC {sourceNPC.characterName} on {sourceNPC.gameObject.name}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvaiLogger.Warn("Speech Sender: No ConvaiNPC found in scene yet", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
SubscribeToNPCEvents();
|
||||
@ -150,18 +221,26 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private void SubscribeToNPCEvents()
|
||||
{
|
||||
if (sourceNPC?.AudioManager != null)
|
||||
if (sourceNPC == null)
|
||||
{
|
||||
// Hook into the character talking events
|
||||
sourceNPC.AudioManager.OnCharacterTalkingChanged += HandleCharacterTalkingChanged;
|
||||
sourceNPC.AudioManager.OnAudioTranscriptAvailable += HandleTranscriptAvailable;
|
||||
|
||||
ConvaiLogger.Info($"UDP Speech Sender subscribed to NPC: {sourceNPC.characterName}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Warn("SubscribeToNPCEvents: sourceNPC is null", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
if (sourceNPC.AudioManager == null)
|
||||
{
|
||||
ConvaiLogger.Warn("No source NPC available for speech transmission", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Warn($"SubscribeToNPCEvents: AudioManager is null for {sourceNPC.characterName}", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook into the character talking events
|
||||
sourceNPC.AudioManager.OnCharacterTalkingChanged += HandleCharacterTalkingChanged;
|
||||
sourceNPC.AudioManager.OnAudioTranscriptAvailable += HandleTranscriptAvailable;
|
||||
|
||||
ConvaiLogger.Info($"✅ UDP Speech Sender subscribed to NPC: {sourceNPC.characterName} (on {sourceNPC.gameObject.name}), AudioManager: {sourceNPC.AudioManager.name}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Also start continuous monitoring as a fallback in case events don't fire
|
||||
StartCoroutine(ContinuousAudioMonitoring());
|
||||
}
|
||||
|
||||
private void HandleCharacterTalkingChanged(bool isTalking)
|
||||
@ -170,11 +249,13 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
if (isTalking)
|
||||
{
|
||||
ConvaiLogger.Info($"🔊 NPC {sourceNPC.characterName} started talking, monitoring audio clips...", ConvaiLogger.LogCategory.Character);
|
||||
// Start monitoring for audio clips
|
||||
StartCoroutine(MonitorAudioClips());
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvaiLogger.Info($"🔊 NPC {sourceNPC.characterName} stopped talking", ConvaiLogger.LogCategory.Character);
|
||||
// End speech transmission
|
||||
_ = SendFinalPacket();
|
||||
}
|
||||
@ -190,17 +271,33 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private IEnumerator MonitorAudioClips()
|
||||
{
|
||||
if (sourceNPC?.AudioManager == null) yield break;
|
||||
if (sourceNPC?.AudioManager == null)
|
||||
{
|
||||
ConvaiLogger.Error("MonitorAudioClips: AudioManager is null on sourceNPC", ConvaiLogger.LogCategory.Character);
|
||||
yield break;
|
||||
}
|
||||
|
||||
AudioSource audioSource = sourceNPC.AudioManager.GetComponent<AudioSource>();
|
||||
AudioClip lastClip = null;
|
||||
|
||||
while (sourceNPC.IsCharacterTalking)
|
||||
if (audioSource == null)
|
||||
{
|
||||
ConvaiLogger.Error($"MonitorAudioClips: No AudioSource found on AudioManager ({sourceNPC.AudioManager.name})", ConvaiLogger.LogCategory.Character);
|
||||
yield break;
|
||||
}
|
||||
|
||||
ConvaiLogger.Info($"🔊 Started monitoring audio clips on {audioSource.name} for NPC {sourceNPC.characterName}", ConvaiLogger.LogCategory.Character);
|
||||
AudioClip lastClip = null;
|
||||
int checkCount = 0;
|
||||
|
||||
while (sourceNPC != null && sourceNPC.IsCharacterTalking)
|
||||
{
|
||||
checkCount++;
|
||||
|
||||
// SIMPLIFIED: Check EVERY frame if there's a new clip, don't wait for events
|
||||
if (audioSource?.clip != null && audioSource.clip != lastClip)
|
||||
{
|
||||
// New clip detected!
|
||||
lastClip = audioSource.clip;
|
||||
ConvaiLogger.Info($"🔊 NEW CLIP DETECTED: {lastClip.name}, length: {lastClip.length:F2}s, samples: {lastClip.samples}, freq: {lastClip.frequency}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Only send if we haven't sent this clip before
|
||||
if (!_sentClips.Contains(lastClip))
|
||||
@ -210,18 +307,135 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
// Get the transcript from the most recent available transcript
|
||||
string transcript = GetRecentTranscript();
|
||||
|
||||
// Start transmission if not already started
|
||||
if (!_isSendingSpeech)
|
||||
{
|
||||
_isSendingSpeech = true;
|
||||
OnSpeechTransmission?.Invoke(true);
|
||||
ConvaiLogger.Info($"🔊 Starting speech transmission", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
// Send this clip
|
||||
ConvaiLogger.Info($"🔊 TRANSMITTING CLIP to {targetIP}:{targetPort}", ConvaiLogger.LogCategory.Character);
|
||||
_ = TransmitAudioClip(lastClip, transcript);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvaiLogger.Warn($"🔊 Clip already sent, skipping: {lastClip.name}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.1f); // Check every 100ms
|
||||
// Log periodically to show we're still monitoring
|
||||
if (enableDebugLogging && checkCount % 100 == 0)
|
||||
{
|
||||
ConvaiLogger.DebugLog($"🔊 Monitoring... check #{checkCount}, current clip: {(audioSource?.clip != null ? audioSource.clip.name : "null")}, isTalking: {sourceNPC.IsCharacterTalking}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(0.05f); // Check every 50ms for faster detection
|
||||
}
|
||||
|
||||
ConvaiLogger.Info($"🔊 Stopped monitoring audio clips (NPC stopped talking or was destroyed). Checks performed: {checkCount}", ConvaiLogger.LogCategory.Character);
|
||||
// Clear sent clips when done
|
||||
_sentClips.Clear();
|
||||
}
|
||||
|
||||
private IEnumerator ContinuousAudioMonitoring()
|
||||
{
|
||||
ConvaiLogger.Info("🔊 Starting continuous audio monitoring as fallback", ConvaiLogger.LogCategory.Character);
|
||||
AudioClip lastMonitoredClip = null;
|
||||
float lastNPCCheckLog = 0f;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Wait a bit between checks
|
||||
yield return new WaitForSeconds(0.2f);
|
||||
|
||||
// Check if we still have a valid source NPC
|
||||
if (sourceNPC == null || sourceNPC.AudioManager == null)
|
||||
{
|
||||
// Log every 2 seconds if no NPC found (AGGRESSIVE LOGGING)
|
||||
if (Time.time - lastNPCCheckLog > 2f)
|
||||
{
|
||||
if (sourceNPC == null)
|
||||
ConvaiLogger.Warn("⚠️ [Speech Sender] No source NPC found! Looking for ConvaiNPC in scene...", ConvaiLogger.LogCategory.Character);
|
||||
else
|
||||
ConvaiLogger.Warn($"⚠️ [Speech Sender] NPC '{sourceNPC.characterName}' has no AudioManager!", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Try to find NPC again
|
||||
if (useActiveNPC)
|
||||
{
|
||||
var foundNPC = FindEnabledConvaiNPC();
|
||||
if (foundNPC != null && foundNPC != sourceNPC)
|
||||
{
|
||||
ConvaiLogger.Info($"✅ [Speech Sender] Found NPC: {foundNPC.characterName} on GameObject '{foundNPC.gameObject.name}'", ConvaiLogger.LogCategory.Character);
|
||||
sourceNPC = foundNPC;
|
||||
SubscribeToNPCEvents();
|
||||
}
|
||||
}
|
||||
|
||||
lastNPCCheckLog = Time.time;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(1f); // Wait longer if no NPC
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the audio source
|
||||
AudioSource audioSource = sourceNPC.AudioManager.GetComponent<AudioSource>();
|
||||
if (audioSource == null)
|
||||
{
|
||||
if (Time.time - lastNPCCheckLog > 2f)
|
||||
{
|
||||
ConvaiLogger.Warn($"⚠️ [Speech Sender] NPC '{sourceNPC.characterName}' AudioManager has no AudioSource component!", ConvaiLogger.LogCategory.Character);
|
||||
lastNPCCheckLog = Time.time;
|
||||
}
|
||||
yield return new WaitForSeconds(1f);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if there's a new audio clip playing
|
||||
if (audioSource.clip != null &&
|
||||
audioSource.clip != lastMonitoredClip &&
|
||||
audioSource.isPlaying)
|
||||
{
|
||||
lastMonitoredClip = audioSource.clip;
|
||||
|
||||
// Only send if we haven't sent this clip before
|
||||
if (!_sentClips.Contains(lastMonitoredClip))
|
||||
{
|
||||
_sentClips.Add(lastMonitoredClip);
|
||||
|
||||
ConvaiLogger.Info($"🔊 [Continuous Monitor] NEW CLIP DETECTED: {lastMonitoredClip.name}, length: {lastMonitoredClip.length:F2}s", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Start transmission if not already started
|
||||
if (!_isSendingSpeech)
|
||||
{
|
||||
_isSendingSpeech = true;
|
||||
OnSpeechTransmission?.Invoke(true);
|
||||
}
|
||||
|
||||
string transcript = "";
|
||||
_ = TransmitAudioClip(lastMonitoredClip, transcript);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old clips from the sent list if NPC is not talking
|
||||
if (!sourceNPC.IsCharacterTalking && _sentClips.Count > 0)
|
||||
{
|
||||
if (enableDebugLogging)
|
||||
ConvaiLogger.DebugLog($"🔊 [Continuous Monitor] NPC stopped talking, clearing sent clips list ({_sentClips.Count} clips)", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
_sentClips.Clear();
|
||||
|
||||
// Send final packet
|
||||
if (_isSendingSpeech)
|
||||
{
|
||||
_ = SendFinalPacket();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRecentTranscript()
|
||||
{
|
||||
// Try to get transcript from the NPC's recent activity
|
||||
@ -235,15 +449,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
try
|
||||
{
|
||||
// Start transmission if not already started
|
||||
if (!_isSendingSpeech)
|
||||
{
|
||||
_isSendingSpeech = true;
|
||||
OnSpeechTransmission?.Invoke(true);
|
||||
|
||||
ConvaiLogger.Info($"🔊 Starting speech transmission", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
// Use the current speech sequence for this entire clip
|
||||
int clipSequence = _speechSequence;
|
||||
|
||||
@ -259,6 +464,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)
|
||||
@ -275,8 +484,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
byte[] packet = CreateAudioStartPacket(audioClip, transcript, sequence);
|
||||
await _udpClient.SendAsync(packet, packet.Length, _targetEndPoint);
|
||||
|
||||
if (enableDebugLogging)
|
||||
ConvaiLogger.DebugLog($"📤 Sent start packet {sequence}: {audioClip.samples} samples", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($"📤 Sent speech START packet #{sequence} (magic: 0x{MAGIC_NUMBER:X}) to {targetIP}:{targetPort}, {audioClip.samples} samples", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private async Task SendAudioClipInChunks(AudioClip audioClip, int sequence)
|
||||
@ -483,6 +691,9 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged -= HandleActiveNPCChanged;
|
||||
}
|
||||
|
||||
// Stop all coroutines when cleaning up (will restart with new NPC)
|
||||
StopAllCoroutines();
|
||||
}
|
||||
|
||||
private void CleanupNetwork()
|
||||
|
||||
714
Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs
Normal file
714
Unity-Master/Assets/Scripts/Multiplayer/NetworkDebugUI.cs
Normal file
@ -0,0 +1,714 @@
|
||||
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;
|
||||
|
||||
// Packet tracking for enhanced debugging
|
||||
private System.Collections.Generic.Queue<string> _packetLog = new System.Collections.Generic.Queue<string>();
|
||||
private const int MAX_PACKET_LOG = 15;
|
||||
private int _lastAudioSentCount = 0;
|
||||
private int _lastAudioReceivedCount = 0;
|
||||
private int _lastSpeechSentCount = 0;
|
||||
private int _lastSpeechReceivedCount = 0;
|
||||
private float _lastRateCheckTime = 0f;
|
||||
|
||||
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(900, 1200); // Increased size for more info
|
||||
|
||||
// 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(900, 1200);
|
||||
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();
|
||||
|
||||
// Port Binding Status
|
||||
AppendPortBindingStatus(sb);
|
||||
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();
|
||||
|
||||
// Packet Rates
|
||||
AppendPacketRates(sb);
|
||||
sb.AppendLine();
|
||||
|
||||
// Event Log
|
||||
AppendEventLog(sb);
|
||||
sb.AppendLine();
|
||||
|
||||
// Packet Log
|
||||
AppendPacketLog(sb);
|
||||
|
||||
_debugText.text = sb.ToString();
|
||||
}
|
||||
|
||||
private void AppendPortBindingStatus(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("🔌 PORT BINDING STATUS");
|
||||
|
||||
// Check if shared listener is active
|
||||
bool sharedListenerActive = SharedUDPListener.Instance != null && SharedUDPListener.Instance.IsListening;
|
||||
|
||||
if (sharedListenerActive)
|
||||
{
|
||||
sb.AppendLine($"✅ Shared UDP Listener ACTIVE");
|
||||
sb.AppendLine($" Port: {SharedUDPListener.Instance.ListenPort}");
|
||||
sb.AppendLine($" Total Packets: {SharedUDPListener.Instance.TotalPacketsReceived}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"❌ Shared UDP Listener NOT FOUND!");
|
||||
sb.AppendLine($" Add SharedUDPListener to scene!");
|
||||
}
|
||||
|
||||
// Check which components are subscribed
|
||||
bool discoveryActive = _peerDiscovery != null;
|
||||
bool audioReceiverActive = _audioReceiver != null;
|
||||
bool speechReceiverActive = _speechReceiver != null;
|
||||
|
||||
sb.AppendLine($"Discovery: {(discoveryActive ? "✅ SUBSCRIBED" : "❌ NOT FOUND")} (0x44495343)");
|
||||
sb.AppendLine($"Audio RX: {(audioReceiverActive ? "✅ SUBSCRIBED" : "❌ NOT FOUND")} (0xC0A1)");
|
||||
sb.AppendLine($"Speech RX: {(speechReceiverActive ? "✅ SUBSCRIBED" : "❌ NOT FOUND")} (0xC0A3)");
|
||||
|
||||
if (!sharedListenerActive)
|
||||
{
|
||||
sb.AppendLine("⚠️ CRITICAL: No shared listener!");
|
||||
}
|
||||
}
|
||||
|
||||
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")}");
|
||||
|
||||
// WARNING if not sending packets while recording
|
||||
if (_audioSender.IsRecording && _audioSender.TotalPacketsSent == 0)
|
||||
{
|
||||
sb.AppendLine("⚠️ RECORDING BUT NO PACKETS SENT!");
|
||||
}
|
||||
}
|
||||
}
|
||||
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} ✅");
|
||||
|
||||
// Show if NPC is currently talking
|
||||
if (sourceNPC.IsCharacterTalking)
|
||||
{
|
||||
sb.AppendLine($"NPC Talking: YES 🗣️");
|
||||
}
|
||||
}
|
||||
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")}");
|
||||
|
||||
// WARNING if transmitting but no clips sent
|
||||
if (_speechSender.IsSendingSpeech && _speechSender.TotalClipsSent == 0)
|
||||
{
|
||||
sb.AppendLine("⚠️ TRANSMITTING BUT NO CLIPS SENT!");
|
||||
}
|
||||
}
|
||||
}
|
||||
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} (shared)");
|
||||
sb.AppendLine($"Listening: {(_audioReceiver.IsListening ? "YES ✅" : "NO ❌")}");
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// WARNING if not listening
|
||||
if (!_audioReceiver.IsListening)
|
||||
{
|
||||
sb.AppendLine("⚠️ NOT LISTENING - PORT BIND FAILED?");
|
||||
}
|
||||
}
|
||||
}
|
||||
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} (shared)");
|
||||
sb.AppendLine($"Listening: {(_speechReceiver.IsListening ? "YES ✅" : "NO ❌")}");
|
||||
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");
|
||||
}
|
||||
|
||||
// WARNING if not listening
|
||||
if (!_speechReceiver.IsListening)
|
||||
{
|
||||
sb.AppendLine("⚠️ NOT LISTENING - PORT BIND FAILED?");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("❌ NOT FOUND IN SCENE");
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendPacketRates(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("📊 PACKET RATES (last second)");
|
||||
|
||||
float currentTime = Time.time;
|
||||
float deltaTime = currentTime - _lastRateCheckTime;
|
||||
|
||||
if (deltaTime >= 1f)
|
||||
{
|
||||
// Calculate rates
|
||||
int audioSentDelta = 0;
|
||||
int audioReceivedDelta = 0;
|
||||
int speechSentDelta = 0;
|
||||
int speechReceivedDelta = 0;
|
||||
|
||||
if (_audioSender != null)
|
||||
{
|
||||
audioSentDelta = _audioSender.TotalPacketsSent - _lastAudioSentCount;
|
||||
_lastAudioSentCount = _audioSender.TotalPacketsSent;
|
||||
}
|
||||
|
||||
if (_audioReceiver != null)
|
||||
{
|
||||
audioReceivedDelta = _audioReceiver.TotalPacketsReceived - _lastAudioReceivedCount;
|
||||
_lastAudioReceivedCount = _audioReceiver.TotalPacketsReceived;
|
||||
}
|
||||
|
||||
if (_speechSender != null)
|
||||
{
|
||||
speechSentDelta = _speechSender.TotalClipsSent - _lastSpeechSentCount;
|
||||
_lastSpeechSentCount = _speechSender.TotalClipsSent;
|
||||
}
|
||||
|
||||
if (_speechReceiver != null)
|
||||
{
|
||||
speechReceivedDelta = _speechReceiver.TotalClipsReceived - _lastSpeechReceivedCount;
|
||||
_lastSpeechReceivedCount = _speechReceiver.TotalClipsReceived;
|
||||
}
|
||||
|
||||
_lastRateCheckTime = currentTime;
|
||||
|
||||
// Log significant activity
|
||||
if (audioSentDelta > 0)
|
||||
LogPacketActivity($"Sent {audioSentDelta} voice packets (0xC0A1)");
|
||||
if (audioReceivedDelta > 0)
|
||||
LogPacketActivity($"Received {audioReceivedDelta} voice packets (0xC0A1)");
|
||||
if (speechSentDelta > 0)
|
||||
LogPacketActivity($"Sent {speechSentDelta} speech clips (0xC0A3)");
|
||||
if (speechReceivedDelta > 0)
|
||||
LogPacketActivity($"Received {speechReceivedDelta} speech clips (0xC0A3)");
|
||||
}
|
||||
|
||||
sb.AppendLine($"Voice Sent: {(_audioSender != null ? _audioSender.TotalPacketsSent : 0)} total");
|
||||
sb.AppendLine($"Voice Received: {(_audioReceiver != null ? _audioReceiver.TotalPacketsReceived : 0)} total");
|
||||
sb.AppendLine($"Speech Sent: {(_speechSender != null ? _speechSender.TotalClipsSent : 0)} clips");
|
||||
sb.AppendLine($"Speech Received: {(_speechReceiver != null ? _speechReceiver.TotalClipsReceived : 0)} clips");
|
||||
}
|
||||
|
||||
private void LogPacketActivity(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
_packetLog.Enqueue($"[{timestamp}] {message}");
|
||||
|
||||
while (_packetLog.Count > MAX_PACKET_LOG)
|
||||
{
|
||||
_packetLog.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendPacketLog(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("📦 PACKET ACTIVITY LOG");
|
||||
|
||||
if (_packetLog.Count > 0)
|
||||
{
|
||||
foreach (var entry in _packetLog)
|
||||
{
|
||||
sb.AppendLine(entry);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("No packet activity yet");
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendEventLog(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("📋 CONNECTION EVENTS");
|
||||
|
||||
if (_peerDiscovery != null && _peerDiscovery.EventLog.Count > 0)
|
||||
{
|
||||
int startIndex = Math.Max(0, _peerDiscovery.EventLog.Count - 6);
|
||||
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:
|
||||
|
||||
195
Unity-Master/Assets/Scripts/Multiplayer/SharedUDPListener.cs
Normal file
195
Unity-Master/Assets/Scripts/Multiplayer/SharedUDPListener.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Runtime.LoggerSystem;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared UDP Listener - Single UDP socket that dispatches packets to appropriate handlers
|
||||
/// Solves the port binding issue where multiple components tried to bind to the same port
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-10000)]
|
||||
public class SharedUDPListener : MonoBehaviour
|
||||
{
|
||||
// Singleton
|
||||
private static SharedUDPListener _instance;
|
||||
public static SharedUDPListener Instance => _instance;
|
||||
|
||||
// Network components
|
||||
private UdpClient _udpClient;
|
||||
private bool _isListening = false;
|
||||
private int _listenPort;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
// Packet handlers - registered by other components
|
||||
public event Action<byte[], IPEndPoint> OnPacketReceived;
|
||||
|
||||
// Metrics
|
||||
private int _totalPacketsReceived = 0;
|
||||
private DateTime _lastPacketTime;
|
||||
public int TotalPacketsReceived => _totalPacketsReceived;
|
||||
public bool IsListening => _isListening;
|
||||
public int ListenPort => _listenPort;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Singleton pattern
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Get network config
|
||||
var cfg = NetworkConfig.Instance;
|
||||
if (cfg != null)
|
||||
{
|
||||
_listenPort = cfg.port;
|
||||
}
|
||||
else
|
||||
{
|
||||
ConvaiLogger.Error("NetworkConfig not found! Using default port 1221", ConvaiLogger.LogCategory.Character);
|
||||
_listenPort = 1221;
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
StartListening();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopListening();
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
|
||||
if (_instance == this)
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
private void StartListening()
|
||||
{
|
||||
if (_isListening) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Create single UDP client for ALL network traffic
|
||||
_udpClient = new UdpClient();
|
||||
_udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, _listenPort));
|
||||
_udpClient.EnableBroadcast = true;
|
||||
|
||||
_isListening = true;
|
||||
|
||||
ConvaiLogger.Info($"✅ Shared UDP Listener BOUND to port {_listenPort}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($" Will dispatch packets to handlers based on magic numbers:", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($" - Discovery: 0x44495343", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($" - Voice Audio: 0xC0A1", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($" - NPC Speech: 0xC0A3", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($" - Avatar Sync: 0xC0A0", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($" - Experiment Control: JSON (text-based)", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Start listening for all packets
|
||||
_ = ListenForPackets(_cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"❌ FAILED to bind Shared UDP Listener to port {_listenPort}: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Error($"Stack trace: {ex.StackTrace}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopListening()
|
||||
{
|
||||
if (!_isListening) return;
|
||||
|
||||
_isListening = false;
|
||||
_udpClient?.Close();
|
||||
_udpClient?.Dispose();
|
||||
_udpClient = null;
|
||||
|
||||
ConvaiLogger.Info("Shared UDP Listener stopped", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private async Task ListenForPackets(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (_isListening && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = await _udpClient.ReceiveAsync();
|
||||
_totalPacketsReceived++;
|
||||
_lastPacketTime = DateTime.UtcNow;
|
||||
|
||||
// Dispatch to all registered handlers
|
||||
OnPacketReceived?.Invoke(result.Buffer, result.RemoteEndPoint);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Normal when stopping
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_isListening)
|
||||
{
|
||||
ConvaiLogger.Error($"Error in shared UDP listener: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a packet through the shared UDP client
|
||||
/// </summary>
|
||||
public async Task<int> SendAsync(byte[] data, IPEndPoint endPoint)
|
||||
{
|
||||
if (_udpClient == null || !_isListening)
|
||||
{
|
||||
ConvaiLogger.Warn("Cannot send packet - UDP client not initialized", ConvaiLogger.LogCategory.Character);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await _udpClient.SendAsync(data, data.Length, endPoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Failed to send UDP packet: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a broadcast packet
|
||||
/// </summary>
|
||||
public async Task<int> SendBroadcastAsync(byte[] data, int port)
|
||||
{
|
||||
if (_udpClient == null || !_isListening)
|
||||
{
|
||||
ConvaiLogger.Warn("Cannot send broadcast - UDP client not initialized", ConvaiLogger.LogCategory.Character);
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, port);
|
||||
return await _udpClient.SendAsync(data, data.Length, broadcastEndpoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConvaiLogger.Error($"Failed to send broadcast packet: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d600859d24aeed44da6b10910a773ab3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Runtime.LoggerSystem;
|
||||
@ -29,7 +28,6 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
public event Action<ConnectionState> OnConnectionStateChanged;
|
||||
|
||||
// Network components
|
||||
private UdpClient _udpClient;
|
||||
private bool _isRunning = false;
|
||||
private int _listenPort;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
@ -39,6 +37,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
private byte _peerPlayerID = 0;
|
||||
private DateTime _lastPeerPacketTime;
|
||||
private ConnectionState _connectionState = ConnectionState.Disconnected;
|
||||
private bool _hasEverConnected = false; // Track if we've connected this session
|
||||
|
||||
// Discovery protocol constants
|
||||
private const uint DISCOVERY_MAGIC = 0x44495343; // "DISC" in hex
|
||||
@ -58,12 +57,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 +97,9 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Always start with discovery on game launch (IP addresses may have changed)
|
||||
LogEvent($"🔍 Discovery started (Player {localPlayerID})");
|
||||
StartDiscovery();
|
||||
}
|
||||
|
||||
@ -106,15 +115,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Check for peer timeout
|
||||
if (_connectionState == ConnectionState.Connected)
|
||||
{
|
||||
TimeSpan timeSinceLastPacket = DateTime.UtcNow - _lastPeerPacketTime;
|
||||
if (timeSinceLastPacket.TotalSeconds > peerTimeoutSeconds)
|
||||
{
|
||||
HandlePeerLost();
|
||||
}
|
||||
}
|
||||
// No timeout checks - once connected, stay connected until restart
|
||||
// Removed: peer timeout monitoring
|
||||
}
|
||||
|
||||
private void StartDiscovery()
|
||||
@ -123,25 +125,25 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
try
|
||||
{
|
||||
// Create UDP client with port reuse for shared port
|
||||
_udpClient = new UdpClient();
|
||||
_udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
_udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, _listenPort));
|
||||
_udpClient.EnableBroadcast = true;
|
||||
// Wait for shared listener to be ready
|
||||
if (SharedUDPListener.Instance == null)
|
||||
{
|
||||
ConvaiLogger.Error("SharedUDPListener not found! Make sure it's in the scene.", ConvaiLogger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
|
||||
// Subscribe to shared listener
|
||||
SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
|
||||
|
||||
_isRunning = true;
|
||||
SetConnectionState(ConnectionState.Discovering);
|
||||
|
||||
ConvaiLogger.Info($"🔍 Peer Discovery started - Player {localPlayerID} on port {_listenPort}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Start listening for discovery packets
|
||||
_ = ListenForDiscoveryPackets(_cancellationTokenSource.Token);
|
||||
ConvaiLogger.Info($"✅ Peer Discovery subscribed to shared listener - Player {localPlayerID}, listening for magic 0x{DISCOVERY_MAGIC:X}", ConvaiLogger.LogCategory.Character);
|
||||
|
||||
// Start broadcasting discovery requests
|
||||
_ = BroadcastDiscoveryRequests(_cancellationTokenSource.Token);
|
||||
|
||||
// Start heartbeat when connected
|
||||
_ = SendHeartbeats(_cancellationTokenSource.Token);
|
||||
// NO HEARTBEATS - once connected, stay connected
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -154,36 +156,27 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
if (!_isRunning) return;
|
||||
|
||||
_isRunning = false;
|
||||
_udpClient?.Close();
|
||||
_udpClient?.Dispose();
|
||||
_udpClient = null;
|
||||
|
||||
// Unsubscribe from shared listener
|
||||
if (SharedUDPListener.Instance != null)
|
||||
{
|
||||
SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
|
||||
}
|
||||
|
||||
SetConnectionState(ConnectionState.Disconnected);
|
||||
ConvaiLogger.Info("Peer Discovery stopped", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private async Task ListenForDiscoveryPackets(CancellationToken cancellationToken)
|
||||
private void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
|
||||
{
|
||||
while (_isRunning && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _udpClient.ReceiveAsync();
|
||||
await ProcessDiscoveryPacket(result.Buffer, result.RemoteEndPoint);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Normal when stopping
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
ConvaiLogger.Error($"Error receiving discovery packet: {ex.Message}", ConvaiLogger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if this is a discovery packet (by magic number)
|
||||
if (data.Length < 14) return;
|
||||
|
||||
uint magic = BitConverter.ToUInt32(data, 0);
|
||||
if (magic != DISCOVERY_MAGIC) return;
|
||||
|
||||
// Process discovery packet
|
||||
_ = ProcessDiscoveryPacket(data, senderEndPoint);
|
||||
}
|
||||
|
||||
private async Task BroadcastDiscoveryRequests(CancellationToken cancellationToken)
|
||||
@ -192,8 +185,9 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
try
|
||||
{
|
||||
// Only broadcast if we're still discovering
|
||||
if (_connectionState == ConnectionState.Discovering || _connectionState == ConnectionState.Lost)
|
||||
// Only broadcast if we're discovering AND haven't connected yet this session
|
||||
// Once connected, we remember the peer IP for this session
|
||||
if (_connectionState == ConnectionState.Discovering && !_hasEverConnected)
|
||||
{
|
||||
await SendDiscoveryRequest();
|
||||
}
|
||||
@ -219,8 +213,10 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
await Task.Delay((int)(heartbeatInterval * 1000), cancellationToken);
|
||||
|
||||
// Only send heartbeat if connected
|
||||
if (_connectionState == ConnectionState.Connected && !string.IsNullOrEmpty(_peerIP))
|
||||
// Send heartbeat if we have a peer IP (Connected or Lost state)
|
||||
// This allows automatic reconnection when peer comes back online
|
||||
if (!string.IsNullOrEmpty(_peerIP) &&
|
||||
(_connectionState == ConnectionState.Connected || _connectionState == ConnectionState.Lost || _connectionState == ConnectionState.Discovering))
|
||||
{
|
||||
await SendHeartbeat();
|
||||
}
|
||||
@ -242,9 +238,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
{
|
||||
byte[] packet = CreateDiscoveryPacket(PACKET_TYPE_REQUEST);
|
||||
|
||||
// Broadcast to subnet (will be blocked by AP isolation but we try anyway)
|
||||
var broadcastEndPoint = new IPEndPoint(IPAddress.Broadcast, _listenPort);
|
||||
await _udpClient.SendAsync(packet, packet.Length, broadcastEndPoint);
|
||||
// Broadcast to subnet using shared listener
|
||||
await SharedUDPListener.Instance.SendBroadcastAsync(packet, _listenPort);
|
||||
|
||||
if (enableDebugLogging && UnityEngine.Random.value < 0.1f) // Log 10% of broadcasts to reduce spam
|
||||
{
|
||||
@ -262,7 +257,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
try
|
||||
{
|
||||
byte[] packet = CreateDiscoveryPacket(PACKET_TYPE_RESPONSE);
|
||||
await _udpClient.SendAsync(packet, packet.Length, targetEndPoint);
|
||||
await SharedUDPListener.Instance.SendAsync(packet, targetEndPoint);
|
||||
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
@ -283,7 +278,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
byte[] packet = CreateDiscoveryPacket(PACKET_TYPE_HEARTBEAT);
|
||||
var peerEndPoint = new IPEndPoint(IPAddress.Parse(_peerIP), _listenPort);
|
||||
await _udpClient.SendAsync(packet, packet.Length, peerEndPoint);
|
||||
await SharedUDPListener.Instance.SendAsync(packet, peerEndPoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -373,16 +368,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
break;
|
||||
|
||||
case PACKET_TYPE_HEARTBEAT:
|
||||
// Heartbeat keeps connection alive
|
||||
if (_connectionState == ConnectionState.Connected && _peerIP == senderIP)
|
||||
{
|
||||
// Connection still alive
|
||||
}
|
||||
else if (_connectionState != ConnectionState.Connected)
|
||||
{
|
||||
// Reconnected
|
||||
HandlePeerDiscovered(senderIP, senderPlayerID);
|
||||
}
|
||||
// Ignore heartbeats - no reconnection logic
|
||||
// Once connected, stay connected until restart
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -397,10 +384,12 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
_peerIP = peerIP;
|
||||
_peerPlayerID = peerPlayerID;
|
||||
_lastPeerPacketTime = DateTime.UtcNow;
|
||||
_hasEverConnected = true; // Mark that we've connected this session
|
||||
|
||||
SetConnectionState(ConnectionState.Connected);
|
||||
|
||||
ConvaiLogger.Info($"✅ Peer discovered! Player {peerPlayerID} at {peerIP}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($"✅ Peer discovered! Player {peerPlayerID} at {peerIP} (will remember for this session)", ConvaiLogger.LogCategory.Character);
|
||||
LogEvent($"✅ Peer discovered! Player {peerPlayerID} at {peerIP}");
|
||||
|
||||
// Notify listeners
|
||||
OnPeerDiscovered?.Invoke(peerIP);
|
||||
@ -408,19 +397,12 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
|
||||
private void HandlePeerLost()
|
||||
{
|
||||
ConvaiLogger.Warn($"⚠️ Peer connection lost (Player {_peerPlayerID} at {_peerIP})", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Warn($"⚠️ Peer connection lost (Player {_peerPlayerID} at {_peerIP}) - but ignoring, keeping connection alive", ConvaiLogger.LogCategory.Character);
|
||||
LogEvent($"⚠️ Peer connection lost (Player {_peerPlayerID}) - IGNORING (stay connected until restart)");
|
||||
|
||||
string lostPeerIP = _peerIP;
|
||||
_peerIP = "";
|
||||
_peerPlayerID = 0;
|
||||
|
||||
SetConnectionState(ConnectionState.Lost);
|
||||
|
||||
// Notify listeners
|
||||
OnPeerLost?.Invoke();
|
||||
|
||||
// Restart discovery
|
||||
SetConnectionState(ConnectionState.Discovering);
|
||||
// DO NOTHING - keep the connection state as Connected
|
||||
// Once connected at the start of the session, stay connected forever
|
||||
// Senders will continue to send to the stored IP
|
||||
}
|
||||
|
||||
private void SetConnectionState(ConnectionState newState)
|
||||
@ -441,8 +423,12 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
public void RestartDiscovery()
|
||||
{
|
||||
ConvaiLogger.Info("Manually restarting peer discovery", ConvaiLogger.LogCategory.Character);
|
||||
LogEvent("🔄 Manually restarting discovery");
|
||||
|
||||
// Reset state
|
||||
_peerIP = "";
|
||||
_peerPlayerID = 0;
|
||||
_hasEverConnected = false;
|
||||
SetConnectionState(ConnectionState.Discovering);
|
||||
}
|
||||
|
||||
@ -454,6 +440,20 @@ namespace Convai.Scripts.Runtime.Multiplayer
|
||||
ConvaiLogger.Info($"Peer IP: {(_peerIP ?? "None")}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($"Peer Player ID: {_peerPlayerID}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($"Listen Port: {_listenPort}", ConvaiLogger.LogCategory.Character);
|
||||
ConvaiLogger.Info($"Has Ever Connected This Session: {_hasEverConnected}", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Unity-Master/Assets/Scripts/NetworkConfig.asset
(Stored with Git LFS)
BIN
Unity-Master/Assets/Scripts/NetworkConfig.asset
(Stored with Git LFS)
Binary file not shown.
@ -2,9 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
public class UDPAvatarReceiver : MonoBehaviour
|
||||
@ -38,9 +35,6 @@ public class UDPAvatarReceiver : MonoBehaviour
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool logReceivedPackets = false;
|
||||
|
||||
private UdpClient udpClient;
|
||||
private Thread udpListenerThread;
|
||||
private bool threadRunning = false;
|
||||
private int listenPort;
|
||||
private Dictionary<string, Transform> boneCache;
|
||||
private List<Transform> allBones; // For full data mode
|
||||
@ -104,7 +98,7 @@ public class UDPAvatarReceiver : MonoBehaviour
|
||||
|
||||
if (enableReceiver)
|
||||
{
|
||||
StartUDPListener();
|
||||
StartCoroutine(WaitAndSubscribe());
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
@ -205,122 +199,71 @@ public class UDPAvatarReceiver : MonoBehaviour
|
||||
{
|
||||
try
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
// Create UDP client with port reuse for local testing
|
||||
udpClient = new UdpClient();
|
||||
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, listenPort));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard UDP client binding
|
||||
udpClient = new UdpClient(listenPort);
|
||||
}
|
||||
|
||||
threadRunning = true;
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerThread));
|
||||
udpListenerThread.IsBackground = true;
|
||||
udpListenerThread.Start();
|
||||
// Subscribe to shared listener
|
||||
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"UDP Avatar Receiver started on port {listenPort} (filtering binary avatar data only, Port sharing: {allowPortSharing})");
|
||||
Debug.Log($"UDP Avatar Receiver subscribed to shared listener on port {listenPort} (filtering avatar magic 0x{AVATAR_MAGIC:X})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (allowPortSharing)
|
||||
Debug.LogError($"Failed to subscribe to shared UDP listener: {e.Message}");
|
||||
enableReceiver = false;
|
||||
}
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator WaitAndSubscribe()
|
||||
{
|
||||
float timeout = 3f;
|
||||
while (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null && timeout > 0f)
|
||||
{
|
||||
timeout -= Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null)
|
||||
{
|
||||
Debug.LogError("SharedUDPListener not ready after wait.");
|
||||
enableReceiver = false;
|
||||
yield break;
|
||||
}
|
||||
StartUDPListener();
|
||||
}
|
||||
|
||||
void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
|
||||
{
|
||||
// Check if this is avatar data (by magic number)
|
||||
if (!IsAvatarData(data)) return;
|
||||
|
||||
// Process avatar packet
|
||||
CompactAvatarData avatarData = DeserializeCompactData(data);
|
||||
|
||||
// Check if this is from the target player (0 means accept from any player)
|
||||
if (targetPlayerID == 0 || avatarData.playerID == targetPlayerID)
|
||||
{
|
||||
// Check for packet loss
|
||||
if (avatarData.sequenceNumber > lastSequenceNumber + 1)
|
||||
{
|
||||
Debug.LogWarning($"Failed to start UDP listener with port sharing: {e.Message}");
|
||||
Debug.LogWarning("Trying with different port...");
|
||||
TryAlternativePort();
|
||||
packetsDropped += (int)(avatarData.sequenceNumber - lastSequenceNumber - 1);
|
||||
}
|
||||
else
|
||||
|
||||
lastSequenceNumber = avatarData.sequenceNumber;
|
||||
packetsReceived++;
|
||||
|
||||
// Store the new data (thread-safe)
|
||||
lock (dataLock)
|
||||
{
|
||||
Debug.LogError($"Failed to start UDP listener: {e.Message}");
|
||||
enableReceiver = false;
|
||||
lastReceivedData = avatarData;
|
||||
hasNewData = true;
|
||||
}
|
||||
|
||||
if (logReceivedPackets && packetsReceived % 30 == 0)
|
||||
{
|
||||
string modeStr = avatarData.isFullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Received {modeStr} packet #{avatarData.sequenceNumber} from player {avatarData.playerID}, size: {data.Length} bytes, bones: {avatarData.bones?.Length ?? 0}, blend shapes: {avatarData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TryAlternativePort()
|
||||
{
|
||||
// Try a few alternative ports for local testing
|
||||
int[] alternativePorts = { 8081, 8082, 8083, 8084, 8085 };
|
||||
|
||||
foreach (int port in alternativePorts)
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient(port);
|
||||
threadRunning = true;
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerThread));
|
||||
udpListenerThread.IsBackground = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
Debug.Log($"UDP listener started on alternative port {port}");
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Try next port
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogError("Failed to start UDP listener on any available port");
|
||||
enableReceiver = false;
|
||||
}
|
||||
|
||||
void UDPListenerThread()
|
||||
{
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
|
||||
|
||||
while (threadRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] data = udpClient.Receive(ref remoteEndPoint);
|
||||
|
||||
if (data.Length > 0 && IsAvatarData(data))
|
||||
{
|
||||
CompactAvatarData avatarData = DeserializeCompactData(data);
|
||||
|
||||
// Check if this is from the target player (0 means accept from any player)
|
||||
if (targetPlayerID == 0 || avatarData.playerID == targetPlayerID)
|
||||
{
|
||||
// Check for packet loss
|
||||
if (avatarData.sequenceNumber > lastSequenceNumber + 1)
|
||||
{
|
||||
packetsDropped += (int)(avatarData.sequenceNumber - lastSequenceNumber - 1);
|
||||
}
|
||||
|
||||
lastSequenceNumber = avatarData.sequenceNumber;
|
||||
packetsReceived++;
|
||||
|
||||
// Store the new data (thread-safe)
|
||||
lock (dataLock)
|
||||
{
|
||||
lastReceivedData = avatarData;
|
||||
hasNewData = true;
|
||||
}
|
||||
|
||||
if (logReceivedPackets && packetsReceived % 30 == 0)
|
||||
{
|
||||
string modeStr = avatarData.isFullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Received {modeStr} packet #{avatarData.sequenceNumber} from player {avatarData.playerID}, size: {data.Length} bytes, bones: {avatarData.bones?.Length ?? 0}, blend shapes: {avatarData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (threadRunning) // Only log errors if we're supposed to be running
|
||||
{
|
||||
Debug.LogError($"UDP receive error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAvatarData(byte[] data)
|
||||
{
|
||||
@ -616,19 +559,10 @@ public class UDPAvatarReceiver : MonoBehaviour
|
||||
|
||||
void StopUDPListener()
|
||||
{
|
||||
threadRunning = false;
|
||||
|
||||
if (udpClient != null)
|
||||
// Unsubscribe from shared listener
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient.Dispose();
|
||||
udpClient = null;
|
||||
}
|
||||
|
||||
if (udpListenerThread != null)
|
||||
{
|
||||
udpListenerThread.Join(1000); // Wait up to 1 second for thread to finish
|
||||
udpListenerThread = null;
|
||||
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
|
||||
}
|
||||
}
|
||||
|
||||
@ -688,9 +622,9 @@ public class UDPAvatarReceiver : MonoBehaviour
|
||||
|
||||
int GetActualListenPort()
|
||||
{
|
||||
if (udpClient?.Client?.LocalEndPoint != null)
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
|
||||
{
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
return Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.ListenPort;
|
||||
}
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@ -2,9 +2,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
@ -41,9 +38,6 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool logReceivedPackets = false;
|
||||
|
||||
private UdpClient udpClient;
|
||||
private Thread udpListenerThread;
|
||||
private bool threadRunning = false;
|
||||
private int listenPort;
|
||||
private Dictionary<string, Transform> boneCache;
|
||||
private List<Transform> allBones; // For full data mode
|
||||
@ -107,7 +101,7 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
|
||||
|
||||
if (enableReceiver)
|
||||
{
|
||||
StartUDPListener();
|
||||
StartCoroutine(WaitAndSubscribe());
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
@ -208,122 +202,71 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
|
||||
{
|
||||
try
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
// Create UDP client with port reuse for local testing
|
||||
udpClient = new UdpClient();
|
||||
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, listenPort));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard UDP client binding
|
||||
udpClient = new UdpClient(listenPort);
|
||||
}
|
||||
|
||||
threadRunning = true;
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerThread));
|
||||
udpListenerThread.IsBackground = true;
|
||||
udpListenerThread.Start();
|
||||
// Subscribe to shared listener
|
||||
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"UDP Avatar Receiver Agent started on port {listenPort} (filtering binary avatar data only, Port sharing: {allowPortSharing})");
|
||||
Debug.Log($"UDP Avatar Receiver Agent subscribed to shared listener on port {listenPort} (filtering avatar magic 0x{AVATAR_MAGIC:X})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (allowPortSharing)
|
||||
Debug.LogError($"Failed to subscribe to shared UDP listener: {e.Message}");
|
||||
enableReceiver = false;
|
||||
}
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator WaitAndSubscribe()
|
||||
{
|
||||
float timeout = 3f;
|
||||
while (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null && timeout > 0f)
|
||||
{
|
||||
timeout -= Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null)
|
||||
{
|
||||
Debug.LogError("SharedUDPListener not ready after wait.");
|
||||
enableReceiver = false;
|
||||
yield break;
|
||||
}
|
||||
StartUDPListener();
|
||||
}
|
||||
|
||||
void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
|
||||
{
|
||||
// Check if this is avatar data (by magic number)
|
||||
if (!IsAvatarData(data)) return;
|
||||
|
||||
// Process avatar packet
|
||||
CompactAvatarData avatarData = DeserializeCompactData(data);
|
||||
|
||||
// Check if this is from the target player (0 means accept from any player)
|
||||
if (targetPlayerID == 0 || avatarData.playerID == targetPlayerID)
|
||||
{
|
||||
// Check for packet loss
|
||||
if (avatarData.sequenceNumber > lastSequenceNumber + 1)
|
||||
{
|
||||
Debug.LogWarning($"Failed to start UDP listener with port sharing: {e.Message}");
|
||||
Debug.LogWarning("Trying with different port...");
|
||||
TryAlternativePort();
|
||||
packetsDropped += (int)(avatarData.sequenceNumber - lastSequenceNumber - 1);
|
||||
}
|
||||
else
|
||||
|
||||
lastSequenceNumber = avatarData.sequenceNumber;
|
||||
packetsReceived++;
|
||||
|
||||
// Store the new data (thread-safe)
|
||||
lock (dataLock)
|
||||
{
|
||||
Debug.LogError($"Failed to start UDP listener: {e.Message}");
|
||||
enableReceiver = false;
|
||||
lastReceivedData = avatarData;
|
||||
hasNewData = true;
|
||||
}
|
||||
|
||||
if (logReceivedPackets && packetsReceived % 30 == 0)
|
||||
{
|
||||
string modeStr = avatarData.isFullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Received {modeStr} packet #{avatarData.sequenceNumber} from player {avatarData.playerID}, size: {data.Length} bytes, bones: {avatarData.bones?.Length ?? 0}, blend shapes: {avatarData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TryAlternativePort()
|
||||
{
|
||||
// Try a few alternative ports for local testing
|
||||
int[] alternativePorts = { 8081, 8082, 8083, 8084, 8085 };
|
||||
|
||||
foreach (int port in alternativePorts)
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient(port);
|
||||
threadRunning = true;
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerThread));
|
||||
udpListenerThread.IsBackground = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
Debug.Log($"UDP listener started on alternative port {port}");
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Try next port
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogError("Failed to start UDP listener on any available port");
|
||||
enableReceiver = false;
|
||||
}
|
||||
|
||||
void UDPListenerThread()
|
||||
{
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
|
||||
|
||||
while (threadRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] data = udpClient.Receive(ref remoteEndPoint);
|
||||
|
||||
if (data.Length > 0 && IsAvatarData(data))
|
||||
{
|
||||
CompactAvatarData avatarData = DeserializeCompactData(data);
|
||||
|
||||
// Check if this is from the target player (0 means accept from any player)
|
||||
if (targetPlayerID == 0 || avatarData.playerID == targetPlayerID)
|
||||
{
|
||||
// Check for packet loss
|
||||
if (avatarData.sequenceNumber > lastSequenceNumber + 1)
|
||||
{
|
||||
packetsDropped += (int)(avatarData.sequenceNumber - lastSequenceNumber - 1);
|
||||
}
|
||||
|
||||
lastSequenceNumber = avatarData.sequenceNumber;
|
||||
packetsReceived++;
|
||||
|
||||
// Store the new data (thread-safe)
|
||||
lock (dataLock)
|
||||
{
|
||||
lastReceivedData = avatarData;
|
||||
hasNewData = true;
|
||||
}
|
||||
|
||||
if (logReceivedPackets && packetsReceived % 30 == 0)
|
||||
{
|
||||
string modeStr = avatarData.isFullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Received {modeStr} packet #{avatarData.sequenceNumber} from player {avatarData.playerID}, size: {data.Length} bytes, bones: {avatarData.bones?.Length ?? 0}, blend shapes: {avatarData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (threadRunning) // Only log errors if we're supposed to be running
|
||||
{
|
||||
Debug.LogError($"UDP receive error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAvatarData(byte[] data)
|
||||
{
|
||||
@ -619,19 +562,10 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
|
||||
|
||||
void StopUDPListener()
|
||||
{
|
||||
threadRunning = false;
|
||||
|
||||
if (udpClient != null)
|
||||
// Unsubscribe from shared listener
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient.Dispose();
|
||||
udpClient = null;
|
||||
}
|
||||
|
||||
if (udpListenerThread != null)
|
||||
{
|
||||
udpListenerThread.Join(1000); // Wait up to 1 second for thread to finish
|
||||
udpListenerThread = null;
|
||||
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
|
||||
}
|
||||
}
|
||||
|
||||
@ -691,9 +625,9 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
|
||||
|
||||
int GetActualListenPort()
|
||||
{
|
||||
if (udpClient?.Client?.LocalEndPoint != null)
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
|
||||
{
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
return Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.ListenPort;
|
||||
}
|
||||
return listenPort;
|
||||
}
|
||||
|
||||
@ -2,9 +2,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -34,8 +32,6 @@ public class VRExperimentController : MonoBehaviour
|
||||
[SerializeField] private bool enableDebugLogging = true;
|
||||
|
||||
// Network components
|
||||
private UdpClient udpClient;
|
||||
private Thread udpListenerThread;
|
||||
private bool isListening = false;
|
||||
private int udpPort;
|
||||
|
||||
@ -68,7 +64,7 @@ public class VRExperimentController : MonoBehaviour
|
||||
}
|
||||
|
||||
InitializeObjectMaps();
|
||||
StartUDPListener();
|
||||
StartCoroutine(WaitAndSubscribe());
|
||||
|
||||
// Initially deactivate all objects and avatars
|
||||
DeactivateAllObjects();
|
||||
@ -197,70 +193,49 @@ public class VRExperimentController : MonoBehaviour
|
||||
{
|
||||
try
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
// Create UDP client with port reuse for local testing
|
||||
udpClient = new UdpClient();
|
||||
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, udpPort));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard UDP client binding
|
||||
udpClient = new UdpClient(udpPort);
|
||||
}
|
||||
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerLoop));
|
||||
udpListenerThread.IsBackground = true;
|
||||
// Subscribe to shared listener
|
||||
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
|
||||
isListening = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
LogMessage($"UDP Experiment Control Listener started on port {udpPort} (filtering JSON messages only, Port sharing: {allowPortSharing})");
|
||||
LogMessage($"UDP Experiment Control Listener subscribed to shared listener on port {udpPort} (filtering JSON messages only)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
LogError($"Failed to start UDP listener with port sharing: {e.Message}");
|
||||
LogMessage("Trying with different port...");
|
||||
TryAlternativePort();
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError($"Failed to start UDP listener: {e.Message}");
|
||||
}
|
||||
LogError($"Failed to subscribe to shared UDP listener: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try alternative ports for local testing
|
||||
/// </summary>
|
||||
private void TryAlternativePort()
|
||||
private System.Collections.IEnumerator WaitAndSubscribe()
|
||||
{
|
||||
// Try a few alternative ports for local testing
|
||||
int[] alternativePorts = { 1222, 1223, 1224, 1225, 1226 };
|
||||
|
||||
foreach (int port in alternativePorts)
|
||||
float timeout = 3f;
|
||||
while (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null && timeout > 0f)
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient(port);
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerLoop));
|
||||
udpListenerThread.IsBackground = true;
|
||||
isListening = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
LogMessage($"UDP Experiment Control Listener started on alternative port {port}");
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Try next port
|
||||
continue;
|
||||
}
|
||||
timeout -= Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null)
|
||||
{
|
||||
LogError("SharedUDPListener not ready after wait.");
|
||||
yield break;
|
||||
}
|
||||
StartUDPListener();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle packet received from shared listener
|
||||
/// </summary>
|
||||
private void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
|
||||
{
|
||||
// Check if this is an experiment control message (JSON)
|
||||
if (!IsExperimentControlMessage(data)) return;
|
||||
|
||||
LogError("Failed to start UDP listener on any available port");
|
||||
string message = Encoding.UTF8.GetString(data);
|
||||
|
||||
// Add message to queue for main thread processing
|
||||
lock (queueLock)
|
||||
{
|
||||
messageQueue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -270,59 +245,15 @@ public class VRExperimentController : MonoBehaviour
|
||||
{
|
||||
isListening = false;
|
||||
|
||||
if (udpClient != null)
|
||||
// Unsubscribe from shared listener
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient = null;
|
||||
}
|
||||
|
||||
if (udpListenerThread != null && udpListenerThread.IsAlive)
|
||||
{
|
||||
udpListenerThread.Join(1000); // Wait up to 1 second
|
||||
if (udpListenerThread.IsAlive)
|
||||
{
|
||||
udpListenerThread.Abort();
|
||||
}
|
||||
udpListenerThread = null;
|
||||
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
|
||||
}
|
||||
|
||||
LogMessage("UDP Listener stopped");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UDP listener loop (runs in separate thread)
|
||||
/// </summary>
|
||||
private void UDPListenerLoop()
|
||||
{
|
||||
while (isListening)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, udpPort);
|
||||
byte[] data = udpClient.Receive(ref remoteEndPoint);
|
||||
|
||||
// Check if this looks like a JSON experiment control message
|
||||
if (IsExperimentControlMessage(data))
|
||||
{
|
||||
string message = Encoding.UTF8.GetString(data);
|
||||
|
||||
// Add message to queue for main thread processing
|
||||
lock (queueLock)
|
||||
{
|
||||
messageQueue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
// If it's not an experiment control message (likely avatar data), ignore it
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isListening) // Only log if we're still supposed to be listening
|
||||
{
|
||||
LogError($"UDP Listener error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the received data is an experiment control message (JSON) vs avatar data (binary)
|
||||
@ -684,9 +615,9 @@ public class VRExperimentController : MonoBehaviour
|
||||
/// </summary>
|
||||
private int GetActualListenPort()
|
||||
{
|
||||
if (udpClient?.Client?.LocalEndPoint != null)
|
||||
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
|
||||
{
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
return Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.ListenPort;
|
||||
}
|
||||
return udpPort;
|
||||
}
|
||||
|
||||
141
Unity-Master/Assets/Scripts/script-overview.md
Normal file
141
Unity-Master/Assets/Scripts/script-overview.md
Normal file
@ -0,0 +1,141 @@
|
||||
# Masterarbeit UDP Multiplayer System: Structure & Roles Overview
|
||||
|
||||
This document explains how this custom Unity multiplayer system works. It covers real-time avatar, audio, and experiment synchronization, and goes through all the main multiplayer components.
|
||||
|
||||
---
|
||||
|
||||
## 1. High-Level Architecture
|
||||
|
||||
The system enables multiple VR clients (users, experimenters, or bots) to:
|
||||
- Synchronize avatars (pose, facial blendshapes)
|
||||
- Stream live audio (mic/NPC speech)
|
||||
- React to central experiment control (tasks, object activation)
|
||||
- Discover peers automatically (no manual IP entry)
|
||||
- Debug and inspect all network state in real time
|
||||
|
||||
All this communication happens over a _single shared UDP port_ (from `NetworkConfig`), allowing both unicast and broadcast, and avoiding race conditions/conflicts in Unity's component-based design.
|
||||
|
||||
---
|
||||
|
||||
## 2. Key Modules & Their Roles
|
||||
|
||||
### **A. Avatar Synchronization**
|
||||
|
||||
**Scripts:**
|
||||
- `UDPAvatarBroadcaster.cs`, `UDPAvatarBroadcasterAgent.cs`
|
||||
- `UDPAvatarReceiver.cs`, `UDPAvatarReceiverAgent.cs`
|
||||
- _Location_: Assets/Scripts (main code), "Agent" variants support ReadyPlayerMe/CC4 naming
|
||||
|
||||
**Function:**
|
||||
- **Broadcaster**: Gathers local avatar transform (root, bones, blendshapes) and sends packets at a steady rate using UDP (binary, compact protocol)
|
||||
- **Receiver**: Applies the most recent received pose/facial data to the local duplicate avatar
|
||||
- **Mode:** Optimized (priority bones only) or Full (all bones/blends)
|
||||
|
||||
**Public API:**
|
||||
- `SetPlayerID(byte id)` (filter by remote peer if needed)
|
||||
- Inspector toggles (start/stop broadcast, debug info)
|
||||
|
||||
---
|
||||
|
||||
### **B. Audio Streaming**
|
||||
|
||||
**Scripts:**
|
||||
- `ConvaiSimpleUDPAudioSender/Receiver.cs` (Player-to-player, live mic, PTT)
|
||||
- `ConvaiUDPSpeechSender/Receiver.cs` (NPC/AI speech across network)
|
||||
- _Location_: Assets/Scripts/Multiplayer
|
||||
|
||||
**Function:**
|
||||
- **SimpleUDPAudioSender**: Captures mic, chunks to short UDP packets, broadcasts to peer
|
||||
- **SimpleUDPAudioReceiver**: Buffers incoming packets, simulates mic input on target NPC
|
||||
- **UDPSpeechSender/Receiver**: Dedicated for Convai/ReadyPlayerMe/NPC speech playback, includes transcript metadata and allows for sequential clip transmission (not just live voice)
|
||||
|
||||
**Public Events:**
|
||||
- `OnAudioReceiving`, `OnSpeechReceiving` (Unity Actions for state/busy status)
|
||||
|
||||
---
|
||||
|
||||
### **C. Experiment State Control**
|
||||
|
||||
**Script:**
|
||||
- `VRExperimentController.cs`
|
||||
- _Location_: Assets/Scripts
|
||||
|
||||
**Function:**
|
||||
- Listens to incoming UDP (JSON messages) for commands from a supervisor or experimenter
|
||||
- Controls what avatars/objects/world elements are active based on experimental tasks/blocks/trials
|
||||
- Ensures logic for practice rounds, condition changes, object resets
|
||||
|
||||
**Key API/Bindings:**
|
||||
- Maps all avatars/objects via inspector
|
||||
- Handles: start/stop experiment, practice block, activate/deactivate particular items
|
||||
- Scene state queries (is object/condition active, etc)
|
||||
|
||||
---
|
||||
|
||||
### **D. UDP Port Sharing & Packet Routing**
|
||||
|
||||
**Script:**
|
||||
- `SharedUDPListener.cs`
|
||||
- _Location_: Assets/Scripts/Multiplayer
|
||||
|
||||
**Function:**
|
||||
- **Singleton** MonoBehaviour that _owns the UDP socket_
|
||||
- Dispatches incoming packets to all subscribers based on event: `OnPacketReceived(byte[] packet, IPEndPoint from)`
|
||||
- Ensures only one script binds to each UDP port (solves race/conflict problem in Unity)
|
||||
|
||||
**Usage:**
|
||||
- All networked scripts _subscribe/unsubscribe_ as needed
|
||||
|
||||
---
|
||||
|
||||
### **E. Peer Discovery & Dynamic Targeting**
|
||||
|
||||
**Script:**
|
||||
- `UDPPeerDiscovery.cs`
|
||||
- _Location_: Assets/Scripts/Multiplayer
|
||||
|
||||
**Function:**
|
||||
- Broadcasts regular 'hello' packets with player ID and waits for similar packets from peers
|
||||
- On discovery, updates all Broadcaster/Audio scripts to direct unicast, not just broadcast
|
||||
- Handles peer disconnects/loss, provides connection state events
|
||||
|
||||
**Public Events:**
|
||||
- `OnPeerDiscovered`, `OnPeerLost`, and status properties (connection state, peer IP, local player ID)
|
||||
|
||||
---
|
||||
|
||||
### **F. In-Game Network Debugging**
|
||||
|
||||
**Script:**
|
||||
- `NetworkDebugUI.cs`
|
||||
- _Location_: Assets/Scripts/Multiplayer
|
||||
|
||||
**Function:**
|
||||
- Provides a world-space (VR compatible) or 2D overlay UI showing:
|
||||
- Peer discovery status
|
||||
- Audio/pose sender/receiver status (packets, port, active/inactive)
|
||||
- Experiment state, currently active objects
|
||||
- Packet throughput/logs
|
||||
- Uses Input System for toggle, supports VR and editor
|
||||
|
||||
**Public API:**
|
||||
- Toggle/show/hide programmatically
|
||||
- Extendable for additional waveform, voice, or custom logs
|
||||
|
||||
---
|
||||
|
||||
## 4. Script/Module Summary Table
|
||||
|
||||
| Script | Role/Function | Key API / Events |
|
||||
|-----------------------------------|----------------------------------------------------------------------|---------------------------------|
|
||||
| UDPAvatarBroadcaster(.Agent) | Broadcast live pose/facial state as binary UDP | SetPlayerID, Inspector toggles |
|
||||
| UDPAvatarReceiver(.Agent) | Receive/apply remote pose/facial updates | SetPlayerID |
|
||||
| ConvaiSimpleUDPAudioSender/Rcv | Mic/PTT live audio stream; player voice | OnAudioReceiving |
|
||||
| ConvaiUDPSpeechSender/Receiver | NPC/AI speech transfer with transcript | OnSpeechReceiving |
|
||||
| VRExperimentController | Orchestrate scene, respond to experiment JSON, control object state | Condition/object methods |
|
||||
| SharedUDPListener | UDP socket singleton, routes all packets | OnPacketReceived event |
|
||||
| UDPPeerDiscovery | Peer IP autodiscovery, targeting updates | OnPeerDiscovered/Lost |
|
||||
| NetworkDebugUI | VR debug/status in-game overlay | Show/Hide |
|
||||
| NetworkConfig | ScriptableObject for IP/port/config | Static instance |
|
||||
|
||||
---
|
||||
7
Unity-Master/Assets/Scripts/script-overview.md.meta
Normal file
7
Unity-Master/Assets/Scripts/script-overview.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b725a59a21efb044ba2d8b4ef29095c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Unity-Master/ProjectSettings/EditorBuildSettings.asset
(Stored with Git LFS)
BIN
Unity-Master/ProjectSettings/EditorBuildSettings.asset
(Stored with Git LFS)
Binary file not shown.
BIN
Unity-Master/ProjectSettings/ProjectSettings.asset
(Stored with Git LFS)
BIN
Unity-Master/ProjectSettings/ProjectSettings.asset
(Stored with Git LFS)
Binary file not shown.
Reference in New Issue
Block a user