removed legacy avatar synchronization scripts and server implementation, transitioning to a new UDP-based system for improved performance and efficiency

This commit is contained in:
tom.hempel
2025-10-21 18:01:33 +02:00
parent f96c76d095
commit 18f3abbd7d
35 changed files with 162 additions and 3963 deletions

View File

@ -1,319 +0,0 @@
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
public class AvatarDataDownloader : MonoBehaviour
{
[Header("Server Configuration")]
[SerializeField] private string serverHost = "127.0.0.1";
[SerializeField] private int serverPort = 8080;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[SerializeField] private string targetPlayer = "player1"; // Which player data to fetch
[Header("Download Configuration")]
[SerializeField] private bool autoDownload = true;
[SerializeField] private float downloadInterval = 0.1f; // How often to fetch data (in seconds)
[SerializeField] private string localFileName = ""; // Leave empty to use targetPlayer name
[Header("Debug")]
[SerializeField] private bool enableDebugMode = false;
[SerializeField] private bool showConnectionStatus = true;
private string syncFilesPath;
private string outputFilePath;
private Coroutine downloadCoroutine;
private bool isConnected = false;
private float lastSuccessfulDownload = 0f;
private int totalDownloads = 0;
private int successfulDownloads = 0;
// Connection status
public bool IsConnected => isConnected;
public string ServerUrl => $"http://{serverHost}:{serverPort}";
public float LastDownloadTime => lastSuccessfulDownload;
public float SuccessRate => totalDownloads > 0 ? (float)successfulDownloads / totalDownloads : 0f;
void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
serverHost = cfg.ipAddress;
serverPort = cfg.port;
}
}
// Set up file paths
syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
if (!Directory.Exists(syncFilesPath))
{
Directory.CreateDirectory(syncFilesPath);
}
// Determine output file name
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
outputFilePath = Path.Combine(syncFilesPath, fileName);
Debug.Log($"Avatar Data Downloader initialized");
Debug.Log($"Server: {ServerUrl}");
Debug.Log($"Target Player: {targetPlayer}");
Debug.Log($"Output File: {outputFilePath}");
// Start downloading if auto-download is enabled
if (autoDownload)
{
StartDownload();
}
// Test connection
StartCoroutine(TestConnection());
}
void OnValidate()
{
// Ensure valid values
if (downloadInterval < 0.01f)
downloadInterval = 0.01f;
if (serverPort < 1 || serverPort > 65535)
serverPort = 8080;
}
public void StartDownload()
{
if (downloadCoroutine == null)
{
downloadCoroutine = StartCoroutine(DownloadLoop());
Debug.Log("Avatar download started");
}
}
public void StopDownload()
{
if (downloadCoroutine != null)
{
StopCoroutine(downloadCoroutine);
downloadCoroutine = null;
Debug.Log("Avatar download stopped");
}
}
IEnumerator TestConnection()
{
string url = $"{ServerUrl}/status";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 5; // 5 second timeout for connection test
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
isConnected = true;
if (showConnectionStatus)
{
Debug.Log($"Successfully connected to avatar sync server at {ServerUrl}");
}
if (enableDebugMode)
{
Debug.Log($"Server status: {request.downloadHandler.text}");
}
}
else
{
isConnected = false;
if (showConnectionStatus)
{
Debug.LogWarning($"Failed to connect to avatar sync server at {ServerUrl}: {request.error}");
}
}
}
}
IEnumerator DownloadLoop()
{
while (true)
{
yield return StartCoroutine(FetchPlayerData());
yield return new WaitForSeconds(downloadInterval);
}
}
IEnumerator FetchPlayerData()
{
string url = $"{ServerUrl}/{targetPlayer}";
totalDownloads++;
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 10; // 10 second timeout
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// Try to validate JSON structure
bool isValidData = false;
float dataTimestamp = 0f;
try
{
var avatarData = JsonConvert.DeserializeObject<AvatarSyncData>(request.downloadHandler.text);
isValidData = avatarData != null;
dataTimestamp = avatarData?.timestamp ?? 0f;
}
catch (Exception e)
{
Debug.LogError($"Error parsing avatar data for {targetPlayer}: {e.Message}");
isValidData = false;
}
if (isValidData)
{
// Write to local file
File.WriteAllText(outputFilePath, request.downloadHandler.text);
successfulDownloads++;
lastSuccessfulDownload = Time.time;
isConnected = true;
if (enableDebugMode)
{
Debug.Log($"Successfully fetched and saved data for {targetPlayer}. Timestamp: {dataTimestamp}");
}
}
else
{
if (enableDebugMode)
{
Debug.LogWarning($"Received invalid avatar data for {targetPlayer}");
}
}
}
else
{
isConnected = false;
if (enableDebugMode)
{
Debug.LogWarning($"Failed to fetch data for {targetPlayer}: {request.error}");
}
}
}
}
// Public methods for runtime control
public void SetServerAddress(string host, int port)
{
serverHost = host;
serverPort = port;
// Test new connection
StartCoroutine(TestConnection());
}
public void SetTargetPlayer(string player)
{
targetPlayer = player;
// Update output file path
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
outputFilePath = Path.Combine(syncFilesPath, fileName);
}
public void ManualDownload()
{
if (gameObject.activeInHierarchy)
{
StartCoroutine(FetchPlayerData());
}
}
// Get current stats for UI display
public string GetDownloadStats()
{
return $"Connected: {isConnected}\n" +
$"Success Rate: {(SuccessRate * 100):F1}%\n" +
$"Total Downloads: {totalDownloads}\n" +
$"Last Download: {(Time.time - lastSuccessfulDownload):F1}s ago";
}
// Check if the output file exists and has recent data
public bool HasRecentData(float maxAgeSeconds = 5f)
{
if (!File.Exists(outputFilePath))
return false;
try
{
FileInfo fileInfo = new FileInfo(outputFilePath);
return (DateTime.Now - fileInfo.LastWriteTime).TotalSeconds < maxAgeSeconds;
}
catch
{
return false;
}
}
void OnDisable()
{
StopDownload();
}
void OnDestroy()
{
StopDownload();
}
// GUI for runtime debugging
void OnGUI()
{
if (!enableDebugMode)
return;
GUILayout.BeginArea(new Rect(320, 10, 300, 180));
GUILayout.BeginVertical("box");
GUILayout.Label($"Avatar Downloader - {targetPlayer}");
GUILayout.Label($"Server: {ServerUrl}");
GUILayout.Label($"Status: {(isConnected ? "Connected" : "Disconnected")}");
GUILayout.Label($"Success Rate: {(SuccessRate * 100):F1}%");
GUILayout.Label($"Last Download: {(Time.time - lastSuccessfulDownload):F1}s ago");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Manual Download"))
{
ManualDownload();
}
if (GUILayout.Button("Test Connection"))
{
StartCoroutine(TestConnection());
}
GUILayout.EndHorizontal();
if (downloadCoroutine == null)
{
if (GUILayout.Button("Start Auto Download"))
{
StartDownload();
}
}
else
{
if (GUILayout.Button("Stop Auto Download"))
{
StopDownload();
}
}
GUILayout.EndVertical();
GUILayout.EndArea();
}
}

View File

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

View File

@ -1,366 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Newtonsoft.Json;
public class AvatarDataReader : MonoBehaviour
{
[Header("Avatar Configuration")]
[SerializeField] private Transform targetAvatarRoot;
[SerializeField] private string fileName = "avatar_sync_data.json";
[SerializeField] private bool readEveryFrame = true;
[SerializeField] private float updateRate = 60f; // Updates per second
[SerializeField] private bool smoothTransitions = true;
[SerializeField] private float transitionSpeed = 10f;
[Header("Synchronization Options")]
[SerializeField] private bool syncWorldPosition = true; // Apply avatar world position
[SerializeField] private bool syncWorldRotation = true; // Apply avatar world rotation
[SerializeField] private bool syncLocalScale = false; // Apply avatar local scale
[Header("Debug")]
[SerializeField] private bool showDebugInfo = false;
[SerializeField] private bool enableDebugMode = false;
[SerializeField] private string debugBoneName = "LeftArm"; // Bone to monitor
private string filePath;
private float lastUpdateTime;
private Dictionary<string, Transform> boneLookup;
private Dictionary<string, SkinnedMeshRenderer> skinnedMeshLookup;
private AvatarSyncData lastSyncData;
private bool isInitialized = false;
private int successfulReads = 0;
private int appliedBones = 0;
private Vector3 lastAppliedWorldPosition;
void Start()
{
// Set up file path
string syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
filePath = Path.Combine(syncFilesPath, fileName);
// If targetAvatarRoot is not assigned, try to find it automatically
if (targetAvatarRoot == null)
{
targetAvatarRoot = transform;
}
// Cache target avatar components
CacheTargetAvatarComponents();
// Initialize debug tracking
if (enableDebugMode)
{
lastAppliedWorldPosition = targetAvatarRoot.position;
}
Debug.Log($"Avatar Data Reader initialized. Reading from: {filePath}");
Debug.Log($"World Position Sync: {syncWorldPosition}, World Rotation Sync: {syncWorldRotation}");
}
void CacheTargetAvatarComponents()
{
boneLookup = new Dictionary<string, Transform>();
skinnedMeshLookup = new Dictionary<string, SkinnedMeshRenderer>();
// Get all skinned mesh renderers
SkinnedMeshRenderer[] smrs = targetAvatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
// Cache bones from SkinnedMeshRenderers
foreach (SkinnedMeshRenderer smr in smrs)
{
// Cache this skinned mesh renderer
if (!skinnedMeshLookup.ContainsKey(smr.gameObject.name))
{
skinnedMeshLookup.Add(smr.gameObject.name, smr);
}
// Cache all bones from this SkinnedMeshRenderer
if (smr.bones != null)
{
foreach (Transform bone in smr.bones)
{
if (bone != null && !boneLookup.ContainsKey(bone.name))
{
boneLookup.Add(bone.name, bone);
}
}
}
}
isInitialized = true;
if (showDebugInfo)
{
Debug.Log($"Cached {boneLookup.Count} bones and {skinnedMeshLookup.Count} skinned mesh renderers");
}
// Debug mode: Print some bone names and check for the debug bone
if (enableDebugMode)
{
Debug.Log("Reader - Found bones:");
int count = 0;
foreach (var kvp in boneLookup)
{
if (count < 10)
{
Debug.Log($" Reader Bone {count}: {kvp.Key}");
count++;
}
if (kvp.Key.Contains(debugBoneName))
{
Debug.Log($"Found debug bone in reader: {kvp.Key}");
}
}
}
}
void Update()
{
if (!isInitialized || !File.Exists(filePath))
return;
if (readEveryFrame)
{
ReadAndApplyAvatarData();
}
else
{
// Rate-limited updates
if (Time.time - lastUpdateTime >= 1f / updateRate)
{
ReadAndApplyAvatarData();
lastUpdateTime = Time.time;
}
}
}
void ReadAndApplyAvatarData()
{
try
{
string json = File.ReadAllText(filePath);
AvatarSyncData syncData = JsonConvert.DeserializeObject<AvatarSyncData>(json);
if (syncData == null)
{
if (showDebugInfo)
Debug.LogWarning("Failed to deserialize avatar sync data");
return;
}
successfulReads++;
appliedBones = 0;
// Apply root transform data
ApplyRootTransformData(syncData.rootTransform);
// Apply bone data
ApplyBoneData(syncData.bones);
// Apply blendshape data
ApplyBlendShapeData(syncData.blendShapes);
if (enableDebugMode && successfulReads % 60 == 0) // Log every 60 reads (about once per second)
{
Debug.Log($"Reader stats: {successfulReads} successful reads, {appliedBones} bones applied in last read");
}
lastSyncData = syncData;
}
catch (Exception e)
{
if (showDebugInfo)
Debug.LogError($"Error reading avatar data: {e.Message}");
}
}
void ApplyRootTransformData(RootTransformData rootData)
{
if (rootData == null)
return;
if (syncWorldPosition && rootData.worldPosition != null)
{
Vector3 targetWorldPosition = rootData.worldPosition.ToVector3();
// Debug: Log world position changes
if (enableDebugMode && Vector3.Distance(targetWorldPosition, lastAppliedWorldPosition) > 0.01f)
{
Debug.Log($"Applying world position: {targetWorldPosition} (was: {lastAppliedWorldPosition})");
lastAppliedWorldPosition = targetWorldPosition;
}
if (smoothTransitions)
{
targetAvatarRoot.position = Vector3.Lerp(
targetAvatarRoot.position,
targetWorldPosition,
transitionSpeed * Time.deltaTime
);
}
else
{
targetAvatarRoot.position = targetWorldPosition;
}
}
if (syncWorldRotation && rootData.worldRotation != null)
{
Quaternion targetWorldRotation = rootData.worldRotation.ToQuaternion();
if (smoothTransitions)
{
targetAvatarRoot.rotation = Quaternion.Lerp(
targetAvatarRoot.rotation,
targetWorldRotation,
transitionSpeed * Time.deltaTime
);
}
else
{
targetAvatarRoot.rotation = targetWorldRotation;
}
}
if (syncLocalScale && rootData.localScale != null)
{
Vector3 targetLocalScale = rootData.localScale.ToVector3();
if (smoothTransitions)
{
targetAvatarRoot.localScale = Vector3.Lerp(
targetAvatarRoot.localScale,
targetLocalScale,
transitionSpeed * Time.deltaTime
);
}
else
{
targetAvatarRoot.localScale = targetLocalScale;
}
}
}
void ApplyBoneData(List<BoneData> boneDataList)
{
if (boneDataList == null)
return;
foreach (BoneData boneData in boneDataList)
{
if (boneLookup.TryGetValue(boneData.boneName, out Transform targetBone))
{
// Convert serializable types back to Unity types
Vector3 targetPosition = boneData.position.ToVector3();
Quaternion targetRotation = boneData.rotation.ToQuaternion();
Vector3 targetScale = boneData.scale.ToVector3();
// Debug mode: Log changes to the monitored bone
if (enableDebugMode && boneData.boneName.Contains(debugBoneName))
{
Debug.Log($"Applying to bone {boneData.boneName}: Pos={targetPosition}, Rot={targetRotation}");
}
if (smoothTransitions)
{
// Smooth interpolation
targetBone.localPosition = Vector3.Lerp(
targetBone.localPosition,
targetPosition,
transitionSpeed * Time.deltaTime
);
targetBone.localRotation = Quaternion.Lerp(
targetBone.localRotation,
targetRotation,
transitionSpeed * Time.deltaTime
);
targetBone.localScale = Vector3.Lerp(
targetBone.localScale,
targetScale,
transitionSpeed * Time.deltaTime
);
}
else
{
// Direct application
targetBone.localPosition = targetPosition;
targetBone.localRotation = targetRotation;
targetBone.localScale = targetScale;
}
appliedBones++;
}
else if (showDebugInfo)
{
Debug.LogWarning($"Bone not found: {boneData.boneName}");
}
}
}
void ApplyBlendShapeData(List<BlendShapeData> blendShapeDataList)
{
if (blendShapeDataList == null)
return;
foreach (BlendShapeData blendShapeData in blendShapeDataList)
{
if (skinnedMeshLookup.TryGetValue(blendShapeData.meshName, out SkinnedMeshRenderer targetSmr))
{
if (targetSmr.sharedMesh != null && blendShapeData.weights != null)
{
int maxBlendShapes = Mathf.Min(targetSmr.sharedMesh.blendShapeCount, blendShapeData.weights.Length);
for (int i = 0; i < maxBlendShapes; i++)
{
if (smoothTransitions)
{
float currentWeight = targetSmr.GetBlendShapeWeight(i);
float targetWeight = blendShapeData.weights[i];
float newWeight = Mathf.Lerp(currentWeight, targetWeight, transitionSpeed * Time.deltaTime);
targetSmr.SetBlendShapeWeight(i, newWeight);
}
else
{
targetSmr.SetBlendShapeWeight(i, blendShapeData.weights[i]);
}
}
}
}
else if (showDebugInfo)
{
Debug.LogWarning($"SkinnedMeshRenderer not found: {blendShapeData.meshName}");
}
}
}
public void SetTargetAvatar(Transform newTargetRoot)
{
targetAvatarRoot = newTargetRoot;
if (isInitialized)
{
CacheTargetAvatarComponents();
}
}
public void SetFileName(string newFileName)
{
fileName = newFileName;
string syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
filePath = Path.Combine(syncFilesPath, fileName);
}
public bool IsDataAvailable()
{
return File.Exists(filePath);
}
public float GetLastSyncTimestamp()
{
return lastSyncData?.timestamp ?? 0f;
}
}

