created new UDP scripts for Agents
This commit is contained in:
5
.stfolder/syncthing-folder-9cdae2.txt
Normal file
5
.stfolder/syncthing-folder-9cdae2.txt
Normal file
@ -0,0 +1,5 @@
|
||||
# This directory is a Syncthing folder marker.
|
||||
# Do not delete.
|
||||
|
||||
folderID: wvyum-ftfs2
|
||||
created: 2025-09-26T17:23:38+02:00
|
||||
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.
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a39805992c022064eaa29d4fbc63b48e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
487
Unity-Master/Assets/Scripts/UDPAvatarBroadcasterAgent.cs
Normal file
487
Unity-Master/Assets/Scripts/UDPAvatarBroadcasterAgent.cs
Normal file
@ -0,0 +1,487 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// UDPAvatarBroadcasterAgent - Adapted for CC_Base_ bone naming convention (Ready Player Me / Reallusion Character Creator)
|
||||
/// </summary>
|
||||
public class UDPAvatarBroadcasterAgent : MonoBehaviour
|
||||
{
|
||||
[Header("Network Configuration")]
|
||||
[SerializeField] private int broadcastPort = 8080;
|
||||
[SerializeField] private string broadcastAddress = "10.138.6.255"; // Local network broadcast
|
||||
[SerializeField] private bool useGlobalNetworkConfig = true;
|
||||
[SerializeField] private NetworkConfig networkConfigAsset;
|
||||
[SerializeField] private bool enableBroadcast = true;
|
||||
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform avatarRoot;
|
||||
[SerializeField] private byte playerID = 5; // Unique ID for this player (1-255)
|
||||
[SerializeField] private float updateRate = 30f; // Updates per second
|
||||
|
||||
[Header("Data Selection Mode")]
|
||||
[SerializeField] private bool fullDataMode = false; // Send ALL bones and blend shapes
|
||||
[SerializeField] private bool showFullModeWarning = true;
|
||||
|
||||
[Header("Optimized Mode - Core Bones (CC_Base_ naming)")]
|
||||
[SerializeField] private string[] priorityBones = {
|
||||
"CC_Base_Hip", "CC_Base_Pelvis", "CC_Base_Spine01", "CC_Base_Spine02", "CC_Base_Neck", "CC_Base_Head",
|
||||
"CC_Base_L_Clavicle", "CC_Base_L_Upperarm", "CC_Base_L_Forearm", "CC_Base_L_Hand",
|
||||
"CC_Base_R_Clavicle", "CC_Base_R_Upperarm", "CC_Base_R_Forearm", "CC_Base_R_Hand",
|
||||
"CC_Base_L_Thigh", "CC_Base_L_Calf", "CC_Base_L_Foot",
|
||||
"CC_Base_R_Thigh", "CC_Base_R_Calf", "CC_Base_R_Foot"
|
||||
};
|
||||
|
||||
[Header("Facial Animation")]
|
||||
[SerializeField] private bool includeFacialData = true;
|
||||
[SerializeField] private int maxBlendShapes = 10; // Most important blend shapes only (optimized mode)
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool logPacketSize = false;
|
||||
|
||||
private UdpClient udpClient;
|
||||
private IPEndPoint broadcastEndPoint;
|
||||
private Dictionary<string, Transform> boneCache;
|
||||
private List<Transform> allBones; // For full data mode
|
||||
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
|
||||
private SkinnedMeshRenderer headMesh;
|
||||
private float lastUpdateTime;
|
||||
private uint sequenceNumber = 0;
|
||||
private int currentBoneCount;
|
||||
private int currentBlendShapeCount;
|
||||
|
||||
// Avatar data structure - supports both optimized and full data modes
|
||||
private struct CompactAvatarData
|
||||
{
|
||||
public byte playerID; // 1 byte
|
||||
public uint sequenceNumber; // 4 bytes
|
||||
public uint timestamp; // 4 bytes (Unity time as uint milliseconds)
|
||||
public bool isFullDataMode; // 1 byte - indicates if this is full or optimized data
|
||||
|
||||
// Root transform (40 bytes)
|
||||
public Vector3 rootPosition; // 12 bytes
|
||||
public Quaternion rootRotation; // 16 bytes
|
||||
public Vector3 rootScale; // 12 bytes
|
||||
|
||||
// Variable-length bone data
|
||||
// Optimized: 20 bones * 28 bytes = 560 bytes
|
||||
// Full mode: All bones * 28 bytes (can be 80+ bones = 2240+ bytes)
|
||||
public BoneTransform[] bones;
|
||||
|
||||
// Variable-length blend shape data
|
||||
// Optimized: 10 floats = 40 bytes
|
||||
// Full mode: All blend shapes = up to 200+ floats = 800+ bytes
|
||||
public float[] blendShapes;
|
||||
|
||||
// Total optimized: ~650 bytes, Full mode: potentially 3000+ bytes
|
||||
}
|
||||
|
||||
private struct BoneTransform
|
||||
{
|
||||
public Vector3 position; // 12 bytes
|
||||
public Quaternion rotation; // 16 bytes
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Apply global config if enabled
|
||||
if (useGlobalNetworkConfig)
|
||||
{
|
||||
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
|
||||
if (cfg != null)
|
||||
{
|
||||
broadcastAddress = cfg.ipAddress;
|
||||
broadcastPort = cfg.port;
|
||||
}
|
||||
}
|
||||
InitializeNetworking();
|
||||
CacheAvatarComponents();
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"UDP Avatar Broadcaster Agent initialized - Player {playerID}");
|
||||
Debug.Log($"Broadcasting to {broadcastAddress}:{broadcastPort}");
|
||||
Debug.Log($"Priority bones: {priorityBones.Length}");
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeNetworking()
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient();
|
||||
udpClient.EnableBroadcast = true;
|
||||
broadcastEndPoint = new IPEndPoint(IPAddress.Parse(broadcastAddress), broadcastPort);
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log("UDP client initialized successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to initialize UDP client: {e.Message}");
|
||||
enableBroadcast = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CacheAvatarComponents()
|
||||
{
|
||||
if (avatarRoot == null)
|
||||
avatarRoot = transform;
|
||||
|
||||
boneCache = new Dictionary<string, Transform>();
|
||||
allBones = new List<Transform>();
|
||||
allMeshes = new List<SkinnedMeshRenderer>();
|
||||
|
||||
Transform[] allChildren = avatarRoot.GetComponentsInChildren<Transform>();
|
||||
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full data mode: Cache ALL bones with SkinnedMeshRenderer references
|
||||
SkinnedMeshRenderer[] meshRenderers = avatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
HashSet<Transform> uniqueBones = new HashSet<Transform>();
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in meshRenderers)
|
||||
{
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
uniqueBones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allBones.AddRange(uniqueBones);
|
||||
currentBoneCount = allBones.Count;
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Full data mode: Cached {currentBoneCount} bones");
|
||||
|
||||
// Show warning about packet size
|
||||
if (showFullModeWarning)
|
||||
{
|
||||
int estimatedSize = CalculateFullModePacketSize();
|
||||
if (estimatedSize > 1400) // Close to UDP limit
|
||||
{
|
||||
Debug.LogWarning($"Full data mode packet size: ~{estimatedSize} bytes. Consider optimized mode for better network performance.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: Cache priority bones only
|
||||
foreach (Transform child in allChildren)
|
||||
{
|
||||
foreach (string boneName in priorityBones)
|
||||
{
|
||||
if (child.name.Equals(boneName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!boneCache.ContainsKey(boneName))
|
||||
{
|
||||
boneCache[boneName] = child;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Cached priority bone: {boneName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
currentBoneCount = priorityBones.Length;
|
||||
}
|
||||
|
||||
// Cache facial animation data
|
||||
if (includeFacialData)
|
||||
{
|
||||
SkinnedMeshRenderer[] meshes = avatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full mode: collect all meshes with blend shapes
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.sharedMesh != null && mesh.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
allMeshes.Add(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total blend shape count
|
||||
currentBlendShapeCount = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
{
|
||||
currentBlendShapeCount += mesh.sharedMesh.blendShapeCount;
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Full data mode: Found {allMeshes.Count} meshes with {currentBlendShapeCount} total blend shapes");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: find head mesh only
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.name.ToLower().Contains("head") || mesh.name.ToLower().Contains("face") || mesh.name.ToLower().Contains("wolf3d_head"))
|
||||
{
|
||||
headMesh = mesh;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Found head mesh: {mesh.name} with {mesh.sharedMesh.blendShapeCount} blend shapes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentBlendShapeCount = maxBlendShapes;
|
||||
}
|
||||
}
|
||||
|
||||
string modeStr = fullDataMode ? "FULL" : "OPTIMIZED";
|
||||
Debug.Log($"{modeStr} mode (Agent): {currentBoneCount} bones, {currentBlendShapeCount} blend shapes");
|
||||
}
|
||||
|
||||
int CalculateFullModePacketSize()
|
||||
{
|
||||
int headerSize = 1 + 4 + 4 + 1; // playerID + sequence + timestamp + isFullDataMode
|
||||
int rootTransformSize = (3 + 4 + 3) * 4; // position + rotation + scale
|
||||
int bonesSize = 4 + (currentBoneCount * 7 * 4); // bone count + bones * (pos + rot) * float size
|
||||
int blendShapesSize = 4 + (currentBlendShapeCount * 4); // blend shape count + weights * float size
|
||||
return headerSize + rootTransformSize + bonesSize + blendShapesSize;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!enableBroadcast || udpClient == null)
|
||||
return;
|
||||
|
||||
// Rate-limited updates
|
||||
if (Time.time - lastUpdateTime >= 1f / updateRate)
|
||||
{
|
||||
BroadcastAvatarData();
|
||||
lastUpdateTime = Time.time;
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastAvatarData()
|
||||
{
|
||||
try
|
||||
{
|
||||
CompactAvatarData data = new CompactAvatarData
|
||||
{
|
||||
playerID = playerID,
|
||||
sequenceNumber = sequenceNumber++,
|
||||
timestamp = (uint)(Time.time * 1000), // Convert to milliseconds
|
||||
isFullDataMode = fullDataMode,
|
||||
|
||||
rootPosition = avatarRoot.position,
|
||||
rootRotation = avatarRoot.rotation,
|
||||
rootScale = avatarRoot.localScale,
|
||||
|
||||
bones = new BoneTransform[currentBoneCount],
|
||||
blendShapes = new float[currentBlendShapeCount]
|
||||
};
|
||||
|
||||
// Pack bone data based on mode
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full mode: pack all cached bones
|
||||
for (int i = 0; i < allBones.Count; i++)
|
||||
{
|
||||
Transform bone = allBones[i];
|
||||
data.bones[i] = new BoneTransform
|
||||
{
|
||||
position = bone.localPosition,
|
||||
rotation = bone.localRotation
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: pack priority bones
|
||||
for (int i = 0; i < priorityBones.Length; i++)
|
||||
{
|
||||
if (boneCache.TryGetValue(priorityBones[i], out Transform bone))
|
||||
{
|
||||
data.bones[i] = new BoneTransform
|
||||
{
|
||||
position = bone.localPosition,
|
||||
rotation = bone.localRotation
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default values for missing bones
|
||||
data.bones[i] = new BoneTransform
|
||||
{
|
||||
position = Vector3.zero,
|
||||
rotation = Quaternion.identity
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pack facial blend shape data based on mode
|
||||
if (includeFacialData)
|
||||
{
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full mode: collect all blend shapes from all meshes
|
||||
int blendShapeIndex = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
{
|
||||
if (mesh != null && mesh.sharedMesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
if (blendShapeIndex < data.blendShapes.Length)
|
||||
{
|
||||
data.blendShapes[blendShapeIndex] = mesh.GetBlendShapeWeight(i) / 100f; // Normalize to 0-1
|
||||
blendShapeIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: use head mesh only
|
||||
if (headMesh != null && headMesh.sharedMesh != null)
|
||||
{
|
||||
int blendShapeCount = Mathf.Min(maxBlendShapes, headMesh.sharedMesh.blendShapeCount);
|
||||
for (int i = 0; i < blendShapeCount; i++)
|
||||
{
|
||||
data.blendShapes[i] = headMesh.GetBlendShapeWeight(i) / 100f; // Normalize to 0-1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize to bytes
|
||||
byte[] packetData = SerializeCompactData(data);
|
||||
|
||||
// Send UDP packet
|
||||
udpClient.Send(packetData, packetData.Length, broadcastEndPoint);
|
||||
|
||||
if (logPacketSize && sequenceNumber % 30 == 0) // Log every 30 packets (about once per second)
|
||||
{
|
||||
string modeStr = fullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Sent {modeStr} packet #{sequenceNumber}, size: {packetData.Length} bytes, bones: {currentBoneCount}, blend shapes: {currentBlendShapeCount}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to broadcast avatar data: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
byte[] SerializeCompactData(CompactAvatarData data)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
// Header
|
||||
writer.Write(data.playerID);
|
||||
writer.Write(data.sequenceNumber);
|
||||
writer.Write(data.timestamp);
|
||||
writer.Write(data.isFullDataMode);
|
||||
|
||||
// Root transform
|
||||
WriteVector3(writer, data.rootPosition);
|
||||
WriteQuaternion(writer, data.rootRotation);
|
||||
WriteVector3(writer, data.rootScale);
|
||||
|
||||
// Variable-length bones
|
||||
writer.Write(data.bones.Length); // Write bone count first
|
||||
for (int i = 0; i < data.bones.Length; i++)
|
||||
{
|
||||
WriteVector3(writer, data.bones[i].position);
|
||||
WriteQuaternion(writer, data.bones[i].rotation);
|
||||
}
|
||||
|
||||
// Variable-length blend shapes
|
||||
writer.Write(data.blendShapes.Length); // Write blend shape count first
|
||||
for (int i = 0; i < data.blendShapes.Length; i++)
|
||||
{
|
||||
writer.Write(data.blendShapes[i]);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
void WriteVector3(BinaryWriter writer, Vector3 vector)
|
||||
{
|
||||
writer.Write(vector.x);
|
||||
writer.Write(vector.y);
|
||||
writer.Write(vector.z);
|
||||
}
|
||||
|
||||
void WriteQuaternion(BinaryWriter writer, Quaternion quaternion)
|
||||
{
|
||||
writer.Write(quaternion.x);
|
||||
writer.Write(quaternion.y);
|
||||
writer.Write(quaternion.z);
|
||||
writer.Write(quaternion.w);
|
||||
}
|
||||
|
||||
public void SetPlayerID(byte newPlayerID)
|
||||
{
|
||||
playerID = newPlayerID;
|
||||
}
|
||||
|
||||
public void SetUpdateRate(float newRate)
|
||||
{
|
||||
updateRate = Mathf.Clamp(newRate, 1f, 120f);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (udpClient != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void OnApplicationPause(bool pauseStatus)
|
||||
{
|
||||
enableBroadcast = !pauseStatus;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showDebugInfo) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 240));
|
||||
GUILayout.Label($"UDP Broadcaster Agent - Player {playerID}");
|
||||
GUILayout.Label($"Mode: {(fullDataMode ? "FULL DATA" : "OPTIMIZED")}");
|
||||
GUILayout.Label($"Status: {(enableBroadcast ? "Broadcasting" : "Stopped")}");
|
||||
GUILayout.Label($"Sequence: {sequenceNumber}");
|
||||
GUILayout.Label($"Update Rate: {updateRate:F1} Hz");
|
||||
GUILayout.Label($"Bones: {currentBoneCount}");
|
||||
GUILayout.Label($"Blend Shapes: {currentBlendShapeCount}");
|
||||
|
||||
if (fullDataMode)
|
||||
{
|
||||
int estimatedSize = CalculateFullModePacketSize();
|
||||
GUILayout.Label($"Est. Packet Size: {estimatedSize} bytes");
|
||||
if (estimatedSize > 1400)
|
||||
{
|
||||
GUI.color = Color.yellow;
|
||||
GUILayout.Label("⚠️ Large packet size!");
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button(enableBroadcast ? "Stop Broadcasting" : "Start Broadcasting"))
|
||||
{
|
||||
enableBroadcast = !enableBroadcast;
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68f69be5ef1c06c4ea9fcc9d9f59f1d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
726
Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs
Normal file
726
Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs
Normal file
@ -0,0 +1,726 @@
|
||||
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>
|
||||
/// UDPAvatarReceiverAgent - Adapted for CC_Base_ bone naming convention (Ready Player Me / Reallusion Character Creator)
|
||||
/// </summary>
|
||||
public class UDPAvatarReceiverAgent : MonoBehaviour
|
||||
{
|
||||
[Header("Network Configuration")]
|
||||
[SerializeField] private int listenPort = 8080;
|
||||
[SerializeField] private bool useGlobalNetworkConfig = true;
|
||||
[SerializeField] private NetworkConfig networkConfigAsset;
|
||||
[SerializeField] private bool enableReceiver = true;
|
||||
[SerializeField] private byte targetPlayerID = 5; // Which player to receive data from (0 = any)
|
||||
[SerializeField] private bool allowPortSharing = true; // For local testing with multiple components
|
||||
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform targetAvatarRoot;
|
||||
[SerializeField] private bool smoothTransitions = true;
|
||||
[SerializeField] private float transitionSpeed = 10f;
|
||||
|
||||
[Header("Data Selection - Core Bones (CC_Base_ naming)")]
|
||||
[SerializeField] private string[] priorityBones = {
|
||||
"CC_Base_Hip", "CC_Base_Pelvis", "CC_Base_Spine01", "CC_Base_Spine02", "CC_Base_Neck", "CC_Base_Head",
|
||||
"CC_Base_L_Clavicle", "CC_Base_L_Upperarm", "CC_Base_L_Forearm", "CC_Base_L_Hand",
|
||||
"CC_Base_R_Clavicle", "CC_Base_R_Upperarm", "CC_Base_R_Forearm", "CC_Base_R_Hand",
|
||||
"CC_Base_L_Thigh", "CC_Base_L_Calf", "CC_Base_L_Foot",
|
||||
"CC_Base_R_Thigh", "CC_Base_R_Calf", "CC_Base_R_Foot"
|
||||
};
|
||||
|
||||
[Header("Synchronization Options")]
|
||||
[SerializeField] private bool syncRootPosition = true;
|
||||
[SerializeField] private bool syncRootRotation = true;
|
||||
[SerializeField] private bool syncRootScale = false;
|
||||
[SerializeField] private bool syncFacialData = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool logReceivedPackets = false;
|
||||
|
||||
private UdpClient udpClient;
|
||||
private Thread udpListenerThread;
|
||||
private bool threadRunning = false;
|
||||
private Dictionary<string, Transform> boneCache;
|
||||
private List<Transform> allBones; // For full data mode
|
||||
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
|
||||
private SkinnedMeshRenderer headMesh;
|
||||
|
||||
// Latest received data
|
||||
private CompactAvatarData lastReceivedData;
|
||||
private bool hasNewData = false;
|
||||
private object dataLock = new object();
|
||||
|
||||
// Stats
|
||||
private uint lastSequenceNumber = 0;
|
||||
private int packetsReceived = 0;
|
||||
private int packetsDropped = 0;
|
||||
private float lastPacketTime = 0f;
|
||||
|
||||
// Avatar data structure - supports both optimized and full data modes (matches broadcaster)
|
||||
private struct CompactAvatarData
|
||||
{
|
||||
public byte playerID;
|
||||
public uint sequenceNumber;
|
||||
public uint timestamp;
|
||||
public bool isFullDataMode; // indicates if this is full or optimized data
|
||||
|
||||
public Vector3 rootPosition;
|
||||
public Quaternion rootRotation;
|
||||
public Vector3 rootScale;
|
||||
|
||||
public BoneTransform[] bones; // Variable-length
|
||||
public float[] blendShapes; // Variable-length
|
||||
}
|
||||
|
||||
private struct BoneTransform
|
||||
{
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (targetAvatarRoot == null)
|
||||
targetAvatarRoot = transform;
|
||||
|
||||
// Apply global config if enabled
|
||||
if (useGlobalNetworkConfig)
|
||||
{
|
||||
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
|
||||
if (cfg != null)
|
||||
{
|
||||
listenPort = cfg.port;
|
||||
}
|
||||
}
|
||||
|
||||
CacheAvatarComponents();
|
||||
|
||||
if (enableReceiver)
|
||||
{
|
||||
StartUDPListener();
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"UDP Avatar Receiver Agent initialized");
|
||||
Debug.Log($"Listening on port {listenPort}");
|
||||
Debug.Log($"Target Player ID: {(targetPlayerID == 0 ? "Any" : targetPlayerID.ToString())}");
|
||||
}
|
||||
}
|
||||
|
||||
void CacheAvatarComponents()
|
||||
{
|
||||
boneCache = new Dictionary<string, Transform>();
|
||||
allBones = new List<Transform>();
|
||||
allMeshes = new List<SkinnedMeshRenderer>();
|
||||
|
||||
Transform[] allChildren = targetAvatarRoot.GetComponentsInChildren<Transform>();
|
||||
|
||||
// Cache priority bones for optimized mode
|
||||
foreach (Transform child in allChildren)
|
||||
{
|
||||
foreach (string boneName in priorityBones)
|
||||
{
|
||||
if (child.name.Equals(boneName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!boneCache.ContainsKey(boneName))
|
||||
{
|
||||
boneCache[boneName] = child;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Cached priority bone: {boneName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache ALL bones for full data mode
|
||||
SkinnedMeshRenderer[] meshRenderers = targetAvatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
HashSet<Transform> uniqueBones = new HashSet<Transform>();
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in meshRenderers)
|
||||
{
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
uniqueBones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allBones.AddRange(uniqueBones);
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Cached {allBones.Count} total bones for full data mode");
|
||||
|
||||
// Cache facial animation data
|
||||
if (syncFacialData)
|
||||
{
|
||||
SkinnedMeshRenderer[] meshes = targetAvatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
|
||||
// Find head mesh for optimized mode
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.name.ToLower().Contains("head") || mesh.name.ToLower().Contains("face") || mesh.name.ToLower().Contains("wolf3d_head"))
|
||||
{
|
||||
headMesh = mesh;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Found head mesh: {mesh.name} with {mesh.sharedMesh.blendShapeCount} blend shapes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache all meshes with blend shapes for full data mode
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.sharedMesh != null && mesh.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
allMeshes.Add(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
int totalBlendShapes = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
totalBlendShapes += mesh.sharedMesh.blendShapeCount;
|
||||
Debug.Log($"Found {allMeshes.Count} meshes with {totalBlendShapes} total blend shapes for full data mode");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Receiver Agent ready: {boneCache.Count}/{priorityBones.Length} priority bones, {allBones.Count} total bones");
|
||||
}
|
||||
|
||||
void StartUDPListener()
|
||||
{
|
||||
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();
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"UDP Avatar Receiver Agent started on port {listenPort} (filtering binary avatar data only, Port sharing: {allowPortSharing})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
Debug.LogWarning($"Failed to start UDP listener with port sharing: {e.Message}");
|
||||
Debug.LogWarning("Trying with different port...");
|
||||
TryAlternativePort();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Failed to start UDP listener: {e.Message}");
|
||||
enableReceiver = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Avatar data should be binary and typically larger than JSON experiment messages
|
||||
if (data.Length < 20) // Avatar data should have at least header info
|
||||
return false;
|
||||
|
||||
// Check if it looks like text/JSON (experiment control message)
|
||||
// JSON messages will be valid UTF-8 text starting with '{'
|
||||
try
|
||||
{
|
||||
string text = Encoding.UTF8.GetString(data);
|
||||
if (text.TrimStart().StartsWith("{") && text.Contains("\"command\""))
|
||||
{
|
||||
// This is likely an experiment control message, not avatar data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If it fails to decode as UTF-8, it's likely binary avatar data
|
||||
}
|
||||
|
||||
// Additional check: avatar data should start with a reasonable playerID (0-255)
|
||||
// and have a structure that makes sense
|
||||
using (MemoryStream stream = new MemoryStream(data))
|
||||
using (BinaryReader reader = new BinaryReader(stream))
|
||||
{
|
||||
// Basic structure check - should be able to read at least the header
|
||||
if (data.Length >= 17) // byte + uint32 + uint32 + bool + 12 bytes for Vector3
|
||||
{
|
||||
byte playerID = reader.ReadByte();
|
||||
uint sequenceNumber = reader.ReadUInt32();
|
||||
uint timestamp = reader.ReadUInt32();
|
||||
bool isFullDataMode = reader.ReadBoolean();
|
||||
|
||||
// Basic sanity checks
|
||||
if (playerID <= 10 && sequenceNumber < uint.MaxValue / 2) // Reasonable values
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any parsing fails, assume it's not valid avatar data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CompactAvatarData DeserializeCompactData(byte[] data)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream(data))
|
||||
using (BinaryReader reader = new BinaryReader(stream))
|
||||
{
|
||||
CompactAvatarData result = new CompactAvatarData
|
||||
{
|
||||
// Header
|
||||
playerID = reader.ReadByte(),
|
||||
sequenceNumber = reader.ReadUInt32(),
|
||||
timestamp = reader.ReadUInt32(),
|
||||
isFullDataMode = reader.ReadBoolean(),
|
||||
|
||||
// Root transform
|
||||
rootPosition = ReadVector3(reader),
|
||||
rootRotation = ReadQuaternion(reader),
|
||||
rootScale = ReadVector3(reader)
|
||||
};
|
||||
|
||||
// Variable-length bones
|
||||
int boneCount = reader.ReadInt32();
|
||||
result.bones = new BoneTransform[boneCount];
|
||||
for (int i = 0; i < boneCount; i++)
|
||||
{
|
||||
result.bones[i] = new BoneTransform
|
||||
{
|
||||
position = ReadVector3(reader),
|
||||
rotation = ReadQuaternion(reader)
|
||||
};
|
||||
}
|
||||
|
||||
// Variable-length blend shapes
|
||||
int blendShapeCount = reader.ReadInt32();
|
||||
result.blendShapes = new float[blendShapeCount];
|
||||
for (int i = 0; i < blendShapeCount; i++)
|
||||
{
|
||||
result.blendShapes[i] = reader.ReadSingle();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 ReadVector3(BinaryReader reader)
|
||||
{
|
||||
return new Vector3(
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle()
|
||||
);
|
||||
}
|
||||
|
||||
Quaternion ReadQuaternion(BinaryReader reader)
|
||||
{
|
||||
return new Quaternion(
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle()
|
||||
);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!enableReceiver)
|
||||
return;
|
||||
|
||||
// Apply new data if available
|
||||
lock (dataLock)
|
||||
{
|
||||
if (hasNewData)
|
||||
{
|
||||
ApplyAvatarData(lastReceivedData);
|
||||
hasNewData = false;
|
||||
lastPacketTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyAvatarData(CompactAvatarData data)
|
||||
{
|
||||
// Apply root transform
|
||||
if (syncRootPosition)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.position = Vector3.Lerp(
|
||||
targetAvatarRoot.position,
|
||||
data.rootPosition,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.position = data.rootPosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRootRotation)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.rotation = Quaternion.Lerp(
|
||||
targetAvatarRoot.rotation,
|
||||
data.rootRotation,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.rotation = data.rootRotation;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRootScale)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.localScale = Vector3.Lerp(
|
||||
targetAvatarRoot.localScale,
|
||||
data.rootScale,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.localScale = data.rootScale;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply bone transforms based on data mode
|
||||
if (data.isFullDataMode)
|
||||
{
|
||||
// Full data mode: apply to all bones based on order
|
||||
for (int i = 0; i < Mathf.Min(data.bones.Length, allBones.Count); i++)
|
||||
{
|
||||
Transform bone = allBones[i];
|
||||
if (bone != null)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
bone.localPosition = Vector3.Lerp(
|
||||
bone.localPosition,
|
||||
data.bones[i].position,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
|
||||
bone.localRotation = Quaternion.Lerp(
|
||||
bone.localRotation,
|
||||
data.bones[i].rotation,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
bone.localPosition = data.bones[i].position;
|
||||
bone.localRotation = data.bones[i].rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: apply to priority bones
|
||||
for (int i = 0; i < Mathf.Min(data.bones.Length, priorityBones.Length); i++)
|
||||
{
|
||||
if (boneCache.TryGetValue(priorityBones[i], out Transform bone))
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
bone.localPosition = Vector3.Lerp(
|
||||
bone.localPosition,
|
||||
data.bones[i].position,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
|
||||
bone.localRotation = Quaternion.Lerp(
|
||||
bone.localRotation,
|
||||
data.bones[i].rotation,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
bone.localPosition = data.bones[i].position;
|
||||
bone.localRotation = data.bones[i].rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply facial blend shapes based on data mode
|
||||
if (syncFacialData)
|
||||
{
|
||||
if (data.isFullDataMode)
|
||||
{
|
||||
// Full data mode: apply all blend shapes to all meshes
|
||||
int blendShapeIndex = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
{
|
||||
if (mesh != null && mesh.sharedMesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
if (blendShapeIndex < data.blendShapes.Length)
|
||||
{
|
||||
float targetWeight = data.blendShapes[blendShapeIndex] * 100f; // Convert back to 0-100 range
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
float currentWeight = mesh.GetBlendShapeWeight(i);
|
||||
float newWeight = Mathf.Lerp(currentWeight, targetWeight, Time.deltaTime * transitionSpeed);
|
||||
mesh.SetBlendShapeWeight(i, newWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
mesh.SetBlendShapeWeight(i, targetWeight);
|
||||
}
|
||||
blendShapeIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: apply to head mesh only
|
||||
if (headMesh != null && headMesh.sharedMesh != null)
|
||||
{
|
||||
int blendShapeCount = Mathf.Min(data.blendShapes.Length, headMesh.sharedMesh.blendShapeCount);
|
||||
for (int i = 0; i < blendShapeCount; i++)
|
||||
{
|
||||
float targetWeight = data.blendShapes[i] * 100f; // Convert back to 0-100 range
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
float currentWeight = headMesh.GetBlendShapeWeight(i);
|
||||
float newWeight = Mathf.Lerp(currentWeight, targetWeight, Time.deltaTime * transitionSpeed);
|
||||
headMesh.SetBlendShapeWeight(i, newWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
headMesh.SetBlendShapeWeight(i, targetWeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTargetPlayerID(byte playerID)
|
||||
{
|
||||
targetPlayerID = playerID;
|
||||
}
|
||||
|
||||
public bool IsReceivingData()
|
||||
{
|
||||
return Time.time - lastPacketTime < 1f; // Consider "receiving" if we got data in the last second
|
||||
}
|
||||
|
||||
public float GetPacketLossRate()
|
||||
{
|
||||
int totalExpected = packetsReceived + packetsDropped;
|
||||
return totalExpected > 0 ? (float)packetsDropped / totalExpected : 0f;
|
||||
}
|
||||
|
||||
void StopUDPListener()
|
||||
{
|
||||
threadRunning = false;
|
||||
|
||||
if (udpClient != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient.Dispose();
|
||||
udpClient = null;
|
||||
}
|
||||
|
||||
if (udpListenerThread != null)
|
||||
{
|
||||
udpListenerThread.Join(1000); // Wait up to 1 second for thread to finish
|
||||
udpListenerThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopUDPListener();
|
||||
}
|
||||
|
||||
void OnApplicationPause(bool pauseStatus)
|
||||
{
|
||||
if (pauseStatus)
|
||||
{
|
||||
StopUDPListener();
|
||||
}
|
||||
else if (enableReceiver)
|
||||
{
|
||||
StartUDPListener();
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showDebugInfo) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(320, 10, 300, 260));
|
||||
GUILayout.Label($"UDP Avatar Receiver Agent (Binary Only)");
|
||||
GUILayout.Label($"Status: {(IsReceivingData() ? "Receiving" : "No Data")}");
|
||||
|
||||
// Show last received data mode
|
||||
if (hasNewData)
|
||||
{
|
||||
string modeStr = lastReceivedData.isFullDataMode ? "FULL DATA" : "OPTIMIZED";
|
||||
GUILayout.Label($"Last Data Mode: {modeStr}");
|
||||
GUILayout.Label($"Bones Received: {lastReceivedData.bones?.Length ?? 0}");
|
||||
GUILayout.Label($"Blend Shapes: {lastReceivedData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
|
||||
GUILayout.Label($"Listen Port: {GetActualListenPort()} (shared)");
|
||||
GUILayout.Label($"Target Player: {(targetPlayerID == 0 ? "Any" : targetPlayerID.ToString())}");
|
||||
GUILayout.Label($"Packets: {packetsReceived} received, {packetsDropped} dropped");
|
||||
GUILayout.Label($"Packet Loss: {GetPacketLossRate():P1}");
|
||||
GUILayout.Label($"Last Sequence: {lastSequenceNumber}");
|
||||
GUILayout.Label($"Cached: {boneCache.Count} priority, {allBones.Count} total bones");
|
||||
GUILayout.Label($"Port Sharing: {allowPortSharing}");
|
||||
|
||||
if (GUILayout.Button(enableReceiver ? "Stop Receiver" : "Start Receiver"))
|
||||
{
|
||||
enableReceiver = !enableReceiver;
|
||||
if (enableReceiver)
|
||||
StartUDPListener();
|
||||
else
|
||||
StopUDPListener();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
int GetActualListenPort()
|
||||
{
|
||||
if (udpClient?.Client?.LocalEndPoint != null)
|
||||
{
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
}
|
||||
return listenPort;
|
||||
}
|
||||
}
|
||||
|
||||
11
Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f33b7a715d62bb489bac0262f29f846
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user