diff --git a/Unity-Master/Assets/Scripts/AvatarDataDownloader.cs b/Unity-Master/Assets/Scripts/AvatarDataDownloader.cs deleted file mode 100644 index 4c5a8f0..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataDownloader.cs +++ /dev/null @@ -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(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(); - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/AvatarDataDownloader.cs.meta b/Unity-Master/Assets/Scripts/AvatarDataDownloader.cs.meta deleted file mode 100644 index 63999eb..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataDownloader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e4a8523fc2492f848b5b20e8f378752e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/AvatarDataReader.cs b/Unity-Master/Assets/Scripts/AvatarDataReader.cs deleted file mode 100644 index c569d36..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataReader.cs +++ /dev/null @@ -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 boneLookup; - private Dictionary 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(); - skinnedMeshLookup = new Dictionary(); - - // Get all skinned mesh renderers - SkinnedMeshRenderer[] smrs = targetAvatarRoot.GetComponentsInChildren(); - - // 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(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 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 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; - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/AvatarDataReader.cs.meta b/Unity-Master/Assets/Scripts/AvatarDataReader.cs.meta deleted file mode 100644 index 57e5348..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataReader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 81f65cbb675480c4ca726737a78179e9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/AvatarDataUploader.cs b/Unity-Master/Assets/Scripts/AvatarDataUploader.cs deleted file mode 100644 index 7baf3b0..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataUploader.cs +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/AvatarDataUploader.cs.meta b/Unity-Master/Assets/Scripts/AvatarDataUploader.cs.meta deleted file mode 100644 index 6f60dfd..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataUploader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6f4c0a9d64416544891f7b7501031b0e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/AvatarDataWriter.cs b/Unity-Master/Assets/Scripts/AvatarDataWriter.cs deleted file mode 100644 index e3ee548..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataWriter.cs +++ /dev/null @@ -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 bones; - public List 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 allBones; - private List 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(); - skinnedMeshRenderers = new List(); - - // Get all skinned mesh renderers - skinnedMeshRenderers.AddRange(avatarRoot.GetComponentsInChildren()); - - // Get all unique bones from all SkinnedMeshRenderers - HashSet uniqueBones = new HashSet(); - 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(), - blendShapes = new List(), - 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(); - } - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/AvatarDataWriter.cs.meta b/Unity-Master/Assets/Scripts/AvatarDataWriter.cs.meta deleted file mode 100644 index fa99372..0000000 --- a/Unity-Master/Assets/Scripts/AvatarDataWriter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 029bf22d15ab7214484688dd969f7d28 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/AvatarSyncClient.cs b/Unity-Master/Assets/Scripts/AvatarSyncClient.cs deleted file mode 100644 index f758981..0000000 --- a/Unity-Master/Assets/Scripts/AvatarSyncClient.cs +++ /dev/null @@ -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>(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(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(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(); - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/AvatarSyncClient.cs.meta b/Unity-Master/Assets/Scripts/AvatarSyncClient.cs.meta deleted file mode 100644 index 9287608..0000000 --- a/Unity-Master/Assets/Scripts/AvatarSyncClient.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4724255e4e0e6f04e8d53c6ad4fb7ca1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/AvatarSyncComparison.cs b/Unity-Master/Assets/Scripts/AvatarSyncComparison.cs deleted file mode 100644 index 41b26ce..0000000 --- a/Unity-Master/Assets/Scripts/AvatarSyncComparison.cs +++ /dev/null @@ -1,611 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -using UnityEngine; -using Newtonsoft.Json; - -/// -/// 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 -/// -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(); - } - - // 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(); - HashSet uniqueBones = new HashSet(); - - 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); - } - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/AvatarSyncComparison.cs.meta b/Unity-Master/Assets/Scripts/AvatarSyncComparison.cs.meta deleted file mode 100644 index a9978b5..0000000 --- a/Unity-Master/Assets/Scripts/AvatarSyncComparison.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 44ddcc99fb37a434296ae65e9c186f43 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/LocalTestingSetup.md b/Unity-Master/Assets/Scripts/LocalTestingSetup.md deleted file mode 100644 index 441d426..0000000 --- a/Unity-Master/Assets/Scripts/LocalTestingSetup.md +++ /dev/null @@ -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! \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/LocalTestingSetup.md.meta b/Unity-Master/Assets/Scripts/LocalTestingSetup.md.meta deleted file mode 100644 index 0320893..0000000 --- a/Unity-Master/Assets/Scripts/LocalTestingSetup.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: d6945fe49cfac414eb744e574611562c -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs index 24bddd6..14bce23 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioReceiver.cs @@ -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(); diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs index af82628..910f0a1 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiSimpleUDPAudioSender.cs @@ -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; diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs index a7f1045..d47a4e7 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechReceiver.cs @@ -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(); } diff --git a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs index 49f3a82..759077a 100644 --- a/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs +++ b/Unity-Master/Assets/Scripts/Multiplayer/ConvaiUDPSpeechSender.cs @@ -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(); } diff --git a/Unity-Master/Assets/Scripts/NetworkConfig.asset b/Unity-Master/Assets/Scripts/NetworkConfig.asset index 66df588..1e5c4a8 100644 --- a/Unity-Master/Assets/Scripts/NetworkConfig.asset +++ b/Unity-Master/Assets/Scripts/NetworkConfig.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:397abf447ea7157bbda652c7c87d6d0527f4af64306f1200fc969c0c6782acd2 -size 505 +oid sha256:17852fdd649ea85c2e4ac65f7d4426459d7466e27b5a807f5663811bee8b3c6b +size 443 diff --git a/Unity-Master/Assets/Scripts/NetworkConfig.cs b/Unity-Master/Assets/Scripts/NetworkConfig.cs index f9ff0dd..8b35c98 100644 --- a/Unity-Master/Assets/Scripts/NetworkConfig.cs +++ b/Unity-Master/Assets/Scripts/NetworkConfig.cs @@ -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 diff --git a/Unity-Master/Assets/Scripts/README_AvatarSync.md b/Unity-Master/Assets/Scripts/README_AvatarSync.md deleted file mode 100644 index f2b8439..0000000 --- a/Unity-Master/Assets/Scripts/README_AvatarSync.md +++ /dev/null @@ -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! \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/README_AvatarSync.md.meta b/Unity-Master/Assets/Scripts/README_AvatarSync.md.meta deleted file mode 100644 index 2d0acfc..0000000 --- a/Unity-Master/Assets/Scripts/README_AvatarSync.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: d5cfc93a5dbeee345a3993c27049b134 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md b/Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md deleted file mode 100644 index 1c07ff3..0000000 --- a/Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md +++ /dev/null @@ -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! \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md.meta b/Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md.meta deleted file mode 100644 index 64b7bf5..0000000 --- a/Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 7a81e1eb398089c45a8cf945c42c940c -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/RPMFacialAnimator.cs b/Unity-Master/Assets/Scripts/RPMFacialAnimator.cs deleted file mode 100644 index ee6f9b6..0000000 --- a/Unity-Master/Assets/Scripts/RPMFacialAnimator.cs +++ /dev/null @@ -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 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(); - - 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(); - - 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 startWeights = new Dictionary(); - 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(); - } - } - } -} \ No newline at end of file diff --git a/Unity-Master/Assets/Scripts/RPMFacialAnimator.cs.meta b/Unity-Master/Assets/Scripts/RPMFacialAnimator.cs.meta deleted file mode 100644 index c2d4695..0000000 --- a/Unity-Master/Assets/Scripts/RPMFacialAnimator.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f8b4d07caa16f0e4b81188eb3dc971c6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs b/Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs index b89d605..7343f97 100644 --- a/Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs +++ b/Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs @@ -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 boneCache; private List allBones; // For full data mode private List 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); diff --git a/Unity-Master/Assets/Scripts/UDPAvatarBroadcasterAgent.cs b/Unity-Master/Assets/Scripts/UDPAvatarBroadcasterAgent.cs index f1d86bc..eef2dad 100644 --- a/Unity-Master/Assets/Scripts/UDPAvatarBroadcasterAgent.cs +++ b/Unity-Master/Assets/Scripts/UDPAvatarBroadcasterAgent.cs @@ -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 boneCache; private List allBones; // For full data mode private List 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); diff --git a/Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs b/Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs index 17a73aa..2ed9623 100644 --- a/Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs +++ b/Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs @@ -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 boneCache; private List allBones; // For full data mode private List 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 diff --git a/Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs b/Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs index 0bf8d6c..0a32ade 100644 --- a/Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs +++ b/Unity-Master/Assets/Scripts/UDPAvatarReceiverAgent.cs @@ -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 boneCache; private List allBones; // For full data mode private List 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 diff --git a/Unity-Master/Assets/Scripts/VRExperimentController.cs b/Unity-Master/Assets/Scripts/VRExperimentController.cs index 47a2198..bbc6020 100644 --- a/Unity-Master/Assets/Scripts/VRExperimentController.cs +++ b/Unity-Master/Assets/Scripts/VRExperimentController.cs @@ -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(); diff --git a/Unity-Master/Assets/avatar_sync_server.go b/Unity-Master/Assets/avatar_sync_server.go deleted file mode 100644 index ea8a122..0000000 --- a/Unity-Master/Assets/avatar_sync_server.go +++ /dev/null @@ -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 := ` - - Avatar Sync Server (Go Binary) - -

Avatar Sync Server (Go Binary)

-

Server is running and using binary protocol for improved performance

-

Available Endpoints:

-
    -
  • /player1 - Player 1 data (binary)
  • -
  • /player2 - Player 2 data (binary)
  • -
  • /status - Server status
  • -
-

Player Status:

-
    -
  • Player 1: {{if .Player1}}Connected{{else}}No data{{end}}
  • -
  • Player 2: {{if .Player2}}Connected{{else}}No data{{end}}
  • -
-

Note: This server uses binary protocol instead of JSON for faster data transfer.

- - - ` - - 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)) -} \ No newline at end of file diff --git a/Unity-Master/Assets/avatar_sync_server.go.meta b/Unity-Master/Assets/avatar_sync_server.go.meta deleted file mode 100644 index fcb0945..0000000 --- a/Unity-Master/Assets/avatar_sync_server.go.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 25481c7177f020c44810e18827dd20a2 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Unity-Master/ProjectSettings/EditorBuildSettings.asset b/Unity-Master/ProjectSettings/EditorBuildSettings.asset index 290a860..fb169eb 100644 --- a/Unity-Master/ProjectSettings/EditorBuildSettings.asset +++ b/Unity-Master/ProjectSettings/EditorBuildSettings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57814a7518569d818e2d6fd8ddb8c5543ec9a81bdd1b5c6266f4d9f5ea9eaf31 +oid sha256:1ae56a0aa5e5fcd905f8d677138aa623b5c695c0f95a066ae70847afb086ef01 size 1111 diff --git a/Unity-Master/ProjectSettings/ProjectSettings.asset b/Unity-Master/ProjectSettings/ProjectSettings.asset index 27c6384..5687f99 100644 --- a/Unity-Master/ProjectSettings/ProjectSettings.asset +++ b/Unity-Master/ProjectSettings/ProjectSettings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f63de02c7ce6fb66ff27a5e91d8d6ae745a9a917ca996a6d006f21fb10b3269c -size 24640 +oid sha256:e3021b170955d24126c440f68eacf051f182187e316381b898403f6bba7b1271 +size 24723