View File

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

View File

@ -1,301 +0,0 @@
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class AvatarDataUploader : MonoBehaviour
{
[Header("Server Configuration")]
[SerializeField] private string serverHost = "127.0.0.1";
[SerializeField] private int serverPort = 8080;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[SerializeField] private string uploadAsPlayer = "player1"; // Upload as which player
[Header("Upload Configuration")]
[SerializeField] private bool autoUpload = true;
[SerializeField] private float uploadInterval = 0.1f; // How often to upload data (in seconds)
[SerializeField] private string localDataFile = "avatar_sync_data.json"; // Local file to upload
[Header("Debug")]
[SerializeField] private bool enableDebugMode = false;
[SerializeField] private bool showConnectionStatus = true;
private string syncFilesPath;
private Coroutine uploadCoroutine;
private bool isConnected = false;
private float lastSuccessfulUpload = 0f;
private int totalUploads = 0;
private int successfulUploads = 0;
// Connection status
public bool IsConnected => isConnected;
public string ServerUrl => $"http://{serverHost}:{serverPort}";
public float LastUploadTime => lastSuccessfulUpload;
public float SuccessRate => totalUploads > 0 ? (float)successfulUploads / totalUploads : 0f;
void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
serverHost = cfg.ipAddress;
serverPort = cfg.port;
}
}
// Set up file paths
syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
if (!Directory.Exists(syncFilesPath))
{
Directory.CreateDirectory(syncFilesPath);
}
Debug.Log($"Avatar Data Uploader initialized");
Debug.Log($"Server: {ServerUrl}");
Debug.Log($"Upload As Player: {uploadAsPlayer}");
Debug.Log($"Local Data File: {localDataFile}");
// Start uploading if auto-upload is enabled
if (autoUpload)
{
StartUpload();
}
// Test connection
StartCoroutine(TestConnection());
}
void OnValidate()
{
// Ensure valid values
if (uploadInterval < 0.01f)
uploadInterval = 0.01f;
if (serverPort < 1 || serverPort > 65535)
serverPort = 8080;
}
public void StartUpload()
{
if (uploadCoroutine == null)
{
uploadCoroutine = StartCoroutine(UploadLoop());
Debug.Log("Avatar upload started");
}
}
public void StopUpload()
{
if (uploadCoroutine != null)
{
StopCoroutine(uploadCoroutine);
uploadCoroutine = null;
Debug.Log("Avatar upload stopped");
}
}
IEnumerator TestConnection()
{
string url = $"{ServerUrl}/status";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 5; // 5 second timeout for connection test
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
isConnected = true;
if (showConnectionStatus)
{
Debug.Log($"Successfully connected to avatar sync server at {ServerUrl}");
}
if (enableDebugMode)
{
Debug.Log($"Server status: {request.downloadHandler.text}");
}
}
else
{
isConnected = false;
if (showConnectionStatus)
{
Debug.LogWarning($"Failed to connect to avatar sync server at {ServerUrl}: {request.error}");
}
}
}
}
IEnumerator UploadLoop()
{
while (true)
{
yield return StartCoroutine(UploadLocalData());
yield return new WaitForSeconds(uploadInterval);
}
}
IEnumerator UploadLocalData()
{
string localFilePath = Path.Combine(syncFilesPath, localDataFile);
if (!File.Exists(localFilePath))
{
if (enableDebugMode)
{
Debug.LogWarning($"Local data file not found: {localFilePath}");
}
yield break;
}
string jsonData = null;
// Read file outside of try block with yield
try
{
jsonData = File.ReadAllText(localFilePath);
}
catch (Exception e)
{
Debug.LogError($"Error reading local data file: {e.Message}");
yield break;
}
if (string.IsNullOrEmpty(jsonData))
{
if (enableDebugMode)
{
Debug.LogWarning("Local avatar data is empty, skipping upload");
}
yield break;
}
// Upload to server
string url = $"{ServerUrl}/{uploadAsPlayer}";
totalUploads++;
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonData);
request.uploadHandler = new UploadHandlerRaw(jsonBytes);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.timeout = 10;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
successfulUploads++;
lastSuccessfulUpload = Time.time;
isConnected = true;
if (enableDebugMode)
{
Debug.Log($"Successfully uploaded data as {uploadAsPlayer}");
}
}
else
{
isConnected = false;
if (enableDebugMode)
{
Debug.LogWarning($"Failed to upload data as {uploadAsPlayer}: {request.error}");
}
}
}
}
// Public methods for runtime control
public void SetServerAddress(string host, int port)
{
serverHost = host;
serverPort = port;
// Test new connection
StartCoroutine(TestConnection());
}
public void SetUploadPlayer(string player)
{
uploadAsPlayer = player;
}
public void ManualUpload()
{
if (gameObject.activeInHierarchy)
{
StartCoroutine(UploadLocalData());
}
}
// Get current stats for UI display
public string GetUploadStats()
{
return $"Connected: {isConnected}\n" +
$"Success Rate: {(SuccessRate * 100):F1}%\n" +
$"Total Uploads: {totalUploads}\n" +
$"Last Upload: {(Time.time - lastSuccessfulUpload):F1}s ago";
}
void OnDisable()
{
StopUpload();
}
void OnDestroy()
{
StopUpload();
}
// GUI for runtime debugging
void OnGUI()
{
if (!enableDebugMode)
return;
GUILayout.BeginArea(new Rect(10, 10, 300, 180));
GUILayout.BeginVertical("box");
GUILayout.Label($"Avatar Uploader - {uploadAsPlayer}");
GUILayout.Label($"Server: {ServerUrl}");
GUILayout.Label($"Status: {(isConnected ? "Connected" : "Disconnected")}");
GUILayout.Label($"Success Rate: {(SuccessRate * 100):F1}%");
GUILayout.Label($"Last Upload: {(Time.time - lastSuccessfulUpload):F1}s ago");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Manual Upload"))
{
ManualUpload();
}
if (GUILayout.Button("Test Connection"))
{
StartCoroutine(TestConnection());
}
GUILayout.EndHorizontal();
if (uploadCoroutine == null)
{
if (GUILayout.Button("Start Auto Upload"))
{
StartUpload();
}
}
else
{
if (GUILayout.Button("Stop Auto Upload"))
{
StopUpload();
}
}
GUILayout.EndVertical();
GUILayout.EndArea();
}
}

View File

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

View File

@ -1,331 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Newtonsoft.Json;
[System.Serializable]
public class SerializableVector3
{
public float x, y, z;
public SerializableVector3() { }
public SerializableVector3(Vector3 vector)
{
x = vector.x;
y = vector.y;
z = vector.z;
}
public Vector3 ToVector3()
{
return new Vector3(x, y, z);
}
}
[System.Serializable]
public class SerializableQuaternion
{
public float x, y, z, w;
public SerializableQuaternion() { }
public SerializableQuaternion(Quaternion quaternion)
{
x = quaternion.x;
y = quaternion.y;
z = quaternion.z;
w = quaternion.w;
}
public Quaternion ToQuaternion()
{
return new Quaternion(x, y, z, w);
}
}
[System.Serializable]
public class RootTransformData
{
public SerializableVector3 worldPosition;
public SerializableQuaternion worldRotation;
public SerializableVector3 localScale;
}
[System.Serializable]
public class BoneData
{
public string boneName; // e.g. "LeftArm", "RightHand"
public SerializableVector3 position;
public SerializableQuaternion rotation;
public SerializableVector3 scale;
}
[System.Serializable]
public class BlendShapeData
{
public string meshName;
public float[] weights;
}
[System.Serializable]
public class AvatarSyncData
{
public RootTransformData rootTransform;
public List<BoneData> bones;
public List<BlendShapeData> blendShapes;
public float timestamp;
}
public class AvatarDataWriter : MonoBehaviour
{
[Header("Avatar Configuration")]
[SerializeField] private Transform avatarRoot;
[SerializeField] private string fileName = "avatar_sync_data.json";
[SerializeField] private bool writeEveryFrame = true;
[SerializeField] private float updateRate = 60f; // Updates per second
[Header("Synchronization Options")]
[SerializeField] private bool syncWorldPosition = true; // Sync avatar world position
[SerializeField] private bool syncWorldRotation = true; // Sync avatar world rotation
[SerializeField] private bool syncLocalScale = false; // Sync avatar local scale
[Header("Debug")]
[SerializeField] private bool enableDebugMode = false;
[SerializeField] private string debugBoneName = "LeftArm"; // Bone to monitor
private string filePath;
private float lastUpdateTime;
private List<Transform> allBones;
private List<SkinnedMeshRenderer> skinnedMeshRenderers;
private Vector3 lastDebugPosition;
private Quaternion lastDebugRotation;
private Vector3 lastRootWorldPosition;
void Start()
{
// Create Sync-Files directory if it doesn't exist
string syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
if (!Directory.Exists(syncFilesPath))
{
Directory.CreateDirectory(syncFilesPath);
}
filePath = Path.Combine(syncFilesPath, fileName);
// If avatarRoot is not assigned, try to find it automatically
if (avatarRoot == null)
{
avatarRoot = transform;
}
// Cache all bones and skinned mesh renderers
CacheAvatarComponents();
// Initialize debug tracking
if (enableDebugMode)
{
Transform debugBone = FindBoneByName(debugBoneName);
if (debugBone != null)
{
lastDebugPosition = debugBone.localPosition;
lastDebugRotation = debugBone.localRotation;
Debug.Log($"Debug mode enabled. Monitoring bone: {debugBoneName}");
}
else
{
Debug.LogWarning($"Debug bone '{debugBoneName}' not found!");
}
lastRootWorldPosition = avatarRoot.position;
}
Debug.Log($"Avatar Data Writer initialized. Writing to: {filePath}");
Debug.Log($"World Position Sync: {syncWorldPosition}, World Rotation Sync: {syncWorldRotation}");
}
void CacheAvatarComponents()
{
allBones = new List<Transform>();
skinnedMeshRenderers = new List<SkinnedMeshRenderer>();
// Get all skinned mesh renderers
skinnedMeshRenderers.AddRange(avatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>());
// Get all unique bones from all SkinnedMeshRenderers
HashSet<Transform> uniqueBones = new HashSet<Transform>();
foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers)
{
if (smr.bones != null)
{
foreach (Transform bone in smr.bones)
{
if (bone != null)
{
uniqueBones.Add(bone);
}
}
}
}
allBones.AddRange(uniqueBones);
Debug.Log($"Cached {allBones.Count} bones and {skinnedMeshRenderers.Count} skinned mesh renderers");
// Debug: Print some bone names to help with troubleshooting
if (enableDebugMode)
{
Debug.Log("Found bones:");
for (int i = 0; i < Mathf.Min(10, allBones.Count); i++)
{
Debug.Log($" Bone {i}: {allBones[i].name}");
}
}
}
Transform FindBoneByName(string name)
{
foreach (Transform bone in allBones)
{
if (bone.name.Contains(name))
{
return bone;
}
}
return null;
}
void Update()
{
// Debug mode: Check if the monitored bone has changed
if (enableDebugMode)
{
Transform debugBone = FindBoneByName(debugBoneName);
if (debugBone != null)
{
if (Vector3.Distance(debugBone.localPosition, lastDebugPosition) > 0.001f ||
Quaternion.Angle(debugBone.localRotation, lastDebugRotation) > 0.1f)
{
Debug.Log($"Bone {debugBoneName} changed! Pos: {debugBone.localPosition}, Rot: {debugBone.localRotation}");
lastDebugPosition = debugBone.localPosition;
lastDebugRotation = debugBone.localRotation;
}
}
// Debug: Check if avatar root world position has changed
if (Vector3.Distance(avatarRoot.position, lastRootWorldPosition) > 0.01f)
{
Debug.Log($"Avatar root world position changed! From: {lastRootWorldPosition} To: {avatarRoot.position}");
lastRootWorldPosition = avatarRoot.position;
}
}
if (writeEveryFrame)
{
WriteAvatarData();
}
else
{
// Rate-limited updates
if (Time.time - lastUpdateTime >= 1f / updateRate)
{
WriteAvatarData();
lastUpdateTime = Time.time;
}
}
}
void WriteAvatarData()
{
try
{
AvatarSyncData syncData = new AvatarSyncData
{
rootTransform = new RootTransformData(),
bones = new List<BoneData>(),
blendShapes = new List<BlendShapeData>(),
timestamp = Time.time
};
// Capture root transform data
if (syncWorldPosition)
{
syncData.rootTransform.worldPosition = new SerializableVector3(avatarRoot.position);
}
else
{
syncData.rootTransform.worldPosition = new SerializableVector3(Vector3.zero);
}
if (syncWorldRotation)
{
syncData.rootTransform.worldRotation = new SerializableQuaternion(avatarRoot.rotation);
}
else
{
syncData.rootTransform.worldRotation = new SerializableQuaternion(Quaternion.identity);
}
if (syncLocalScale)
{
syncData.rootTransform.localScale = new SerializableVector3(avatarRoot.localScale);
}
else
{
syncData.rootTransform.localScale = new SerializableVector3(Vector3.one);
}
// Capture bone data
foreach (Transform bone in allBones)
{
BoneData boneData = new BoneData
{
boneName = bone.name,
position = new SerializableVector3(bone.localPosition),
rotation = new SerializableQuaternion(bone.localRotation),
scale = new SerializableVector3(bone.localScale)
};
syncData.bones.Add(boneData);
}
// Capture blendshape data
foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers)
{
if (smr.sharedMesh != null && smr.sharedMesh.blendShapeCount > 0)
{
float[] weights = new float[smr.sharedMesh.blendShapeCount];
for (int i = 0; i < weights.Length; i++)
{
weights[i] = smr.GetBlendShapeWeight(i);
}
BlendShapeData blendShapeData = new BlendShapeData
{
meshName = smr.gameObject.name, // Use GameObject name instead of path
weights = weights
};
syncData.blendShapes.Add(blendShapeData);
}
}
// Convert to JSON and write to file
string json = JsonConvert.SerializeObject(syncData, Formatting.Indented);
File.WriteAllText(filePath, json);
}
catch (Exception e)
{
Debug.LogError($"Error writing avatar data: {e.Message}");
}
}
void OnDisable()
{
// Write final data when component is disabled
if (allBones != null && allBones.Count > 0)
{
WriteAvatarData();
}
}
}

View File

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

View File

@ -1,390 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
public class AvatarSyncClient : MonoBehaviour
{
[Header("Server Configuration")]
[SerializeField] private string serverHost = "127.0.0.1";
[SerializeField] private int serverPort = 8080;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[SerializeField] private string targetPlayer = "player1"; // Which player data to fetch
[Header("Sync Configuration")]
[SerializeField] private bool autoSync = true;
[SerializeField] private float syncInterval = 0.1f; // How often to fetch data (in seconds)
[SerializeField] private string localFileName = ""; // Leave empty to use targetPlayer name
[Header("Upload Configuration")]
[SerializeField] private bool uploadLocalData = false; // Upload this client's data to server
[SerializeField] private string localDataFile = "avatar_sync_data.json"; // Local file to upload
[SerializeField] private string uploadAsPlayer = "player2"; // Upload as which player
[Header("Debug")]
[SerializeField] private bool enableDebugMode = false;
[SerializeField] private bool showConnectionStatus = true;
private string syncFilesPath;
private string outputFilePath;
private Coroutine syncCoroutine;
private bool isConnected = false;
private float lastSuccessfulSync = 0f;
private int totalRequests = 0;
private int successfulRequests = 0;
// Connection status
public bool IsConnected => isConnected;
public string ServerUrl => $"http://{serverHost}:{serverPort}";
public float LastSyncTime => lastSuccessfulSync;
public float SuccessRate => totalRequests > 0 ? (float)successfulRequests / totalRequests : 0f;
void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
serverHost = cfg.ipAddress;
serverPort = cfg.port;
}
}
// Set up file paths
syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
if (!Directory.Exists(syncFilesPath))
{
Directory.CreateDirectory(syncFilesPath);
}
// Determine output file name
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
outputFilePath = Path.Combine(syncFilesPath, fileName);
Debug.Log($"Avatar Sync Client initialized");
Debug.Log($"Server: {ServerUrl}");
Debug.Log($"Target Player: {targetPlayer}");
Debug.Log($"Output File: {outputFilePath}");
Debug.Log($"Upload Mode: {(uploadLocalData ? $"Yes (as {uploadAsPlayer})" : "No")}");
// Start syncing if auto-sync is enabled
if (autoSync)
{
StartSync();
}
// Test connection
StartCoroutine(TestConnection());
}
void OnValidate()
{
// Ensure valid values
if (syncInterval < 0.01f)
syncInterval = 0.01f;
if (serverPort < 1 || serverPort > 65535)
serverPort = 8080;
}
public void StartSync()
{
if (syncCoroutine == null)
{
syncCoroutine = StartCoroutine(SyncLoop());
Debug.Log("Avatar sync started");
}
}
public void StopSync()
{
if (syncCoroutine != null)
{
StopCoroutine(syncCoroutine);
syncCoroutine = null;
Debug.Log("Avatar sync stopped");
}
}
IEnumerator TestConnection()
{
string url = $"{ServerUrl}/status";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 5; // 5 second timeout for connection test
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
isConnected = true;
if (showConnectionStatus)
{
Debug.Log($"Successfully connected to avatar sync server at {ServerUrl}");
}
// Try to parse server status
try
{
var status = JsonConvert.DeserializeObject<Dictionary<string, object>>(request.downloadHandler.text);
if (enableDebugMode)
{
Debug.Log($"Server status: {request.downloadHandler.text}");
}
}
catch (Exception e)
{
if (enableDebugMode)
{
Debug.LogWarning($"Could not parse server status: {e.Message}");
}
}
}
else
{
isConnected = false;
if (showConnectionStatus)
{
Debug.LogWarning($"Failed to connect to avatar sync server at {ServerUrl}: {request.error}");
}
}
}
}
IEnumerator SyncLoop()
{
while (true)
{
// Upload local data if enabled
if (uploadLocalData)
{
yield return StartCoroutine(UploadLocalData());
}
// Fetch remote player data
yield return StartCoroutine(FetchPlayerData());
yield return new WaitForSeconds(syncInterval);
}
}
IEnumerator FetchPlayerData()
{
string url = $"{ServerUrl}/{targetPlayer}";
totalRequests++;
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 10; // 10 second timeout
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
try
{
// Validate JSON
var avatarData = JsonConvert.DeserializeObject<AvatarSyncData>(request.downloadHandler.text);
if (avatarData != null)
{
// Write to local file
File.WriteAllText(outputFilePath, request.downloadHandler.text);
successfulRequests++;
lastSuccessfulSync = Time.time;
isConnected = true;
if (enableDebugMode)
{
Debug.Log($"Successfully fetched and saved data for {targetPlayer}. Timestamp: {avatarData.timestamp}");
}
}
else
{
if (enableDebugMode)
{
Debug.LogWarning($"Received null avatar data for {targetPlayer}");
}
}
}
catch (Exception e)
{
Debug.LogError($"Error processing avatar data for {targetPlayer}: {e.Message}");
}
}
else
{
isConnected = false;
if (enableDebugMode)
{
Debug.LogWarning($"Failed to fetch data for {targetPlayer}: {request.error}");
}
}
}
}
IEnumerator UploadLocalData()
{
string localFilePath = Path.Combine(syncFilesPath, localDataFile);
if (!File.Exists(localFilePath))
{
if (enableDebugMode)
{
Debug.LogWarning($"Local data file not found: {localFilePath}");
}
yield break;
}
string jsonData = null;
AvatarSyncData avatarData = null;
// Read and validate file outside of try block with yield
try
{
jsonData = File.ReadAllText(localFilePath);
avatarData = JsonConvert.DeserializeObject<AvatarSyncData>(jsonData);
}
catch (Exception e)
{
Debug.LogError($"Error reading/parsing local data file: {e.Message}");
yield break;
}
if (avatarData == null)
{
if (enableDebugMode)
{
Debug.LogWarning("Local avatar data is invalid, skipping upload");
}
yield break;
}
// Network request without try-catch since we can't yield in try-catch
string url = $"{ServerUrl}/{uploadAsPlayer}";
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonData);
request.uploadHandler = new UploadHandlerRaw(jsonBytes);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.timeout = 10;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
if (enableDebugMode)
{
Debug.Log($"Successfully uploaded data as {uploadAsPlayer}");
}
}
else
{
if (enableDebugMode)
{
Debug.LogWarning($"Failed to upload data as {uploadAsPlayer}: {request.error}");
}
}
}
}
// Public methods for runtime control
public void SetServerAddress(string host, int port)
{
serverHost = host;
serverPort = port;
// Test new connection
StartCoroutine(TestConnection());
}
public void SetTargetPlayer(string player)
{
targetPlayer = player;
// Update output file path
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
outputFilePath = Path.Combine(syncFilesPath, fileName);
}
public void ManualSync()
{
if (gameObject.activeInHierarchy)
{
StartCoroutine(FetchPlayerData());
}
}
// Get current stats for UI display
public string GetConnectionStats()
{
return $"Connected: {isConnected}\n" +
$"Success Rate: {(SuccessRate * 100):F1}%\n" +
$"Total Requests: {totalRequests}\n" +
$"Last Sync: {(Time.time - lastSuccessfulSync):F1}s ago";
}
void OnDisable()
{
StopSync();
}
void OnDestroy()
{
StopSync();
}
// GUI for runtime debugging
void OnGUI()
{
if (!enableDebugMode)
return;
GUILayout.BeginArea(new Rect(10, 10, 300, 200));
GUILayout.BeginVertical("box");
GUILayout.Label($"Avatar Sync Client - {targetPlayer}");
GUILayout.Label($"Server: {ServerUrl}");
GUILayout.Label($"Status: {(isConnected ? "Connected" : "Disconnected")}");
GUILayout.Label($"Success Rate: {(SuccessRate * 100):F1}%");
GUILayout.Label($"Last Sync: {(Time.time - lastSuccessfulSync):F1}s ago");
GUILayout.BeginHorizontal();
if (GUILayout.Button("Manual Sync"))
{
ManualSync();
}
if (GUILayout.Button("Test Connection"))
{
StartCoroutine(TestConnection());
}
GUILayout.EndHorizontal();
if (syncCoroutine == null)
{
if (GUILayout.Button("Start Auto Sync"))
{
StartSync();
}
}
else
{
if (GUILayout.Button("Stop Auto Sync"))
{
StopSync();
}
}
GUILayout.EndVertical();
GUILayout.EndArea();
}
}

View File

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

View File

@ -1,611 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using Newtonsoft.Json;
/// <summary>
/// Utility script to compare the old JSON system vs new UDP binary system
/// Attach this to any GameObject and check the console for size comparisons
/// </summary>
public class AvatarSyncComparison : MonoBehaviour
{
[Header("Comparison Settings")]
[SerializeField] private bool runComparisonOnStart = true;
[SerializeField] private bool showDetailedBreakdown = true;
[SerializeField] private bool saveToFile = true;
[SerializeField] private string outputFileName = "avatar_sync_comparison.txt";
[Header("Test Data - Avatar Reference")]
[SerializeField] private Transform testAvatar;
[SerializeField] private UDPAvatarBroadcaster udpBroadcaster; // Get actual settings
[SerializeField] private UDPAvatarReceiver udpReceiver; // Get actual settings
[Header("System Parameters")]
[SerializeField] private float updateRate = 30f; // Hz - configurable refresh rate
[SerializeField] private int oldSystemBoneCount = 80; // Calculated from avatar if available
[SerializeField] private int oldSystemMeshCount = 4; // Calculated from avatar if available
[SerializeField] private int oldSystemBlendShapesPerMesh = 52; // Calculated from avatar if available
[Header("Code Complexity (Lines of Code)")]
[SerializeField] private int oldSystemTotalLines = 2300; // Can be calculated by scanning files
[SerializeField] private int newSystemTotalLines = 800; // Can be calculated by scanning files
void Start()
{
if (runComparisonOnStart)
{
// Add 5 second delay to allow UDPAvatarBroadcaster to fully initialize
Invoke(nameof(DelayedStart), 5f);
}
}
void DelayedStart()
{
// Auto-calculate values from avatar if available
CalculateAvatarParameters();
RunComparison();
}
void CalculateAvatarParameters()
{
// Get UDPAvatarBroadcaster from same GameObject if not assigned
if (udpBroadcaster == null)
{
udpBroadcaster = GetComponent<UDPAvatarBroadcaster>();
}
// Get actual values from UDP broadcaster
if (udpBroadcaster != null)
{
updateRate = GetUDPBroadcasterUpdateRate();
Debug.Log($"Got actual update rate from UDPAvatarBroadcaster: {updateRate} Hz");
// Use the avatar from the broadcaster if not set
if (testAvatar == null)
{
testAvatar = GetUDPBroadcasterAvatarRoot();
}
}
if (testAvatar != null)
{
// Calculate actual bone count from avatar
SkinnedMeshRenderer[] meshRenderers = testAvatar.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);
}
}
}
}
if (uniqueBones.Count > 0)
{
oldSystemBoneCount = uniqueBones.Count;
}
// Calculate actual mesh and blend shape counts
if (meshRenderers.Length > 0)
{
oldSystemMeshCount = meshRenderers.Length;
// Calculate average blend shapes per mesh
int totalBlendShapes = 0;
int meshesWithBlendShapes = 0;
foreach (var mesh in meshRenderers)
{
if (mesh.sharedMesh != null && mesh.sharedMesh.blendShapeCount > 0)
{
totalBlendShapes += mesh.sharedMesh.blendShapeCount;
meshesWithBlendShapes++;
}
}
if (meshesWithBlendShapes > 0)
{
oldSystemBlendShapesPerMesh = totalBlendShapes / meshesWithBlendShapes;
}
}
}
Debug.Log($"Calculated Parameters: {oldSystemBoneCount} bones, {oldSystemMeshCount} meshes, {oldSystemBlendShapesPerMesh} blend shapes/mesh, {updateRate} Hz");
}
float GetUDPBroadcasterUpdateRate()
{
if (udpBroadcaster == null) return updateRate;
// Use reflection to get the private updateRate field
var field = typeof(UDPAvatarBroadcaster).GetField("updateRate",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field != null)
{
return (float)field.GetValue(udpBroadcaster);
}
return updateRate; // Fallback to current value
}
Transform GetUDPBroadcasterAvatarRoot()
{
if (udpBroadcaster == null) return null;
// Use reflection to get the private avatarRoot field
var field = typeof(UDPAvatarBroadcaster).GetField("avatarRoot",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field != null)
{
return (Transform)field.GetValue(udpBroadcaster);
}
return null;
}
bool GetUDPBroadcasterFullDataMode()
{
if (udpBroadcaster == null) return false;
// Use reflection to get the private fullDataMode field
var field = typeof(UDPAvatarBroadcaster).GetField("fullDataMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field != null)
{
return (bool)field.GetValue(udpBroadcaster);
}
return false;
}
int GetUDPBroadcasterCurrentBoneCount()
{
if (udpBroadcaster == null) return 20;
// Use reflection to get the private currentBoneCount field
var field = typeof(UDPAvatarBroadcaster).GetField("currentBoneCount",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field != null)
{
return (int)field.GetValue(udpBroadcaster);
}
return 20; // Default optimized mode
}
int GetUDPBroadcasterCurrentBlendShapeCount()
{
if (udpBroadcaster == null) return 10;
// Use reflection to get the private currentBlendShapeCount field
var field = typeof(UDPAvatarBroadcaster).GetField("currentBlendShapeCount",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (field != null)
{
return (int)field.GetValue(udpBroadcaster);
}
return 10; // Default optimized mode
}
[ContextMenu("Run Size Comparison")]
public void RunComparison()
{
Debug.Log("=== Avatar Sync System Comparison ===");
// Simulate old JSON system
var jsonSize = CalculateJSONSystemSize();
// Simulate new UDP binary system
var binarySize = CalculateUDPBinarySize();
// Calculate efficiency gains
float compressionRatio = (float)jsonSize / binarySize;
float bandwidthReduction = (1f - (float)binarySize / jsonSize) * 100f;
Debug.Log($"\n📊 SIZE COMPARISON:");
Debug.Log($"Old JSON System: {jsonSize:N0} bytes ({jsonSize / 1024f:F1} KB)");
Debug.Log($"New UDP Binary: {binarySize:N0} bytes ({binarySize / 1024f:F1} KB)");
Debug.Log($"Compression Ratio: {compressionRatio:F1}x smaller");
Debug.Log($"Bandwidth Reduction: {bandwidthReduction:F1}%");
if (showDetailedBreakdown)
{
ShowDetailedBreakdown();
}
ShowNetworkImpact(jsonSize, binarySize);
ShowSystemComplexity();
if (saveToFile)
{
SaveComparisonToFile(jsonSize, binarySize);
}
}
int CalculateJSONSystemSize()
{
// Recreate the old AvatarSyncData structure using calculated values
var oldSystemData = new
{
rootTransform = new
{
worldPosition = new { x = 0f, y = 0f, z = 0f },
worldRotation = new { x = 0f, y = 0f, z = 0f, w = 1f },
localScale = new { x = 1f, y = 1f, z = 1f }
},
bones = GenerateOldSystemBones(oldSystemBoneCount),
blendShapes = GenerateOldSystemBlendShapes(oldSystemMeshCount, oldSystemBlendShapesPerMesh),
timestamp = Time.time
};
string json = JsonConvert.SerializeObject(oldSystemData, Formatting.Indented);
int jsonBytes = Encoding.UTF8.GetByteCount(json);
Debug.Log($"📄 Old System Structure: {oldSystemBoneCount} bones, {oldSystemMeshCount} meshes × {oldSystemBlendShapesPerMesh} blend shapes = {oldSystemMeshCount * oldSystemBlendShapesPerMesh} total blend shapes");
Debug.Log($"📄 Sample JSON (first 200 chars):\n{json.Substring(0, Math.Min(200, json.Length))}...");
return jsonBytes;
}
int CalculateUDPBinarySize()
{
// Get actual bone and blend shape counts from UDP system
int actualBoneCount = GetNewSystemBoneCount();
int actualBlendShapeCount = GetNewSystemBlendShapeCount();
// Calculate new UDP binary system size
int headerSize = 1 + 4 + 4 + 1; // playerID + sequenceNumber + timestamp + isFullDataMode
int rootTransformSize = (3 + 4 + 3) * 4; // 3 Vector3s + 1 Quaternion * 4 bytes per float
int bonesSize = 4 + (actualBoneCount * (3 + 4) * 4); // bone count + bones * (position + rotation) * 4 bytes per float
int blendShapesSize = 4 + (actualBlendShapeCount * 4); // blend shape count + weights * 4 bytes per float
int totalSize = headerSize + rootTransformSize + bonesSize + blendShapesSize;
Debug.Log($"🔢 New UDP Binary Structure:");
Debug.Log($" Header: {headerSize} bytes");
Debug.Log($" Root Transform: {rootTransformSize} bytes");
Debug.Log($" Bones ({actualBoneCount}): {bonesSize} bytes");
Debug.Log($" Blend Shapes ({actualBlendShapeCount}): {blendShapesSize} bytes");
Debug.Log($" Total: {totalSize} bytes");
return totalSize;
}
int GetNewSystemBoneCount()
{
// Get actual bone count from UDP broadcaster
if (udpBroadcaster != null)
{
int actualBoneCount = GetUDPBroadcasterCurrentBoneCount();
bool fullDataMode = GetUDPBroadcasterFullDataMode();
if (showDetailedBreakdown)
{
string mode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
Debug.Log($"UDP Broadcaster is in {mode} mode with {actualBoneCount} bones");
}
return actualBoneCount;
}
// Default to optimized mode bone count
return 20;
}
int GetNewSystemBlendShapeCount()
{
// Get actual blend shape count from UDP broadcaster
if (udpBroadcaster != null)
{
int actualBlendShapeCount = GetUDPBroadcasterCurrentBlendShapeCount();
bool fullDataMode = GetUDPBroadcasterFullDataMode();
if (showDetailedBreakdown)
{
string mode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
Debug.Log($"UDP Broadcaster is in {mode} mode with {actualBlendShapeCount} blend shapes");
}
return actualBlendShapeCount;
}
// Default to optimized mode blend shape count
return 10;
}
object[] GenerateOldSystemBones(int count)
{
object[] bones = new object[count];
string[] boneNames = {
"Hips", "Spine", "Spine1", "Spine2", "Neck", "Head", "HeadTop_End",
"LeftEye", "RightEye", "LeftShoulder", "LeftArm", "LeftArmTwist",
"LeftForeArm", "LeftForeArmTwist", "LeftHand", "LeftHandThumb1",
"LeftHandThumb2", "LeftHandThumb3", "LeftHandThumb4", "LeftHandIndex0",
"LeftHandIndex1", "LeftHandIndex2", "LeftHandIndex3", "LeftHandIndex4",
"LeftHandMiddle0", "LeftHandMiddle1", "LeftHandMiddle2", "LeftHandMiddle3",
"LeftHandMiddle4", "LeftHandRing0", "LeftHandRing1", "LeftHandRing2",
"LeftHandRing3", "LeftHandRing4", "LeftHandPinky0", "LeftHandPinky1",
"LeftHandPinky2", "LeftHandPinky3", "LeftHandPinky4", "LeftPalm",
"RightShoulder", "RightArm", "RightArmTwist", "RightForeArm",
"RightForeArmTwist", "RightHand", "RightHandThumb1", "RightHandThumb2",
"RightHandThumb3", "RightHandThumb4", "RightHandIndex0", "RightHandIndex1",
"RightHandIndex2", "RightHandIndex3", "RightHandIndex4", "RightHandMiddle0",
"RightHandMiddle1", "RightHandMiddle2", "RightHandMiddle3", "RightHandMiddle4",
"RightHandRing0", "RightHandRing1", "RightHandRing2", "RightHandRing3",
"RightHandRing4", "RightHandPinky0", "RightHandPinky1", "RightHandPinky2",
"RightHandPinky3", "RightHandPinky4", "RightPalm", "LeftUpLeg", "LeftLeg",
"LeftFoot", "LeftToeBase", "LeftToe_End", "RightUpLeg", "RightLeg",
"RightFoot", "RightToeBase", "RightToe_End"
};
for (int i = 0; i < count; i++)
{
string boneName = i < boneNames.Length ? boneNames[i] : $"Bone_{i}";
bones[i] = new
{
boneName = boneName,
position = new { x = UnityEngine.Random.Range(-1f, 1f), y = UnityEngine.Random.Range(-1f, 1f), z = UnityEngine.Random.Range(-1f, 1f) },
rotation = new { x = UnityEngine.Random.Range(-1f, 1f), y = UnityEngine.Random.Range(-1f, 1f), z = UnityEngine.Random.Range(-1f, 1f), w = UnityEngine.Random.Range(-1f, 1f) },
scale = new { x = 1f, y = 1f, z = 1f }
};
}
return bones;
}
object[] GenerateOldSystemBlendShapes(int meshCount, int weightsPerMesh)
{
object[] blendShapes = new object[meshCount];
string[] meshNames = { "Renderer_Head", "Renderer_EyeLeft", "Renderer_EyeRight", "Renderer_Teeth" };
for (int i = 0; i < meshCount; i++)
{
float[] weights = new float[weightsPerMesh];
for (int j = 0; j < weightsPerMesh; j++)
{
weights[j] = UnityEngine.Random.Range(0f, 100f);
}
blendShapes[i] = new
{
meshName = i < meshNames.Length ? meshNames[i] : $"Mesh_{i}",
weights = weights
};
}
return blendShapes;
}
void ShowDetailedBreakdown()
{
int newSystemBones = GetNewSystemBoneCount();
int newSystemBlendShapes = GetNewSystemBlendShapeCount();
int totalOldBlendShapes = oldSystemMeshCount * oldSystemBlendShapesPerMesh;
bool fullDataMode = GetUDPBroadcasterFullDataMode();
string currentMode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
Debug.Log($"\n📋 DETAILED BREAKDOWN:");
Debug.Log($"\nCurrent UDP System Configuration:");
Debug.Log($" • Mode: {currentMode}");
Debug.Log($" • Update Rate: {updateRate} Hz");
Debug.Log($" • Bones: {newSystemBones}");
Debug.Log($" • Blend Shapes: {newSystemBlendShapes}");
Debug.Log($"\nOld JSON System Issues:");
Debug.Log($" • {oldSystemBoneCount} bones with full transform data");
Debug.Log($" • {oldSystemMeshCount} meshes × {oldSystemBlendShapesPerMesh} blend shapes = {totalOldBlendShapes} facial weights");
Debug.Log($" • Verbose JSON with field names repeated");
Debug.Log($" • UTF-8 string encoding overhead");
Debug.Log($" • HTTP headers and protocol overhead");
Debug.Log($" • Requires Python server infrastructure");
Debug.Log($"\nNew UDP Binary Advantages:");
if (fullDataMode)
{
Debug.Log($" • {newSystemBones} bones (FULL avatar data)");
Debug.Log($" • {newSystemBlendShapes} facial blend shapes (ALL expressions)");
}
else
{
Debug.Log($" • Only {newSystemBones} priority bones (most important)");
Debug.Log($" • {newSystemBlendShapes} key facial blend shapes only");
}
Debug.Log($" • Compact binary format, no field names");
Debug.Log($" • Variable-size packets with length prefixes");
Debug.Log($" • Direct UDP broadcast, no server needed");
Debug.Log($" • Predictable network usage");
float boneReduction = (1f - (float)newSystemBones / oldSystemBoneCount) * 100f;
float blendShapeReduction = (1f - (float)newSystemBlendShapes / totalOldBlendShapes) * 100f;
Debug.Log($"\nData Reduction vs Old System:");
Debug.Log($" • Bones: {boneReduction:F1}% reduction ({oldSystemBoneCount} → {newSystemBones})");
Debug.Log($" • Blend Shapes: {blendShapeReduction:F1}% reduction ({totalOldBlendShapes} → {newSystemBlendShapes})");
}
void ShowNetworkImpact(int jsonSize, int binarySize)
{
Debug.Log($"\n🌐 NETWORK IMPACT (at {updateRate} Hz):");
float jsonBandwidthKBps = (jsonSize * updateRate) / 1024f;
float binaryBandwidthKBps = (binarySize * updateRate) / 1024f;
Debug.Log($"Old System: {jsonBandwidthKBps:F1} KB/s per player");
Debug.Log($"New System: {binaryBandwidthKBps:F1} KB/s per player");
// Calculate for 3 player setup
int playerCount = 3;
Debug.Log($"\n3 Player Setup:");
Debug.Log($"Old System: {jsonBandwidthKBps * playerCount:F1} KB/s total");
Debug.Log($"New System: {binaryBandwidthKBps * playerCount:F1} KB/s total");
Debug.Log($"\nData usage per hour (single player):");
float oldMBPerHour = (jsonBandwidthKBps * 3600) / 1024;
float newMBPerHour = (binaryBandwidthKBps * 3600) / 1024;
Debug.Log($"Old System: {oldMBPerHour:F1} MB/hour");
Debug.Log($"New System: {newMBPerHour:F1} MB/hour");
Debug.Log($"Savings: {oldMBPerHour - newMBPerHour:F1} MB/hour ({((oldMBPerHour - newMBPerHour) / oldMBPerHour) * 100:F1}% reduction)");
}
void ShowSystemComplexity()
{
float codeReduction = (1f - (float)newSystemTotalLines / oldSystemTotalLines) * 100f;
Debug.Log($"\n⚙ SYSTEM COMPLEXITY:");
Debug.Log($"\nOld System Components:");
Debug.Log($" • avatar_sync_server.py");
Debug.Log($" • AvatarSyncClient.cs");
Debug.Log($" • AvatarDataUploader.cs");
Debug.Log($" • AvatarDataDownloader.cs");
Debug.Log($" • AvatarDataWriter.cs");
Debug.Log($" • AvatarDataReader.cs");
Debug.Log($" • Multiple serialization classes");
Debug.Log($" • HTTP client/server infrastructure");
Debug.Log($" Total: ~{oldSystemTotalLines:N0} lines of code");
Debug.Log($"\nNew System Components:");
Debug.Log($" • UDPAvatarBroadcaster.cs");
Debug.Log($" • UDPAvatarReceiver.cs");
Debug.Log($" • Simple binary serialization");
Debug.Log($" • No server infrastructure needed");
Debug.Log($" Total: ~{newSystemTotalLines:N0} lines of code");
Debug.Log($"\n✅ New system is {codeReduction:F1}% less code and much simpler!");
Debug.Log($"Code reduction: {oldSystemTotalLines:N0} → {newSystemTotalLines:N0} lines ({oldSystemTotalLines - newSystemTotalLines:N0} lines removed)");
}
void SaveComparisonToFile(int jsonSize, int binarySize)
{
try
{
// Calculate all metrics using dynamic values
float compressionRatio = (float)jsonSize / binarySize;
float bandwidthReduction = (1f - (float)binarySize / jsonSize) * 100f;
float jsonBandwidthKBps = (jsonSize * updateRate) / 1024f;
float binaryBandwidthKBps = (binarySize * updateRate) / 1024f;
// Get calculated counts
int newSystemBones = GetNewSystemBoneCount();
int newSystemBlendShapes = GetNewSystemBlendShapeCount();
int totalOldBlendShapes = oldSystemMeshCount * oldSystemBlendShapesPerMesh;
bool fullDataMode = GetUDPBroadcasterFullDataMode();
string currentMode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
// Create file content with just the data
StringBuilder fileContent = new StringBuilder();
fileContent.AppendLine("Avatar Sync System Comparison Results");
fileContent.AppendLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
fileContent.AppendLine();
// Size comparison
fileContent.AppendLine("PACKET SIZE COMPARISON:");
fileContent.AppendLine($"Old JSON System: {jsonSize:N0} bytes ({jsonSize / 1024f:F2} KB)");
fileContent.AppendLine($"New UDP Binary: {binarySize:N0} bytes ({binarySize / 1024f:F2} KB)");
fileContent.AppendLine($"Compression Ratio: {compressionRatio:F1}x smaller");
fileContent.AppendLine($"Size Reduction: {bandwidthReduction:F1}%");
fileContent.AppendLine();
// Binary structure breakdown using calculated values
int headerSize = 1 + 4 + 4 + 1; // playerID + sequenceNumber + timestamp + isFullDataMode
int rootTransformSize = (3 + 4 + 3) * 4;
int bonesSize = 4 + (newSystemBones * (3 + 4) * 4);
int blendShapesSize = 4 + (newSystemBlendShapes * 4);
fileContent.AppendLine("UDP BINARY STRUCTURE:");
fileContent.AppendLine($"Header: {headerSize} bytes");
fileContent.AppendLine($"Root Transform: {rootTransformSize} bytes");
fileContent.AppendLine($"Bones ({newSystemBones}): {bonesSize} bytes");
fileContent.AppendLine($"Blend Shapes ({newSystemBlendShapes}): {blendShapesSize} bytes");
fileContent.AppendLine($"Total: {binarySize} bytes");
fileContent.AppendLine();
// Bandwidth impact at configured rate
fileContent.AppendLine($"BANDWIDTH USAGE ({updateRate} Hz):");
fileContent.AppendLine($"Old System: {jsonBandwidthKBps:F1} KB/s per player");
fileContent.AppendLine($"New System: {binaryBandwidthKBps:F1} KB/s per player");
fileContent.AppendLine();
// System details
fileContent.AppendLine("SYSTEM DETAILS:");
fileContent.AppendLine($"Update Rate: {updateRate} Hz");
fileContent.AppendLine($"UDP Mode: {currentMode}");
fileContent.AppendLine($"Old System: {oldSystemBoneCount} bones, {totalOldBlendShapes} blend shapes ({oldSystemMeshCount} meshes × {oldSystemBlendShapesPerMesh})");
fileContent.AppendLine($"New System: {newSystemBones} bones, {newSystemBlendShapes} blend shapes");
fileContent.AppendLine();
// 3 Player setup bandwidth
int players = 3;
float oldTotal = jsonBandwidthKBps * players;
float newTotal = binaryBandwidthKBps * players;
fileContent.AppendLine("3 PLAYER SETUP BANDWIDTH:");
fileContent.AppendLine($"Old System: {oldTotal:F1} KB/s total");
fileContent.AppendLine($"New System: {newTotal:F1} KB/s total");
fileContent.AppendLine($"Bandwidth Savings: {oldTotal - newTotal:F1} KB/s ({((oldTotal - newTotal) / oldTotal) * 100:F1}% reduction)");
fileContent.AppendLine();
// Data usage per hour
float oldMBPerHour = (jsonBandwidthKBps * 3600) / 1024;
float newMBPerHour = (binaryBandwidthKBps * 3600) / 1024;
fileContent.AppendLine("DATA USAGE PER HOUR (single player):");
fileContent.AppendLine($"Old System: {oldMBPerHour:F1} MB/hour");
fileContent.AppendLine($"New System: {newMBPerHour:F1} MB/hour");
fileContent.AppendLine($"Savings: {oldMBPerHour - newMBPerHour:F1} MB/hour ({((oldMBPerHour - newMBPerHour) / oldMBPerHour) * 100:F1}% reduction)");
fileContent.AppendLine();
// Code complexity
float codeReduction = (1f - (float)newSystemTotalLines / oldSystemTotalLines) * 100f;
fileContent.AppendLine("CODE COMPLEXITY:");
fileContent.AppendLine($"Old System: ~{oldSystemTotalLines:N0} lines (6 components + Python server)");
fileContent.AppendLine($"New System: ~{newSystemTotalLines:N0} lines (2 components, no server)");
fileContent.AppendLine($"Code Reduction: {codeReduction:F1}% ({oldSystemTotalLines - newSystemTotalLines:N0} lines removed)");
fileContent.AppendLine();
// Performance characteristics
fileContent.AppendLine("PERFORMANCE CHARACTERISTICS:");
fileContent.AppendLine("Old System: HTTP request/response, JSON parsing, server dependency");
fileContent.AppendLine("New System: UDP broadcast, binary format, peer-to-peer");
fileContent.AppendLine("Latency: Local network UDP (~1-10ms vs HTTP ~10-50ms)");
fileContent.AppendLine("Infrastructure: None required (vs Python server)");
// Write to file
string filePath = Path.Combine(Application.dataPath, outputFileName);
File.WriteAllText(filePath, fileContent.ToString());
Debug.Log($"Comparison data saved to: {filePath}");
}
catch (Exception e)
{
Debug.LogError($"Failed to save comparison to file: {e.Message}");
}
}
void OnGUI()
{
if (GUI.Button(new Rect(10, Screen.height - 40, 200, 30), "Run Comparison"))
{
RunComparison();
}
if (GUI.Button(new Rect(220, Screen.height - 40, 150, 30), "Save to File"))
{
var jsonSize = CalculateJSONSystemSize();
var binarySize = CalculateUDPBinarySize();
SaveComparisonToFile(jsonSize, binarySize);
}
}
}

View File

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

View File

@ -1,142 +0,0 @@
# Local Testing Setup Guide
## Testing Both Players in Same Scene
When testing the UDP avatar sync system with both broadcaster and receiver in the same Unity scene, you need to handle port conflicts properly.
### The Port Conflict Issue
The error you encountered:
```
Failed to start UDP listener: Only one usage of each socket address (protocol/network address/port) is normally permitted.
```
This happens because both components try to bind to the same UDP port (8080) simultaneously.
## Solution: Updated Receiver with Port Sharing
The `UDPAvatarReceiver` has been updated with port sharing capabilities:
### New Settings in Inspector
- **Allow Port Sharing**: ✅ Enable this for local testing
- **Listen Port**: 8080 (or will auto-find alternative)
### How It Works
1. **Port Sharing Enabled**: Uses `SO_REUSEADDR` socket option to allow multiple bindings
2. **Fallback Ports**: If port sharing fails, tries ports 8081, 8082, 8083, etc.
3. **Debug Info**: Shows actual port being used in the GUI
## Local Testing Setup
### GameObject 1: Player 1 (Broadcaster + Receiver)
```
Avatar Player 1
├── UDPAvatarBroadcaster
│ ├── Player ID: 1
│ ├── Avatar Root: [Player 1 Avatar]
│ └── Show Debug Info: ✅
└── UDPAvatarReceiver
├── Target Player ID: 2 (receive from Player 2)
├── Target Avatar Root: [Player 2 Avatar Clone]
├── Allow Port Sharing: ✅
└── Show Debug Info: ✅
```
### GameObject 2: Player 2 (Broadcaster + Receiver)
```
Avatar Player 2
├── UDPAvatarBroadcaster
│ ├── Player ID: 2
│ ├── Avatar Root: [Player 2 Avatar]
│ └── Show Debug Info: ✅
└── UDPAvatarReceiver
├── Target Player ID: 1 (receive from Player 1)
├── Target Avatar Root: [Player 1 Avatar Clone]
├── Allow Port Sharing: ✅
└── Show Debug Info: ✅
```
## Alternative Setup Methods
### Method 1: Different Ports (Simple)
- Player 1 Broadcaster: Port 8080
- Player 2 Broadcaster: Port 8081
- Update receivers to listen on corresponding ports
### Method 2: Single Broadcaster (Minimal)
- One broadcaster component sending data
- Multiple receivers in scene listening
- Good for testing receiver logic only
### Method 3: Separate Scenes (Real-world)
- Build and run multiple instances of your game
- Most accurate representation of real multiplayer
## Troubleshooting
### Still Getting Port Errors?
1. **Check Windows Firewall**:
```
Allow Unity.exe through Windows Defender Firewall
Allow UDP port 8080-8085
```
2. **Verify Port Sharing Setting**:
```
UDPAvatarReceiver → Allow Port Sharing: ✅
```
3. **Check Console for Alternative Ports**:
```
Look for: "UDP listener started on alternative port 8081"
```
### No Data Being Received?
1. **Verify Player IDs**:
- Broadcaster Player ID: 1
- Receiver Target Player ID: 1 (to receive from broadcaster)
2. **Check Debug GUI**:
- Broadcaster: "Broadcasting" status
- Receiver: "Listen Port" shows actual port
3. **Test Loopback**:
- Set Receiver Target Player ID: 0 (receive from any)
- Should receive own broadcasts
## Debug Information
Enable debug info on both components to see:
**Broadcaster GUI (Top-left)**:
- Player ID and broadcast status
- Sequence number (should increment)
- Update rate and packet size
**Receiver GUI (Top-right)**:
- Listen port (actual port being used)
- Packets received/dropped
- Target player ID
## Production vs Testing
| Environment | Setup | Port Handling |
|-------------|-------|---------------|
| **Local Testing** | Same scene, port sharing | Allow Port Sharing: ✅ |
| **LAN/Hotspot** | Separate devices | Standard UDP binding |
| **Production** | Different machines | No port conflicts |
## Quick Checklist
1. ✅ Enable "Allow Port Sharing" on all receivers
2. ✅ Set unique Player IDs (1, 2, 3...)
3. ✅ Set correct Target Player IDs on receivers
4. ✅ Enable debug info to monitor status
5. ✅ Check console for port conflict messages
6. ✅ Verify Windows Firewall allows Unity/UDP
The updated receiver should now handle local testing scenarios gracefully!

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: d6945fe49cfac414eb744e574611562c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -19,10 +19,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
public class ConvaiSimpleUDPAudioReceiver : MonoBehaviour
{
[Header("Network Configuration")]
[SerializeField] private int listenPort = 12345;
[SerializeField] private bool enableDebugLogging = true;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[Header("NPC Target")]
[SerializeField] private bool useActiveNPC = true;
@ -35,6 +32,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
private UdpClient _udpListener;
private IPEndPoint _remoteEndPoint;
private bool _isListening = false;
private int listenPort;
private CancellationTokenSource _cancellationTokenSource;
// Audio state tracking
@ -78,15 +76,19 @@ namespace Convai.Scripts.Runtime.Multiplayer
{
_cancellationTokenSource = new CancellationTokenSource();
_persistentDataPath = Application.persistentDataPath;
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
listenPort = cfg.multiplayerAudioPort;
}
listenPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
listenPort = 1221;
}
InitializeNetwork();
InitializeConvai();

View File

@ -20,10 +20,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
public class ConvaiSimpleUDPAudioSender : MonoBehaviour
{
[Header("Network Settings")]
[SerializeField] private string targetIP = "127.0.0.1";
[SerializeField] private int targetPort = 12345;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
// Network configuration loaded from NetworkConfig.Instance
[Header("Audio Settings")]
[SerializeField] private int recordingFrequency = 16000;
@ -46,6 +43,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
private UdpClient _udpClient;
private IPEndPoint _targetEndPoint;
private string targetIP;
private int targetPort;
private AudioClip _audioClip;
private bool _isRecording = false;
private CancellationTokenSource _cancellationTokenSource;
@ -83,16 +82,20 @@ namespace Convai.Scripts.Runtime.Multiplayer
private void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
targetIP = cfg.ipAddress;
targetPort = cfg.multiplayerAudioPort;
}
targetIP = cfg.ipAddress;
targetPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
targetIP = "255.255.255.255";
targetPort = 1221;
}
InitializeNetwork();
InitializeAudio();
_persistentDataPath = Application.persistentDataPath;

View File

@ -17,10 +17,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
public class ConvaiUDPSpeechReceiver : MonoBehaviour
{
[Header("Network Configuration")]
[SerializeField] private int listenPort = 12346;
[SerializeField] private bool enableDebugLogging = true;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[Header("Audio Playback")]
[SerializeField] private AudioSource speechAudioSource;
@ -35,6 +32,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
private UdpClient _udpListener;
private IPEndPoint _remoteEndPoint;
private bool _isListening = false;
private int listenPort;
private CancellationTokenSource _cancellationTokenSource;
// Audio reconstruction
@ -107,15 +105,19 @@ namespace Convai.Scripts.Runtime.Multiplayer
private void Start()
{
_cancellationTokenSource = new CancellationTokenSource();
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
listenPort = cfg.multiplayerSpeechPort;
}
listenPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
listenPort = 1221;
}
InitializeAudio();
InitializeNetwork();
}

View File

@ -18,11 +18,7 @@ namespace Convai.Scripts.Runtime.Multiplayer
public class ConvaiUDPSpeechSender : MonoBehaviour
{
[Header("Network Configuration")]
[SerializeField] private string targetIP = "127.0.0.1";
[SerializeField] private int targetPort = 12346;
[SerializeField] private bool enableDebugLogging = true;
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[Header("NPC Source")]
[SerializeField] private bool useActiveNPC = true;
@ -35,6 +31,8 @@ namespace Convai.Scripts.Runtime.Multiplayer
// Network components
private UdpClient _udpClient;
private IPEndPoint _targetEndPoint;
private string targetIP;
private int targetPort;
private bool _isInitialized = false;
// Speech tracking
@ -55,16 +53,20 @@ namespace Convai.Scripts.Runtime.Multiplayer
private void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
targetIP = cfg.ipAddress;
targetPort = cfg.multiplayerSpeechPort;
}
targetIP = cfg.ipAddress;
targetPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
targetIP = "255.255.255.255";
targetPort = 1221;
}
InitializeNetwork();
InitializeConvai();
}

Binary file not shown.

View File

@ -5,11 +5,7 @@ public class NetworkConfig : ScriptableObject
{
[Header("Global Network Settings")]
public string ipAddress = "127.0.0.1";
public int port = 8080;
[Header("Multiplayer Ports")]
public int multiplayerAudioPort = 12345; // For ConvaiSimpleUDPAudio (send/receive)
public int multiplayerSpeechPort = 12346; // For ConvaiUDPSpeech (send/receive)
public int port = 1221; // Single port for all UDP communication
private static NetworkConfig _instance;
public static NetworkConfig Instance

View File

@ -1,66 +0,0 @@
# Avatar Synchronization Scripts
This system allows you to synchronize Ready Player Me avatars in Unity by reading all transforms, rotations, and blendshapes from one avatar and applying them to another.
## Files Created
- `AvatarDataWriter.cs` - Reads avatar data and saves to JSON
- `AvatarDataReader.cs` - Reads JSON data and applies to target avatar
## Prerequisites
- **Newtonsoft.Json** package must be installed in Unity
- Install via Package Manager: Window → Package Manager → "+" → Add package by name → `com.unity.nuget.newtonsoft-json`
## Setup Instructions
### 1. Writer Setup (Source Avatar)
1. Add the `AvatarDataWriter` component to your source Ready Player Me avatar GameObject
2. In the inspector:
- **Avatar Root**: Assign the root transform of your source avatar (if empty, uses the GameObject this script is on)
- **File Name**: Keep default `avatar_sync_data.json` or change as needed
- **Write Every Frame**: Check for real-time sync, uncheck for rate-limited updates
- **Update Rate**: If not writing every frame, set desired updates per second (default: 60)
### 2. Reader Setup (Target Avatar)
1. Add the `AvatarDataReader` component to your target Ready Player Me avatar GameObject
2. In the inspector:
- **Target Avatar Root**: Assign the root transform of your target avatar
- **File Name**: Must match the writer's file name
- **Read Every Frame**: Check for real-time sync, uncheck for rate-limited updates
- **Update Rate**: If not reading every frame, set desired updates per second (default: 60)
- **Smooth Transitions**: Check for smooth interpolation, uncheck for instant updates
- **Transition Speed**: Speed of smooth transitions (default: 10)
- **Show Debug Info**: Check to see debug messages in console
## How It Works
### AvatarDataWriter
- Captures all transform data (position, rotation, scale) from the entire avatar hierarchy
- Captures all blendshape weights from SkinnedMeshRenderer components
- Saves this data as JSON to `Assets/Sync-Files/[filename].json`
- Updates every frame or at a specified rate
### AvatarDataReader
- Reads the JSON file created by the writer
- Maps transform paths to find corresponding bones on the target avatar
- Applies transform data and blendshape weights to the target avatar
- Supports smooth transitions or instant updates
## Important Notes
1. **File Path**: Data is saved to `Assets/Sync-Files/` directory (created automatically)
2. **Avatar Structure**: Both avatars should have similar bone hierarchies for best results
3. **Performance**: Writing/reading every frame is intensive - consider rate limiting for better performance
4. **Blendshapes**: Only works if both avatars have compatible blendshape setups
5. **Transform Paths**: The system uses transform hierarchy paths to match bones between avatars
## Troubleshooting
- **Missing transforms**: Check debug info to see which transforms couldn't be found
- **Poor performance**: Reduce update rate or disable "Every Frame" options
- **Blendshapes not working**: Ensure both avatars have SkinnedMeshRenderer components with blendshapes
- **File not found**: Check that the Sync-Files directory exists and the file names match
## Example Usage
1. Put `AvatarDataWriter` on your player avatar
2. Put `AvatarDataReader` on your sync avatar
3. Play the scene - the sync avatar should now copy all movements and expressions from the player avatar in real-time!

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: d5cfc93a5dbeee345a3993c27049b134
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,178 +0,0 @@
# UDP Avatar Sync System
A simplified, efficient replacement for the HTTP/JSON avatar sync system using UDP broadcasting and compact binary data.
## Key Improvements
### Old System (HTTP/JSON)
- ❌ Required Python server (`avatar_sync_server.py`)
- ❌ Large JSON payloads (~1792 lines, complex nested objects)
- ❌ HTTP request/response overhead
- ❌ Complex serialization with multiple C# classes
- ❌ All 80+ bones transmitted every frame
- ❌ Inefficient for real-time sync
### New System (UDP Broadcasting)
-**No server required** - pure peer-to-peer broadcasting
-**Compact binary format** - ~670 bytes per packet (vs ~50KB+ JSON)
-**UDP broadcasting** - automatic discovery, no IP configuration
-**Priority-based data** - only essential bones and facial data
-**Fixed packet size** - predictable network usage
-**Real-time optimized** - designed for 30-60 Hz updates
## Components
### UDPAvatarBroadcaster
Captures and broadcasts your avatar data to the local network.
**Key Features:**
- Selects only priority bones (20 most important)
- Compact binary serialization
- Configurable update rates (1-120 Hz)
- Optional facial blend shape data
- Network performance monitoring
### UDPAvatarReceiver
Receives and applies avatar data from other players.
**Key Features:**
- Multi-threaded UDP listening
- Smooth interpolation options
- Packet loss detection
- Player ID filtering
- Thread-safe data handling
## Quick Setup
### For Player 1 (Broadcaster)
1. Add `UDPAvatarBroadcaster` component to your avatar
2. Set `Player ID` to `1`
3. Assign your avatar root transform
4. Enable broadcasting
### For Player 2 (Receiver)
1. Add `UDPAvatarReceiver` component to target avatar
2. Set `Target Player ID` to `1` (to receive from Player 1)
3. Assign target avatar root transform
4. Enable receiver
### For Bidirectional Sync
- Each player needs both components
- Use different Player IDs (1, 2, 3, etc.)
- Set Target Player ID to receive from specific players
## Configuration
### Network Settings
- **Port**: 8080 (default, ensure firewall allows UDP)
- **Broadcast Address**: 255.255.255.255 (local network)
- **Update Rate**: 30 Hz recommended (balance of smoothness vs bandwidth)
### Data Selection
- **Priority Bones**: 20 essential bones (customizable array)
- Core: Hips, Spine, Neck, Head
- Arms: Shoulders, Arms, Forearms, Hands
- Legs: Upper legs, Legs, Feet
- **Facial Data**: 10 most important blend shapes
- **Root Transform**: Position, rotation, scale options
### Performance Options
- **Smooth Transitions**: Interpolate between updates
- **Transition Speed**: How fast to blend changes
- **Debug Info**: Network stats and diagnostics
## Data Structure
The system uses a fixed binary format:
```
Header (9 bytes):
- Player ID (1 byte)
- Sequence Number (4 bytes)
- Timestamp (4 bytes)
Root Transform (40 bytes):
- Position (12 bytes: 3 floats)
- Rotation (16 bytes: 4 floats)
- Scale (12 bytes: 3 floats)
Priority Bones (560 bytes):
- 20 bones × 28 bytes each
- Position (12 bytes) + Rotation (16 bytes) per bone
Facial Data (40 bytes):
- 10 blend shape weights (4 bytes each)
Total: ~649 bytes per packet
```
## Network Requirements
- **Local Network**: All devices must be on same subnet
- **Firewall**: Allow UDP port 8080 (or configured port)
- **Bandwidth**: ~20KB/s per player at 30 Hz (very lightweight)
## Troubleshooting
### No Data Received
1. Check firewall settings (allow UDP on chosen port)
2. Verify devices are on same network subnet
3. Ensure different Player IDs for broadcaster/receiver
4. Check Debug Info for network statistics
### Performance Issues
1. Reduce update rate (try 15-20 Hz)
2. Disable smooth transitions for lower latency
3. Reduce max blend shapes count
4. Check packet loss statistics
### Bone Mapping Issues
1. Verify bone names match priority bone list
2. Check Debug Info to see cached bone count
3. Customize priority bones array for your rig
## Advanced Usage
### Custom Bone Sets
Modify the `priorityBones` array to match your avatar rig:
```csharp
private string[] priorityBones = {
"Root", "Spine1", "Spine2", "Head",
"LeftArm", "RightArm",
// Add your specific bone names
};
```
### Multiple Players
- Each broadcaster needs unique Player ID (1-255)
- Receivers can filter by Target Player ID
- Set Target Player ID to 0 to receive from any player
### Network Optimization
- Increase update rate for smoother motion (higher bandwidth)
- Decrease update rate for lower bandwidth usage
- Adjust transition speed for different responsiveness
## Migration from Old System
1. **Remove old components**:
- AvatarSyncClient
- AvatarDataUploader
- AvatarDataDownloader
- AvatarDataWriter
- AvatarDataReader
2. **Remove server dependency**:
- Stop `avatar_sync_server.py`
- No longer need HTTP endpoints
3. **Add new components**:
- UDPAvatarBroadcaster (for sending)
- UDPAvatarReceiver (for receiving)
4. **Configure network**:
- Ensure UDP port is open
- Set unique Player IDs
- Test on local network first
The new system is much simpler to set up and requires no server infrastructure!

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 7a81e1eb398089c45a8cf945c42c940c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,580 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class FacialExpression
{
public string name;
public BlendShapeWeight[] blendShapeWeights;
[Range(0.1f, 5f)]
public float duration = 1f;
[Range(0f, 2f)]
public float holdTime = 0.5f;
}
[System.Serializable]
public class BlendShapeWeight
{
public string blendShapeName;
[Range(0f, 1f)]
public float weight = 0.5f;
}
public class RPMFacialAnimator : MonoBehaviour
{
[Header("Avatar Configuration")]
[SerializeField] private Transform avatarRoot;
[SerializeField] private string headRendererName = "Renderer_Head";
[Header("Animation Settings")]
[SerializeField] private bool autoPlay = true;
[SerializeField] private bool loopAnimations = true;
[SerializeField] private float timeBetweenExpressions = 2f;
[SerializeField] private bool smoothTransitions = true;
[SerializeField] private AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[Header("Predefined Expressions")]
[SerializeField] private FacialExpression[] facialExpressions;
[Header("Manual Controls")]
[SerializeField] private bool enableManualControls = true;
[Range(0f, 1f)] public float mouthSmile = 0f;
[Range(0f, 1f)] public float mouthOpen = 0f;
[Range(0f, 1f)] public float eyeBlinkLeft = 0f;
[Range(0f, 1f)] public float eyeBlinkRight = 0f;
[Range(0f, 1f)] public float browInnerUp = 0f;
[Range(0f, 1f)] public float cheekPuff = 0f;
[Range(0f, 1f)] public float jawOpen = 0f;
[Header("Auto Blink Settings")]
[SerializeField] private bool enableAutoBlink = true;
[SerializeField] private float blinkInterval = 3f;
[SerializeField] private float blinkDuration = 0.15f;
[SerializeField] private float blinkRandomness = 1f;
[Header("Debug")]
[SerializeField] private bool showDebugInfo = false;
[SerializeField] private bool listAllBlendShapes = false;
private SkinnedMeshRenderer headRenderer;
private Dictionary<string, int> blendShapeIndexMap;
private Coroutine expressionCoroutine;
private Coroutine blinkCoroutine;
private bool isAnimating = false;
// RPM ARKit Blendshape names for reference
private readonly string[] arkitBlendShapes = {
// Eyes
"eyeBlinkLeft", "eyeBlinkRight", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
"eyeSquintLeft", "eyeWideLeft", "eyeLookDownRight", "eyeLookInRight", "eyeLookOutRight", "eyeLookUpRight",
"eyeSquintRight", "eyeWideRight",
// Jaw
"jawForward", "jawLeft", "jawRight", "jawOpen",
// Mouth
"mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight", "mouthSmileLeft", "mouthSmileRight",
"mouthFrownLeft", "mouthFrownRight", "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
"mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", "mouthPressLeft", "mouthPressRight",
"mouthLowerDownLeft", "mouthLowerDownRight", "mouthUpperUpLeft", "mouthUpperUpRight",
// Brows
"browDownLeft", "browDownRight", "browInnerUp", "browOuterUpLeft", "browOuterUpRight",
// Cheeks
"cheekPuff", "cheekSquintLeft", "cheekSquintRight",
// Nose
"noseSneerLeft", "noseSneerRight",
// Tongue
"tongueOut",
// Additional
"mouthOpen", "mouthSmile", "eyesClosed", "eyesLookUp", "eyesLookDown"
};
void Start()
{
InitializeAnimator();
SetupDefaultExpressions();
if (autoPlay && !enableManualControls)
{
StartExpressionAnimation();
}
if (enableAutoBlink)
{
StartAutoBlinking();
}
}
void InitializeAnimator()
{
// Find avatar root if not assigned
if (avatarRoot == null)
{
avatarRoot = transform;
}
// Find the head renderer
Transform headRendererTransform = FindChildRecursive(avatarRoot, headRendererName);
if (headRendererTransform == null)
{
Debug.LogError($"RPMFacialAnimator: Could not find child object with name '{headRendererName}' in avatar hierarchy.");
return;
}
headRenderer = headRendererTransform.GetComponent<SkinnedMeshRenderer>();
if (headRenderer == null)
{
Debug.LogError($"RPMFacialAnimator: '{headRendererName}' does not have a SkinnedMeshRenderer component.");
return;
}
// Build blend shape index map
BuildBlendShapeIndexMap();
if (showDebugInfo)
{
Debug.Log($"RPMFacialAnimator: Initialized with {blendShapeIndexMap.Count} blend shapes on {headRendererName}");
}
if (listAllBlendShapes)
{
ListAllBlendShapes();
}
}
void BuildBlendShapeIndexMap()
{
blendShapeIndexMap = new Dictionary<string, int>();
if (headRenderer?.sharedMesh == null) return;
for (int i = 0; i < headRenderer.sharedMesh.blendShapeCount; i++)
{
string blendShapeName = headRenderer.sharedMesh.GetBlendShapeName(i);
blendShapeIndexMap[blendShapeName] = i;
}
}
void ListAllBlendShapes()
{
if (headRenderer?.sharedMesh == null) return;
Debug.Log("=== RPM Avatar Blend Shapes ===");
for (int i = 0; i < headRenderer.sharedMesh.blendShapeCount; i++)
{
string name = headRenderer.sharedMesh.GetBlendShapeName(i);
bool isARKit = Array.Exists(arkitBlendShapes, bs => bs == name);
Debug.Log($"Blend Shape {i}: {name} {(isARKit ? "(ARKit)" : "(Custom)")}");
}
}
Transform FindChildRecursive(Transform parent, string childName)
{
foreach (Transform child in parent)
{
if (child.name == childName)
return child;
Transform found = FindChildRecursive(child, childName);
if (found != null)
return found;
}
return null;
}
void SetupDefaultExpressions()
{
if (facialExpressions == null || facialExpressions.Length == 0)
{
facialExpressions = new FacialExpression[]
{
// Happy Expression
new FacialExpression
{
name = "Happy",
duration = 1.5f,
holdTime = 1f,
blendShapeWeights = new BlendShapeWeight[]
{
new BlendShapeWeight { blendShapeName = "mouthSmile", weight = 0.9f },
new BlendShapeWeight { blendShapeName = "mouthSmileLeft", weight = 0.8f },
new BlendShapeWeight { blendShapeName = "mouthSmileRight", weight = 0.8f },
new BlendShapeWeight { blendShapeName = "cheekSquintLeft", weight = 0.5f },
new BlendShapeWeight { blendShapeName = "cheekSquintRight", weight = 0.5f }
}
},
// Surprised Expression
new FacialExpression
{
name = "Surprised",
duration = 1f,
holdTime = 0.8f,
blendShapeWeights = new BlendShapeWeight[]
{
new BlendShapeWeight { blendShapeName = "mouthOpen", weight = 0.8f },
new BlendShapeWeight { blendShapeName = "jawOpen", weight = 0.6f },
new BlendShapeWeight { blendShapeName = "eyeWideLeft", weight = 1.0f },
new BlendShapeWeight { blendShapeName = "eyeWideRight", weight = 1.0f },
new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.9f },
new BlendShapeWeight { blendShapeName = "browOuterUpLeft", weight = 0.7f },
new BlendShapeWeight { blendShapeName = "browOuterUpRight", weight = 0.7f }
}
},
// Sad Expression
new FacialExpression
{
name = "Sad",
duration = 2f,
holdTime = 1.5f,
blendShapeWeights = new BlendShapeWeight[]
{
new BlendShapeWeight { blendShapeName = "mouthFrownLeft", weight = 0.8f },
new BlendShapeWeight { blendShapeName = "mouthFrownRight", weight = 0.8f },
new BlendShapeWeight { blendShapeName = "browDownLeft", weight = 0.6f },
new BlendShapeWeight { blendShapeName = "browDownRight", weight = 0.6f },
new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.5f }
}
},
// Angry Expression
new FacialExpression
{
name = "Angry",
duration = 1.2f,
holdTime = 1f,
blendShapeWeights = new BlendShapeWeight[]
{
new BlendShapeWeight { blendShapeName = "browDownLeft", weight = 1.0f },
new BlendShapeWeight { blendShapeName = "browDownRight", weight = 1.0f },
new BlendShapeWeight { blendShapeName = "eyeSquintLeft", weight = 0.7f },
new BlendShapeWeight { blendShapeName = "eyeSquintRight", weight = 0.7f },
new BlendShapeWeight { blendShapeName = "mouthFrownLeft", weight = 0.6f },
new BlendShapeWeight { blendShapeName = "mouthFrownRight", weight = 0.6f },
new BlendShapeWeight { blendShapeName = "noseSneerLeft", weight = 0.5f },
new BlendShapeWeight { blendShapeName = "noseSneerRight", weight = 0.5f }
}
},
// Thinking Expression
new FacialExpression
{
name = "Thinking",
duration = 1.8f,
holdTime = 2f,
blendShapeWeights = new BlendShapeWeight[]
{
new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.6f },
new BlendShapeWeight { blendShapeName = "mouthPucker", weight = 0.5f },
new BlendShapeWeight { blendShapeName = "eyeLookUpLeft", weight = 0.4f },
new BlendShapeWeight { blendShapeName = "eyeLookUpRight", weight = 0.4f }
}
}
};
}
}
void Update()
{
if (enableManualControls && headRenderer != null)
{
ApplyManualControls();
}
}
void ApplyManualControls()
{
SetBlendShapeWeight("mouthSmile", mouthSmile);
SetBlendShapeWeight("mouthOpen", mouthOpen);
SetBlendShapeWeight("eyeBlinkLeft", eyeBlinkLeft);
SetBlendShapeWeight("eyeBlinkRight", eyeBlinkRight);
SetBlendShapeWeight("browInnerUp", browInnerUp);
SetBlendShapeWeight("cheekPuff", cheekPuff);
SetBlendShapeWeight("jawOpen", jawOpen);
}
public void SetBlendShapeWeight(string blendShapeName, float weight)
{
if (headRenderer == null || blendShapeIndexMap == null) return;
if (blendShapeIndexMap.TryGetValue(blendShapeName, out int index))
{
headRenderer.SetBlendShapeWeight(index, Mathf.Clamp(weight, 0f, 1f));
}
else if (showDebugInfo)
{
Debug.LogWarning($"RPMFacialAnimator: Blend shape '{blendShapeName}' not found.");
}
}
public float GetBlendShapeWeight(string blendShapeName)
{
if (headRenderer == null || blendShapeIndexMap == null) return 0f;
if (blendShapeIndexMap.TryGetValue(blendShapeName, out int index))
{
return headRenderer.GetBlendShapeWeight(index);
}
return 0f;
}
public void PlayExpression(string expressionName)
{
FacialExpression expression = Array.Find(facialExpressions, expr => expr.name == expressionName);
if (expression != null)
{
StartCoroutine(PlayExpressionCoroutine(expression));
}
else
{
Debug.LogWarning($"RPMFacialAnimator: Expression '{expressionName}' not found.");
}
}
public void PlayExpression(int expressionIndex)
{
if (expressionIndex >= 0 && expressionIndex < facialExpressions.Length)
{
StartCoroutine(PlayExpressionCoroutine(facialExpressions[expressionIndex]));
}
else
{
Debug.LogWarning($"RPMFacialAnimator: Expression index {expressionIndex} is out of range.");
}
}
IEnumerator PlayExpressionCoroutine(FacialExpression expression)
{
isAnimating = true;
// Store starting weights
Dictionary<string, float> startWeights = new Dictionary<string, float>();
foreach (var blendWeight in expression.blendShapeWeights)
{
startWeights[blendWeight.blendShapeName] = GetBlendShapeWeight(blendWeight.blendShapeName);
}
// Animate to target weights
float elapsedTime = 0f;
while (elapsedTime < expression.duration)
{
float progress = elapsedTime / expression.duration;
float curveValue = transitionCurve.Evaluate(progress);
foreach (var blendWeight in expression.blendShapeWeights)
{
float startWeight = startWeights.ContainsKey(blendWeight.blendShapeName) ?
startWeights[blendWeight.blendShapeName] : 0f;
float targetWeight = blendWeight.weight;
float currentWeight = Mathf.Lerp(startWeight, targetWeight, curveValue);
SetBlendShapeWeight(blendWeight.blendShapeName, currentWeight);
}
elapsedTime += Time.deltaTime;
yield return null;
}
// Hold the expression
if (expression.holdTime > 0f)
{
yield return new WaitForSeconds(expression.holdTime);
}
// Return to neutral
elapsedTime = 0f;
while (elapsedTime < expression.duration)
{
float progress = elapsedTime / expression.duration;
float curveValue = transitionCurve.Evaluate(progress);
foreach (var blendWeight in expression.blendShapeWeights)
{
float currentWeight = Mathf.Lerp(blendWeight.weight, 0f, curveValue);
SetBlendShapeWeight(blendWeight.blendShapeName, currentWeight);
}
elapsedTime += Time.deltaTime;
yield return null;
}
// Ensure all weights are reset to 0
foreach (var blendWeight in expression.blendShapeWeights)
{
SetBlendShapeWeight(blendWeight.blendShapeName, 0f);
}
isAnimating = false;
}
public void StartExpressionAnimation()
{
if (expressionCoroutine != null)
{
StopCoroutine(expressionCoroutine);
}
expressionCoroutine = StartCoroutine(ExpressionAnimationLoop());
}
public void StopExpressionAnimation()
{
if (expressionCoroutine != null)
{
StopCoroutine(expressionCoroutine);
expressionCoroutine = null;
}
}
IEnumerator ExpressionAnimationLoop()
{
while (loopAnimations && !enableManualControls)
{
if (facialExpressions.Length > 0 && !isAnimating)
{
int randomIndex = UnityEngine.Random.Range(0, facialExpressions.Length);
yield return StartCoroutine(PlayExpressionCoroutine(facialExpressions[randomIndex]));
}
yield return new WaitForSeconds(timeBetweenExpressions);
}
}
public void StartAutoBlinking()
{
if (blinkCoroutine != null)
{
StopCoroutine(blinkCoroutine);
}
blinkCoroutine = StartCoroutine(AutoBlinkLoop());
}
public void StopAutoBlinking()
{
if (blinkCoroutine != null)
{
StopCoroutine(blinkCoroutine);
blinkCoroutine = null;
}
}
IEnumerator AutoBlinkLoop()
{
while (enableAutoBlink)
{
// Wait for random interval
float waitTime = blinkInterval + UnityEngine.Random.Range(-blinkRandomness, blinkRandomness);
yield return new WaitForSeconds(Mathf.Max(0.5f, waitTime));
// Blink
yield return StartCoroutine(BlinkCoroutine());
}
}
IEnumerator BlinkCoroutine()
{
// Close eyes
float elapsedTime = 0f;
float halfDuration = blinkDuration * 0.5f;
while (elapsedTime < halfDuration)
{
float progress = elapsedTime / halfDuration;
float blinkWeight = Mathf.Lerp(0f, 1f, progress);
SetBlendShapeWeight("eyeBlinkLeft", blinkWeight);
SetBlendShapeWeight("eyeBlinkRight", blinkWeight);
elapsedTime += Time.deltaTime;
yield return null;
}
// Open eyes
elapsedTime = 0f;
while (elapsedTime < halfDuration)
{
float progress = elapsedTime / halfDuration;
float blinkWeight = Mathf.Lerp(1f, 0f, progress);
SetBlendShapeWeight("eyeBlinkLeft", blinkWeight);
SetBlendShapeWeight("eyeBlinkRight", blinkWeight);
elapsedTime += Time.deltaTime;
yield return null;
}
// Ensure eyes are fully open
SetBlendShapeWeight("eyeBlinkLeft", 0f);
SetBlendShapeWeight("eyeBlinkRight", 0f);
}
public void ResetAllBlendShapes()
{
if (headRenderer == null || blendShapeIndexMap == null) return;
foreach (var kvp in blendShapeIndexMap)
{
headRenderer.SetBlendShapeWeight(kvp.Value, 0f);
}
}
// Public API Methods
public void Smile() => PlayExpression("Happy");
public void Surprise() => PlayExpression("Surprised");
public void Frown() => PlayExpression("Sad");
public void Angry() => PlayExpression("Angry");
public void Think() => PlayExpression("Thinking");
public void SetAvatarRoot(Transform newRoot)
{
avatarRoot = newRoot;
InitializeAnimator();
}
public void SetManualControlMode(bool enabled)
{
enableManualControls = enabled;
if (enabled)
{
// Stop auto-expressions
StopExpressionAnimation();
ResetAllBlendShapes();
}
else if (autoPlay)
{
// Restart auto-expressions
StartExpressionAnimation();
}
}
void OnValidate()
{
// Update manual controls in real-time during edit mode
if (Application.isPlaying && enableManualControls)
{
// Stop auto-expressions when manual controls are enabled
if (expressionCoroutine != null)
{
StopCoroutine(expressionCoroutine);
expressionCoroutine = null;
}
// Reset any ongoing animations
isAnimating = false;
ApplyManualControls();
}
else if (Application.isPlaying && autoPlay && !enableManualControls)
{
// Restart auto-expressions if manual controls are disabled
if (expressionCoroutine == null)
{
StartExpressionAnimation();
}
}
}
}

View File

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

View File

@ -8,10 +8,6 @@ using UnityEngine;
public class UDPAvatarBroadcaster : 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")]
@ -42,6 +38,8 @@ public class UDPAvatarBroadcaster : MonoBehaviour
private UdpClient udpClient;
private IPEndPoint broadcastEndPoint;
private string broadcastAddress;
private int broadcastPort;
private Dictionary<string, Transform> boneCache;
private List<Transform> allBones; // For full data mode
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
@ -51,6 +49,9 @@ public class UDPAvatarBroadcaster : MonoBehaviour
private int currentBoneCount;
private int currentBlendShapeCount;
// Magic number for packet identification
private const uint AVATAR_MAGIC = 0xC0A0;
// Avatar data structure - supports both optimized and full data modes
private struct CompactAvatarData
{
@ -85,16 +86,20 @@ public class UDPAvatarBroadcaster : MonoBehaviour
void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
broadcastAddress = cfg.ipAddress;
broadcastPort = cfg.port;
}
broadcastAddress = cfg.ipAddress;
broadcastPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
broadcastAddress = "255.255.255.255";
broadcastPort = 1221;
}
InitializeNetworking();
CacheAvatarComponents();
@ -379,6 +384,9 @@ public class UDPAvatarBroadcaster : MonoBehaviour
using (MemoryStream stream = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
// Magic number for packet identification
writer.Write(AVATAR_MAGIC);
// Header
writer.Write(data.playerID);
writer.Write(data.sequenceNumber);

View File

@ -11,10 +11,6 @@ using UnityEngine;
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")]
@ -45,6 +41,8 @@ public class UDPAvatarBroadcasterAgent : MonoBehaviour
private UdpClient udpClient;
private IPEndPoint broadcastEndPoint;
private string broadcastAddress;
private int broadcastPort;
private Dictionary<string, Transform> boneCache;
private List<Transform> allBones; // For full data mode
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
@ -54,6 +52,9 @@ public class UDPAvatarBroadcasterAgent : MonoBehaviour
private int currentBoneCount;
private int currentBlendShapeCount;
// Magic number for packet identification
private const uint AVATAR_MAGIC = 0xC0A0;
// Avatar data structure - supports both optimized and full data modes
private struct CompactAvatarData
{
@ -88,16 +89,20 @@ public class UDPAvatarBroadcasterAgent : MonoBehaviour
void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
broadcastAddress = cfg.ipAddress;
broadcastPort = cfg.port;
}
broadcastAddress = cfg.ipAddress;
broadcastPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
broadcastAddress = "255.255.255.255";
broadcastPort = 1221;
}
InitializeNetworking();
CacheAvatarComponents();
@ -382,6 +387,9 @@ public class UDPAvatarBroadcasterAgent : MonoBehaviour
using (MemoryStream stream = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
// Magic number for packet identification
writer.Write(AVATAR_MAGIC);
// Header
writer.Write(data.playerID);
writer.Write(data.sequenceNumber);

View File

@ -10,9 +10,6 @@ using UnityEngine;
public class UDPAvatarReceiver : 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 = 2; // Which player to receive data from (0 = any)
[SerializeField] private bool allowPortSharing = true; // For local testing with multiple components
@ -44,6 +41,7 @@ public class UDPAvatarReceiver : MonoBehaviour
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
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
@ -60,6 +58,9 @@ public class UDPAvatarReceiver : MonoBehaviour
private int packetsDropped = 0;
private float lastPacketTime = 0f;
// Magic number for packet identification
private const uint AVATAR_MAGIC = 0xC0A0;
// Avatar data structure - supports both optimized and full data modes (matches broadcaster)
private struct CompactAvatarData
{
@ -87,14 +88,16 @@ public class UDPAvatarReceiver : MonoBehaviour
if (targetAvatarRoot == null)
targetAvatarRoot = transform;
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
listenPort = cfg.port;
}
listenPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
listenPort = 1221;
}
CacheAvatarComponents();
@ -323,52 +326,16 @@ public class UDPAvatarReceiver : MonoBehaviour
{
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
// Check minimum size for magic number
if (data.Length < 4)
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;
// Check for avatar magic number (0xC0A0)
uint magic = BitConverter.ToUInt32(data, 0);
return magic == AVATAR_MAGIC;
}
catch
{
// If any parsing fails, assume it's not valid avatar data
return false;
}
}
@ -378,6 +345,14 @@ public class UDPAvatarReceiver : MonoBehaviour
using (MemoryStream stream = new MemoryStream(data))
using (BinaryReader reader = new BinaryReader(stream))
{
// Read and validate magic number
uint magic = reader.ReadUInt32();
if (magic != AVATAR_MAGIC)
{
Debug.LogWarning($"Invalid avatar packet magic number: 0x{magic:X} (expected 0x{AVATAR_MAGIC:X})");
return default;
}
CompactAvatarData result = new CompactAvatarData
{
// Header

View File

@ -13,9 +13,6 @@ using UnityEngine;
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
@ -47,6 +44,7 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
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
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
@ -63,6 +61,9 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
private int packetsDropped = 0;
private float lastPacketTime = 0f;
// Magic number for packet identification
private const uint AVATAR_MAGIC = 0xC0A0;
// Avatar data structure - supports both optimized and full data modes (matches broadcaster)
private struct CompactAvatarData
{
@ -90,14 +91,16 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
if (targetAvatarRoot == null)
targetAvatarRoot = transform;
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
listenPort = cfg.port;
}
listenPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
listenPort = 1221;
}
CacheAvatarComponents();
@ -326,52 +329,16 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
{
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
// Check minimum size for magic number
if (data.Length < 4)
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;
// Check for avatar magic number (0xC0A0)
uint magic = BitConverter.ToUInt32(data, 0);
return magic == AVATAR_MAGIC;
}
catch
{
// If any parsing fails, assume it's not valid avatar data
return false;
}
}
@ -381,6 +348,14 @@ public class UDPAvatarReceiverAgent : MonoBehaviour
using (MemoryStream stream = new MemoryStream(data))
using (BinaryReader reader = new BinaryReader(stream))
{
// Read and validate magic number
uint magic = reader.ReadUInt32();
if (magic != AVATAR_MAGIC)
{
Debug.LogWarning($"Invalid avatar packet magic number: 0x{magic:X} (expected 0x{AVATAR_MAGIC:X})");
return default;
}
CompactAvatarData result = new CompactAvatarData
{
// Header

View File

@ -16,10 +16,7 @@ using Newtonsoft.Json;
public class VRExperimentController : MonoBehaviour
{
[Header("Network Settings")]
[SerializeField] private int udpPort = 1221;
[SerializeField] private bool allowPortSharing = true; // For local testing with multiple components
[SerializeField] private bool useGlobalNetworkConfig = true;
[SerializeField] private NetworkConfig networkConfigAsset;
[Header("Avatar Assignments")]
[SerializeField] private GameObject helpfulAvatar;
@ -40,6 +37,7 @@ public class VRExperimentController : MonoBehaviour
private UdpClient udpClient;
private Thread udpListenerThread;
private bool isListening = false;
private int udpPort;
// Current experiment state
private string currentConditionType = "";
@ -57,15 +55,18 @@ public class VRExperimentController : MonoBehaviour
void Start()
{
// Apply global config if enabled
if (useGlobalNetworkConfig)
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
var cfg = networkConfigAsset != null ? networkConfigAsset : NetworkConfig.Instance;
if (cfg != null)
{
udpPort = cfg.port;
}
udpPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
udpPort = 1221;
}
InitializeObjectMaps();
StartUDPListener();

View File

@ -1,384 +0,0 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"html/template"
"log"
"net"
"net/http"
"sync"
"time"
)
// Vector3 represents a 3D vector
type Vector3 struct {
X, Y, Z float32
}
// Quaternion represents a rotation quaternion
type Quaternion struct {
X, Y, Z, W float32
}
// Transform represents a 3D transform
type Transform struct {
WorldPosition Vector3
WorldRotation Quaternion
LocalScale Vector3
}
// BoneData represents bone transformation data
type BoneData struct {
BoneIndex int32
Transform Transform
}
// BlendShapeData represents blend shape data
type BlendShapeData struct {
ShapeIndex int32
Weight float32
}
// AvatarData represents the complete avatar data structure
type AvatarData struct {
RootTransform Transform
Timestamp float64
BoneCount int32
Bones []BoneData
BlendCount int32
BlendShapes []BlendShapeData
ServerTime float64
}
// AvatarSyncServer handles the avatar synchronization
type AvatarSyncServer struct {
playerData map[string]*AvatarData
mutex sync.RWMutex
}
// NewAvatarSyncServer creates a new server instance
func NewAvatarSyncServer() *AvatarSyncServer {
return &AvatarSyncServer{
playerData: map[string]*AvatarData{
"player1": nil,
"player2": nil,
},
}
}
// SerializeAvatarData converts AvatarData to binary format
func (a *AvatarData) SerializeAvatarData() ([]byte, error) {
buf := new(bytes.Buffer)
// Write root transform
if err := binary.Write(buf, binary.LittleEndian, a.RootTransform); err != nil {
return nil, err
}
// Write timestamp
if err := binary.Write(buf, binary.LittleEndian, a.Timestamp); err != nil {
return nil, err
}
// Write server time
if err := binary.Write(buf, binary.LittleEndian, a.ServerTime); err != nil {
return nil, err
}
// Write bone count
if err := binary.Write(buf, binary.LittleEndian, a.BoneCount); err != nil {
return nil, err
}
// Write bones
for _, bone := range a.Bones {
if err := binary.Write(buf, binary.LittleEndian, bone); err != nil {
return nil, err
}
}
// Write blend shape count
if err := binary.Write(buf, binary.LittleEndian, a.BlendCount); err != nil {
return nil, err
}
// Write blend shapes
for _, blend := range a.BlendShapes {
if err := binary.Write(buf, binary.LittleEndian, blend); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// DeserializeAvatarData converts binary data back to AvatarData
func DeserializeAvatarData(data []byte) (*AvatarData, error) {
buf := bytes.NewReader(data)
avatar := &AvatarData{}
// Read root transform
if err := binary.Read(buf, binary.LittleEndian, &avatar.RootTransform); err != nil {
return nil, err
}
// Read timestamp
if err := binary.Read(buf, binary.LittleEndian, &avatar.Timestamp); err != nil {
return nil, err
}
// Read server time
if err := binary.Read(buf, binary.LittleEndian, &avatar.ServerTime); err != nil {
return nil, err
}
// Read bone count
if err := binary.Read(buf, binary.LittleEndian, &avatar.BoneCount); err != nil {
return nil, err
}
// Read bones
avatar.Bones = make([]BoneData, avatar.BoneCount)
for i := int32(0); i < avatar.BoneCount; i++ {
if err := binary.Read(buf, binary.LittleEndian, &avatar.Bones[i]); err != nil {
return nil, err
}
}
// Read blend shape count
if err := binary.Read(buf, binary.LittleEndian, &avatar.BlendCount); err != nil {
return nil, err
}
// Read blend shapes
avatar.BlendShapes = make([]BlendShapeData, avatar.BlendCount)
for i := int32(0); i < avatar.BlendCount; i++ {
if err := binary.Read(buf, binary.LittleEndian, &avatar.BlendShapes[i]); err != nil {
return nil, err
}
}
return avatar, nil
}
// setCORSHeaders sets CORS headers for cross-origin requests
func setCORSHeaders(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}
// handleRoot serves the status page
func (s *AvatarSyncServer) handleRoot(w http.ResponseWriter, r *http.Request) {
setCORSHeaders(w)
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Content-Type", "text/html")
s.mutex.RLock()
player1Connected := s.playerData["player1"] != nil
player2Connected := s.playerData["player2"] != nil
s.mutex.RUnlock()
statusTemplate := `
<html>
<head><title>Avatar Sync Server (Go Binary)</title></head>
<body>
<h1>Avatar Sync Server (Go Binary)</h1>
<p>Server is running and using binary protocol for improved performance</p>
<h2>Available Endpoints:</h2>
<ul>
<li>/player1 - Player 1 data (binary)</li>
<li>/player2 - Player 2 data (binary)</li>
<li>/status - Server status</li>
</ul>
<h2>Player Status:</h2>
<ul>
<li>Player 1: {{if .Player1}}Connected{{else}}No data{{end}}</li>
<li>Player 2: {{if .Player2}}Connected{{else}}No data{{end}}</li>
</ul>
<p><strong>Note:</strong> This server uses binary protocol instead of JSON for faster data transfer.</p>
</body>
</html>
`
tmpl := template.Must(template.New("status").Parse(statusTemplate))
data := struct {
Player1 bool
Player2 bool
}{
Player1: player1Connected,
Player2: player2Connected,
}
tmpl.Execute(w, data)
}
// handleStatus provides server status information
func (s *AvatarSyncServer) handleStatus(w http.ResponseWriter, r *http.Request) {
setCORSHeaders(w)
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Content-Type", "text/plain")
s.mutex.RLock()
defer s.mutex.RUnlock()
currentTime := time.Now().Unix()
status := fmt.Sprintf("Avatar Sync Server (Go Binary)\n")
status += fmt.Sprintf("Server Time: %d\n", currentTime)
status += fmt.Sprintf("Protocol: Binary (Little Endian)\n")
status += fmt.Sprintf("Player 1: %s\n", func() string {
if s.playerData["player1"] != nil {
return fmt.Sprintf("Connected (last update: %.2f)", s.playerData["player1"].Timestamp)
}
return "No data"
}())
status += fmt.Sprintf("Player 2: %s\n", func() string {
if s.playerData["player2"] != nil {
return fmt.Sprintf("Connected (last update: %.2f)", s.playerData["player2"].Timestamp)
}
return "No data"
}())
w.Write([]byte(status))
}
// handlePlayer handles both GET and POST requests for player data
func (s *AvatarSyncServer) handlePlayer(w http.ResponseWriter, r *http.Request, playerID string) {
setCORSHeaders(w)
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
switch r.Method {
case "GET":
s.mutex.RLock()
playerData := s.playerData[playerID]
s.mutex.RUnlock()
w.Header().Set("Content-Type", "application/octet-stream")
if playerData != nil {
// Update server timestamp before sending
playerData.ServerTime = float64(time.Now().UnixNano()) / 1e9
data, err := playerData.SerializeAvatarData()
if err != nil {
http.Error(w, "Failed to serialize data", http.StatusInternalServerError)
return
}
w.Write(data)
} else {
// Return empty avatar data structure
emptyAvatar := &AvatarData{
RootTransform: Transform{
WorldPosition: Vector3{X: 0.0, Y: 0.0, Z: 0.0},
WorldRotation: Quaternion{X: 0.0, Y: 0.0, Z: 0.0, W: 1.0},
LocalScale: Vector3{X: 1.0, Y: 1.0, Z: 1.0},
},
Timestamp: 0.0,
BoneCount: 0,
Bones: []BoneData{},
BlendCount: 0,
BlendShapes: []BlendShapeData{},
ServerTime: float64(time.Now().UnixNano()) / 1e9,
}
data, err := emptyAvatar.SerializeAvatarData()
if err != nil {
http.Error(w, "Failed to serialize empty data", http.StatusInternalServerError)
return
}
w.Write(data)
}
case "POST":
// Read binary data from request body
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
// Deserialize avatar data
avatarData, err := DeserializeAvatarData(buf.Bytes())
if err != nil {
http.Error(w, "Failed to deserialize avatar data", http.StatusBadRequest)
return
}
// Store the avatar data
avatarData.ServerTime = float64(time.Now().UnixNano()) / 1e9
s.mutex.Lock()
s.playerData[playerID] = avatarData
s.mutex.Unlock()
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Data updated for %s at %.2f", playerID, avatarData.ServerTime)))
log.Printf("Updated data for %s at %s", playerID, time.Now().Format("15:04:05"))
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// getLocalIP returns the local IP address
func getLocalIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return "127.0.0.1"
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP.String()
}
func main() {
server := NewAvatarSyncServer()
// Set up routes
http.HandleFunc("/", server.handleRoot)
http.HandleFunc("/status", server.handleStatus)
http.HandleFunc("/player1", func(w http.ResponseWriter, r *http.Request) {
server.handlePlayer(w, r, "player1")
})
http.HandleFunc("/player2", func(w http.ResponseWriter, r *http.Request) {
server.handlePlayer(w, r, "player2")
})
port := ":8080"
localIP := getLocalIP()
fmt.Println("Avatar Sync Server (Go Binary) starting...")
fmt.Printf("Server running on:\n")
fmt.Printf(" Local: http://127.0.0.1%s\n", port)
fmt.Printf(" Network: http://%s%s\n", localIP, port)
fmt.Printf(" External: http://0.0.0.0%s\n", port)
fmt.Println()
fmt.Println("Available endpoints:")
fmt.Println(" GET /player1 - Get Player 1 avatar data (binary)")
fmt.Println(" GET /player2 - Get Player 2 avatar data (binary)")
fmt.Println(" POST /player1 - Update Player 1 avatar data (binary)")
fmt.Println(" POST /player2 - Update Player 2 avatar data (binary)")
fmt.Println(" GET /status - Server status")
fmt.Println()
fmt.Println("Protocol: Binary (Little Endian) for improved performance")
fmt.Println("Press Ctrl+C to stop the server...")
log.Fatal(http.ListenAndServe(port, nil))
}

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 25481c7177f020c44810e18827dd20a2
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Binary file not shown.