initial upload
This commit is contained in:
307
Unity-Master/Assets/Scripts/AvatarDataDownloader.cs
Normal file
307
Unity-Master/Assets/Scripts/AvatarDataDownloader.cs
Normal file
@ -0,0 +1,307 @@
|
||||
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 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()
|
||||
{
|
||||
// Set up file paths
|
||||
syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
|
||||
if (!Directory.Exists(syncFilesPath))
|
||||
{
|
||||
Directory.CreateDirectory(syncFilesPath);
|
||||
}
|
||||
|
||||
// Determine output file name
|
||||
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
|
||||
outputFilePath = Path.Combine(syncFilesPath, fileName);
|
||||
|
||||
Debug.Log($"Avatar Data Downloader initialized");
|
||||
Debug.Log($"Server: {ServerUrl}");
|
||||
Debug.Log($"Target Player: {targetPlayer}");
|
||||
Debug.Log($"Output File: {outputFilePath}");
|
||||
|
||||
// Start downloading if auto-download is enabled
|
||||
if (autoDownload)
|
||||
{
|
||||
StartDownload();
|
||||
}
|
||||
|
||||
// Test connection
|
||||
StartCoroutine(TestConnection());
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
// Ensure valid values
|
||||
if (downloadInterval < 0.01f)
|
||||
downloadInterval = 0.01f;
|
||||
|
||||
if (serverPort < 1 || serverPort > 65535)
|
||||
serverPort = 8080;
|
||||
}
|
||||
|
||||
public void StartDownload()
|
||||
{
|
||||
if (downloadCoroutine == null)
|
||||
{
|
||||
downloadCoroutine = StartCoroutine(DownloadLoop());
|
||||
Debug.Log("Avatar download started");
|
||||
}
|
||||
}
|
||||
|
||||
public void StopDownload()
|
||||
{
|
||||
if (downloadCoroutine != null)
|
||||
{
|
||||
StopCoroutine(downloadCoroutine);
|
||||
downloadCoroutine = null;
|
||||
Debug.Log("Avatar download stopped");
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator TestConnection()
|
||||
{
|
||||
string url = $"{ServerUrl}/status";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 5; // 5 second timeout for connection test
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
isConnected = true;
|
||||
if (showConnectionStatus)
|
||||
{
|
||||
Debug.Log($"Successfully connected to avatar sync server at {ServerUrl}");
|
||||
}
|
||||
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log($"Server status: {request.downloadHandler.text}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isConnected = false;
|
||||
if (showConnectionStatus)
|
||||
{
|
||||
Debug.LogWarning($"Failed to connect to avatar sync server at {ServerUrl}: {request.error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator DownloadLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return StartCoroutine(FetchPlayerData());
|
||||
yield return new WaitForSeconds(downloadInterval);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator FetchPlayerData()
|
||||
{
|
||||
string url = $"{ServerUrl}/{targetPlayer}";
|
||||
totalDownloads++;
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 10; // 10 second timeout
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
// Try to validate JSON structure
|
||||
bool isValidData = false;
|
||||
float dataTimestamp = 0f;
|
||||
|
||||
try
|
||||
{
|
||||
var avatarData = JsonConvert.DeserializeObject<AvatarSyncData>(request.downloadHandler.text);
|
||||
isValidData = avatarData != null;
|
||||
dataTimestamp = avatarData?.timestamp ?? 0f;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Error parsing avatar data for {targetPlayer}: {e.Message}");
|
||||
isValidData = false;
|
||||
}
|
||||
|
||||
if (isValidData)
|
||||
{
|
||||
// Write to local file
|
||||
File.WriteAllText(outputFilePath, request.downloadHandler.text);
|
||||
|
||||
successfulDownloads++;
|
||||
lastSuccessfulDownload = Time.time;
|
||||
isConnected = true;
|
||||
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log($"Successfully fetched and saved data for {targetPlayer}. Timestamp: {dataTimestamp}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Received invalid avatar data for {targetPlayer}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isConnected = false;
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Failed to fetch data for {targetPlayer}: {request.error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods for runtime control
|
||||
public void SetServerAddress(string host, int port)
|
||||
{
|
||||
serverHost = host;
|
||||
serverPort = port;
|
||||
|
||||
// Test new connection
|
||||
StartCoroutine(TestConnection());
|
||||
}
|
||||
|
||||
public void SetTargetPlayer(string player)
|
||||
{
|
||||
targetPlayer = player;
|
||||
|
||||
// Update output file path
|
||||
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
|
||||
outputFilePath = Path.Combine(syncFilesPath, fileName);
|
||||
}
|
||||
|
||||
public void ManualDownload()
|
||||
{
|
||||
if (gameObject.activeInHierarchy)
|
||||
{
|
||||
StartCoroutine(FetchPlayerData());
|
||||
}
|
||||
}
|
||||
|
||||
// Get current stats for UI display
|
||||
public string GetDownloadStats()
|
||||
{
|
||||
return $"Connected: {isConnected}\n" +
|
||||
$"Success Rate: {(SuccessRate * 100):F1}%\n" +
|
||||
$"Total Downloads: {totalDownloads}\n" +
|
||||
$"Last Download: {(Time.time - lastSuccessfulDownload):F1}s ago";
|
||||
}
|
||||
|
||||
// Check if the output file exists and has recent data
|
||||
public bool HasRecentData(float maxAgeSeconds = 5f)
|
||||
{
|
||||
if (!File.Exists(outputFilePath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(outputFilePath);
|
||||
return (DateTime.Now - fileInfo.LastWriteTime).TotalSeconds < maxAgeSeconds;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
StopDownload();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopDownload();
|
||||
}
|
||||
|
||||
// GUI for runtime debugging
|
||||
void OnGUI()
|
||||
{
|
||||
if (!enableDebugMode)
|
||||
return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(320, 10, 300, 180));
|
||||
GUILayout.BeginVertical("box");
|
||||
|
||||
GUILayout.Label($"Avatar Downloader - {targetPlayer}");
|
||||
GUILayout.Label($"Server: {ServerUrl}");
|
||||
GUILayout.Label($"Status: {(isConnected ? "Connected" : "Disconnected")}");
|
||||
GUILayout.Label($"Success Rate: {(SuccessRate * 100):F1}%");
|
||||
GUILayout.Label($"Last Download: {(Time.time - lastSuccessfulDownload):F1}s ago");
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Manual Download"))
|
||||
{
|
||||
ManualDownload();
|
||||
}
|
||||
if (GUILayout.Button("Test Connection"))
|
||||
{
|
||||
StartCoroutine(TestConnection());
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (downloadCoroutine == null)
|
||||
{
|
||||
if (GUILayout.Button("Start Auto Download"))
|
||||
{
|
||||
StartDownload();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("Stop Auto Download"))
|
||||
{
|
||||
StopDownload();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/AvatarDataDownloader.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/AvatarDataDownloader.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4a8523fc2492f848b5b20e8f378752e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
366
Unity-Master/Assets/Scripts/AvatarDataReader.cs
Normal file
366
Unity-Master/Assets/Scripts/AvatarDataReader.cs
Normal file
@ -0,0 +1,366 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
public class AvatarDataReader : MonoBehaviour
|
||||
{
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform targetAvatarRoot;
|
||||
[SerializeField] private string fileName = "avatar_sync_data.json";
|
||||
[SerializeField] private bool readEveryFrame = true;
|
||||
[SerializeField] private float updateRate = 60f; // Updates per second
|
||||
[SerializeField] private bool smoothTransitions = true;
|
||||
[SerializeField] private float transitionSpeed = 10f;
|
||||
|
||||
[Header("Synchronization Options")]
|
||||
[SerializeField] private bool syncWorldPosition = true; // Apply avatar world position
|
||||
[SerializeField] private bool syncWorldRotation = true; // Apply avatar world rotation
|
||||
[SerializeField] private bool syncLocalScale = false; // Apply avatar local scale
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool enableDebugMode = false;
|
||||
[SerializeField] private string debugBoneName = "LeftArm"; // Bone to monitor
|
||||
|
||||
private string filePath;
|
||||
private float lastUpdateTime;
|
||||
private Dictionary<string, Transform> boneLookup;
|
||||
private Dictionary<string, SkinnedMeshRenderer> skinnedMeshLookup;
|
||||
private AvatarSyncData lastSyncData;
|
||||
private bool isInitialized = false;
|
||||
private int successfulReads = 0;
|
||||
private int appliedBones = 0;
|
||||
private Vector3 lastAppliedWorldPosition;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Set up file path
|
||||
string syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
|
||||
filePath = Path.Combine(syncFilesPath, fileName);
|
||||
|
||||
// If targetAvatarRoot is not assigned, try to find it automatically
|
||||
if (targetAvatarRoot == null)
|
||||
{
|
||||
targetAvatarRoot = transform;
|
||||
}
|
||||
|
||||
// Cache target avatar components
|
||||
CacheTargetAvatarComponents();
|
||||
|
||||
// Initialize debug tracking
|
||||
if (enableDebugMode)
|
||||
{
|
||||
lastAppliedWorldPosition = targetAvatarRoot.position;
|
||||
}
|
||||
|
||||
Debug.Log($"Avatar Data Reader initialized. Reading from: {filePath}");
|
||||
Debug.Log($"World Position Sync: {syncWorldPosition}, World Rotation Sync: {syncWorldRotation}");
|
||||
}
|
||||
|
||||
void CacheTargetAvatarComponents()
|
||||
{
|
||||
boneLookup = new Dictionary<string, Transform>();
|
||||
skinnedMeshLookup = new Dictionary<string, SkinnedMeshRenderer>();
|
||||
|
||||
// Get all skinned mesh renderers
|
||||
SkinnedMeshRenderer[] smrs = targetAvatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
|
||||
// Cache bones from SkinnedMeshRenderers
|
||||
foreach (SkinnedMeshRenderer smr in smrs)
|
||||
{
|
||||
// Cache this skinned mesh renderer
|
||||
if (!skinnedMeshLookup.ContainsKey(smr.gameObject.name))
|
||||
{
|
||||
skinnedMeshLookup.Add(smr.gameObject.name, smr);
|
||||
}
|
||||
|
||||
// Cache all bones from this SkinnedMeshRenderer
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null && !boneLookup.ContainsKey(bone.name))
|
||||
{
|
||||
boneLookup.Add(bone.name, bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"Cached {boneLookup.Count} bones and {skinnedMeshLookup.Count} skinned mesh renderers");
|
||||
}
|
||||
|
||||
// Debug mode: Print some bone names and check for the debug bone
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log("Reader - Found bones:");
|
||||
int count = 0;
|
||||
foreach (var kvp in boneLookup)
|
||||
{
|
||||
if (count < 10)
|
||||
{
|
||||
Debug.Log($" Reader Bone {count}: {kvp.Key}");
|
||||
count++;
|
||||
}
|
||||
if (kvp.Key.Contains(debugBoneName))
|
||||
{
|
||||
Debug.Log($"Found debug bone in reader: {kvp.Key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!isInitialized || !File.Exists(filePath))
|
||||
return;
|
||||
|
||||
if (readEveryFrame)
|
||||
{
|
||||
ReadAndApplyAvatarData();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rate-limited updates
|
||||
if (Time.time - lastUpdateTime >= 1f / updateRate)
|
||||
{
|
||||
ReadAndApplyAvatarData();
|
||||
lastUpdateTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReadAndApplyAvatarData()
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(filePath);
|
||||
AvatarSyncData syncData = JsonConvert.DeserializeObject<AvatarSyncData>(json);
|
||||
|
||||
if (syncData == null)
|
||||
{
|
||||
if (showDebugInfo)
|
||||
Debug.LogWarning("Failed to deserialize avatar sync data");
|
||||
return;
|
||||
}
|
||||
|
||||
successfulReads++;
|
||||
appliedBones = 0;
|
||||
|
||||
// Apply root transform data
|
||||
ApplyRootTransformData(syncData.rootTransform);
|
||||
|
||||
// Apply bone data
|
||||
ApplyBoneData(syncData.bones);
|
||||
|
||||
// Apply blendshape data
|
||||
ApplyBlendShapeData(syncData.blendShapes);
|
||||
|
||||
if (enableDebugMode && successfulReads % 60 == 0) // Log every 60 reads (about once per second)
|
||||
{
|
||||
Debug.Log($"Reader stats: {successfulReads} successful reads, {appliedBones} bones applied in last read");
|
||||
}
|
||||
|
||||
lastSyncData = syncData;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (showDebugInfo)
|
||||
Debug.LogError($"Error reading avatar data: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyRootTransformData(RootTransformData rootData)
|
||||
{
|
||||
if (rootData == null)
|
||||
return;
|
||||
|
||||
if (syncWorldPosition && rootData.worldPosition != null)
|
||||
{
|
||||
Vector3 targetWorldPosition = rootData.worldPosition.ToVector3();
|
||||
|
||||
// Debug: Log world position changes
|
||||
if (enableDebugMode && Vector3.Distance(targetWorldPosition, lastAppliedWorldPosition) > 0.01f)
|
||||
{
|
||||
Debug.Log($"Applying world position: {targetWorldPosition} (was: {lastAppliedWorldPosition})");
|
||||
lastAppliedWorldPosition = targetWorldPosition;
|
||||
}
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.position = Vector3.Lerp(
|
||||
targetAvatarRoot.position,
|
||||
targetWorldPosition,
|
||||
transitionSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.position = targetWorldPosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncWorldRotation && rootData.worldRotation != null)
|
||||
{
|
||||
Quaternion targetWorldRotation = rootData.worldRotation.ToQuaternion();
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.rotation = Quaternion.Lerp(
|
||||
targetAvatarRoot.rotation,
|
||||
targetWorldRotation,
|
||||
transitionSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.rotation = targetWorldRotation;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncLocalScale && rootData.localScale != null)
|
||||
{
|
||||
Vector3 targetLocalScale = rootData.localScale.ToVector3();
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.localScale = Vector3.Lerp(
|
||||
targetAvatarRoot.localScale,
|
||||
targetLocalScale,
|
||||
transitionSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.localScale = targetLocalScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyBoneData(List<BoneData> boneDataList)
|
||||
{
|
||||
if (boneDataList == null)
|
||||
return;
|
||||
|
||||
foreach (BoneData boneData in boneDataList)
|
||||
{
|
||||
if (boneLookup.TryGetValue(boneData.boneName, out Transform targetBone))
|
||||
{
|
||||
// Convert serializable types back to Unity types
|
||||
Vector3 targetPosition = boneData.position.ToVector3();
|
||||
Quaternion targetRotation = boneData.rotation.ToQuaternion();
|
||||
Vector3 targetScale = boneData.scale.ToVector3();
|
||||
|
||||
// Debug mode: Log changes to the monitored bone
|
||||
if (enableDebugMode && boneData.boneName.Contains(debugBoneName))
|
||||
{
|
||||
Debug.Log($"Applying to bone {boneData.boneName}: Pos={targetPosition}, Rot={targetRotation}");
|
||||
}
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
// Smooth interpolation
|
||||
targetBone.localPosition = Vector3.Lerp(
|
||||
targetBone.localPosition,
|
||||
targetPosition,
|
||||
transitionSpeed * Time.deltaTime
|
||||
);
|
||||
|
||||
targetBone.localRotation = Quaternion.Lerp(
|
||||
targetBone.localRotation,
|
||||
targetRotation,
|
||||
transitionSpeed * Time.deltaTime
|
||||
);
|
||||
|
||||
targetBone.localScale = Vector3.Lerp(
|
||||
targetBone.localScale,
|
||||
targetScale,
|
||||
transitionSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Direct application
|
||||
targetBone.localPosition = targetPosition;
|
||||
targetBone.localRotation = targetRotation;
|
||||
targetBone.localScale = targetScale;
|
||||
}
|
||||
|
||||
appliedBones++;
|
||||
}
|
||||
else if (showDebugInfo)
|
||||
{
|
||||
Debug.LogWarning($"Bone not found: {boneData.boneName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyBlendShapeData(List<BlendShapeData> blendShapeDataList)
|
||||
{
|
||||
if (blendShapeDataList == null)
|
||||
return;
|
||||
|
||||
foreach (BlendShapeData blendShapeData in blendShapeDataList)
|
||||
{
|
||||
if (skinnedMeshLookup.TryGetValue(blendShapeData.meshName, out SkinnedMeshRenderer targetSmr))
|
||||
{
|
||||
if (targetSmr.sharedMesh != null && blendShapeData.weights != null)
|
||||
{
|
||||
int maxBlendShapes = Mathf.Min(targetSmr.sharedMesh.blendShapeCount, blendShapeData.weights.Length);
|
||||
|
||||
for (int i = 0; i < maxBlendShapes; i++)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
float currentWeight = targetSmr.GetBlendShapeWeight(i);
|
||||
float targetWeight = blendShapeData.weights[i];
|
||||
float newWeight = Mathf.Lerp(currentWeight, targetWeight, transitionSpeed * Time.deltaTime);
|
||||
targetSmr.SetBlendShapeWeight(i, newWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetSmr.SetBlendShapeWeight(i, blendShapeData.weights[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (showDebugInfo)
|
||||
{
|
||||
Debug.LogWarning($"SkinnedMeshRenderer not found: {blendShapeData.meshName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTargetAvatar(Transform newTargetRoot)
|
||||
{
|
||||
targetAvatarRoot = newTargetRoot;
|
||||
if (isInitialized)
|
||||
{
|
||||
CacheTargetAvatarComponents();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetFileName(string newFileName)
|
||||
{
|
||||
fileName = newFileName;
|
||||
string syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
|
||||
filePath = Path.Combine(syncFilesPath, fileName);
|
||||
}
|
||||
|
||||
public bool IsDataAvailable()
|
||||
{
|
||||
return File.Exists(filePath);
|
||||
}
|
||||
|
||||
public float GetLastSyncTimestamp()
|
||||
{
|
||||
return lastSyncData?.timestamp ?? 0f;
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/AvatarDataReader.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/AvatarDataReader.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81f65cbb675480c4ca726737a78179e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
289
Unity-Master/Assets/Scripts/AvatarDataUploader.cs
Normal file
289
Unity-Master/Assets/Scripts/AvatarDataUploader.cs
Normal file
@ -0,0 +1,289 @@
|
||||
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 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()
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/AvatarDataUploader.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/AvatarDataUploader.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f4c0a9d64416544891f7b7501031b0e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
331
Unity-Master/Assets/Scripts/AvatarDataWriter.cs
Normal file
331
Unity-Master/Assets/Scripts/AvatarDataWriter.cs
Normal file
@ -0,0 +1,331 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
[System.Serializable]
|
||||
public class SerializableVector3
|
||||
{
|
||||
public float x, y, z;
|
||||
|
||||
public SerializableVector3() { }
|
||||
|
||||
public SerializableVector3(Vector3 vector)
|
||||
{
|
||||
x = vector.x;
|
||||
y = vector.y;
|
||||
z = vector.z;
|
||||
}
|
||||
|
||||
public Vector3 ToVector3()
|
||||
{
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class SerializableQuaternion
|
||||
{
|
||||
public float x, y, z, w;
|
||||
|
||||
public SerializableQuaternion() { }
|
||||
|
||||
public SerializableQuaternion(Quaternion quaternion)
|
||||
{
|
||||
x = quaternion.x;
|
||||
y = quaternion.y;
|
||||
z = quaternion.z;
|
||||
w = quaternion.w;
|
||||
}
|
||||
|
||||
public Quaternion ToQuaternion()
|
||||
{
|
||||
return new Quaternion(x, y, z, w);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class RootTransformData
|
||||
{
|
||||
public SerializableVector3 worldPosition;
|
||||
public SerializableQuaternion worldRotation;
|
||||
public SerializableVector3 localScale;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class BoneData
|
||||
{
|
||||
public string boneName; // e.g. "LeftArm", "RightHand"
|
||||
public SerializableVector3 position;
|
||||
public SerializableQuaternion rotation;
|
||||
public SerializableVector3 scale;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class BlendShapeData
|
||||
{
|
||||
public string meshName;
|
||||
public float[] weights;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class AvatarSyncData
|
||||
{
|
||||
public RootTransformData rootTransform;
|
||||
public List<BoneData> bones;
|
||||
public List<BlendShapeData> blendShapes;
|
||||
public float timestamp;
|
||||
}
|
||||
|
||||
public class AvatarDataWriter : MonoBehaviour
|
||||
{
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform avatarRoot;
|
||||
[SerializeField] private string fileName = "avatar_sync_data.json";
|
||||
[SerializeField] private bool writeEveryFrame = true;
|
||||
[SerializeField] private float updateRate = 60f; // Updates per second
|
||||
|
||||
[Header("Synchronization Options")]
|
||||
[SerializeField] private bool syncWorldPosition = true; // Sync avatar world position
|
||||
[SerializeField] private bool syncWorldRotation = true; // Sync avatar world rotation
|
||||
[SerializeField] private bool syncLocalScale = false; // Sync avatar local scale
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool enableDebugMode = false;
|
||||
[SerializeField] private string debugBoneName = "LeftArm"; // Bone to monitor
|
||||
|
||||
private string filePath;
|
||||
private float lastUpdateTime;
|
||||
private List<Transform> allBones;
|
||||
private List<SkinnedMeshRenderer> skinnedMeshRenderers;
|
||||
private Vector3 lastDebugPosition;
|
||||
private Quaternion lastDebugRotation;
|
||||
private Vector3 lastRootWorldPosition;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Create Sync-Files directory if it doesn't exist
|
||||
string syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
|
||||
if (!Directory.Exists(syncFilesPath))
|
||||
{
|
||||
Directory.CreateDirectory(syncFilesPath);
|
||||
}
|
||||
|
||||
filePath = Path.Combine(syncFilesPath, fileName);
|
||||
|
||||
// If avatarRoot is not assigned, try to find it automatically
|
||||
if (avatarRoot == null)
|
||||
{
|
||||
avatarRoot = transform;
|
||||
}
|
||||
|
||||
// Cache all bones and skinned mesh renderers
|
||||
CacheAvatarComponents();
|
||||
|
||||
// Initialize debug tracking
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Transform debugBone = FindBoneByName(debugBoneName);
|
||||
if (debugBone != null)
|
||||
{
|
||||
lastDebugPosition = debugBone.localPosition;
|
||||
lastDebugRotation = debugBone.localRotation;
|
||||
Debug.Log($"Debug mode enabled. Monitoring bone: {debugBoneName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Debug bone '{debugBoneName}' not found!");
|
||||
}
|
||||
|
||||
lastRootWorldPosition = avatarRoot.position;
|
||||
}
|
||||
|
||||
Debug.Log($"Avatar Data Writer initialized. Writing to: {filePath}");
|
||||
Debug.Log($"World Position Sync: {syncWorldPosition}, World Rotation Sync: {syncWorldRotation}");
|
||||
}
|
||||
|
||||
void CacheAvatarComponents()
|
||||
{
|
||||
allBones = new List<Transform>();
|
||||
skinnedMeshRenderers = new List<SkinnedMeshRenderer>();
|
||||
|
||||
// Get all skinned mesh renderers
|
||||
skinnedMeshRenderers.AddRange(avatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>());
|
||||
|
||||
// Get all unique bones from all SkinnedMeshRenderers
|
||||
HashSet<Transform> uniqueBones = new HashSet<Transform>();
|
||||
foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers)
|
||||
{
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
uniqueBones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allBones.AddRange(uniqueBones);
|
||||
|
||||
Debug.Log($"Cached {allBones.Count} bones and {skinnedMeshRenderers.Count} skinned mesh renderers");
|
||||
|
||||
// Debug: Print some bone names to help with troubleshooting
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log("Found bones:");
|
||||
for (int i = 0; i < Mathf.Min(10, allBones.Count); i++)
|
||||
{
|
||||
Debug.Log($" Bone {i}: {allBones[i].name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Transform FindBoneByName(string name)
|
||||
{
|
||||
foreach (Transform bone in allBones)
|
||||
{
|
||||
if (bone.name.Contains(name))
|
||||
{
|
||||
return bone;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Debug mode: Check if the monitored bone has changed
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Transform debugBone = FindBoneByName(debugBoneName);
|
||||
if (debugBone != null)
|
||||
{
|
||||
if (Vector3.Distance(debugBone.localPosition, lastDebugPosition) > 0.001f ||
|
||||
Quaternion.Angle(debugBone.localRotation, lastDebugRotation) > 0.1f)
|
||||
{
|
||||
Debug.Log($"Bone {debugBoneName} changed! Pos: {debugBone.localPosition}, Rot: {debugBone.localRotation}");
|
||||
lastDebugPosition = debugBone.localPosition;
|
||||
lastDebugRotation = debugBone.localRotation;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Check if avatar root world position has changed
|
||||
if (Vector3.Distance(avatarRoot.position, lastRootWorldPosition) > 0.01f)
|
||||
{
|
||||
Debug.Log($"Avatar root world position changed! From: {lastRootWorldPosition} To: {avatarRoot.position}");
|
||||
lastRootWorldPosition = avatarRoot.position;
|
||||
}
|
||||
}
|
||||
|
||||
if (writeEveryFrame)
|
||||
{
|
||||
WriteAvatarData();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rate-limited updates
|
||||
if (Time.time - lastUpdateTime >= 1f / updateRate)
|
||||
{
|
||||
WriteAvatarData();
|
||||
lastUpdateTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteAvatarData()
|
||||
{
|
||||
try
|
||||
{
|
||||
AvatarSyncData syncData = new AvatarSyncData
|
||||
{
|
||||
rootTransform = new RootTransformData(),
|
||||
bones = new List<BoneData>(),
|
||||
blendShapes = new List<BlendShapeData>(),
|
||||
timestamp = Time.time
|
||||
};
|
||||
|
||||
// Capture root transform data
|
||||
if (syncWorldPosition)
|
||||
{
|
||||
syncData.rootTransform.worldPosition = new SerializableVector3(avatarRoot.position);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.rootTransform.worldPosition = new SerializableVector3(Vector3.zero);
|
||||
}
|
||||
|
||||
if (syncWorldRotation)
|
||||
{
|
||||
syncData.rootTransform.worldRotation = new SerializableQuaternion(avatarRoot.rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.rootTransform.worldRotation = new SerializableQuaternion(Quaternion.identity);
|
||||
}
|
||||
|
||||
if (syncLocalScale)
|
||||
{
|
||||
syncData.rootTransform.localScale = new SerializableVector3(avatarRoot.localScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
syncData.rootTransform.localScale = new SerializableVector3(Vector3.one);
|
||||
}
|
||||
|
||||
// Capture bone data
|
||||
foreach (Transform bone in allBones)
|
||||
{
|
||||
BoneData boneData = new BoneData
|
||||
{
|
||||
boneName = bone.name,
|
||||
position = new SerializableVector3(bone.localPosition),
|
||||
rotation = new SerializableQuaternion(bone.localRotation),
|
||||
scale = new SerializableVector3(bone.localScale)
|
||||
};
|
||||
syncData.bones.Add(boneData);
|
||||
}
|
||||
|
||||
// Capture blendshape data
|
||||
foreach (SkinnedMeshRenderer smr in skinnedMeshRenderers)
|
||||
{
|
||||
if (smr.sharedMesh != null && smr.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
float[] weights = new float[smr.sharedMesh.blendShapeCount];
|
||||
for (int i = 0; i < weights.Length; i++)
|
||||
{
|
||||
weights[i] = smr.GetBlendShapeWeight(i);
|
||||
}
|
||||
|
||||
BlendShapeData blendShapeData = new BlendShapeData
|
||||
{
|
||||
meshName = smr.gameObject.name, // Use GameObject name instead of path
|
||||
weights = weights
|
||||
};
|
||||
syncData.blendShapes.Add(blendShapeData);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to JSON and write to file
|
||||
string json = JsonConvert.SerializeObject(syncData, Formatting.Indented);
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Error writing avatar data: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
// Write final data when component is disabled
|
||||
if (allBones != null && allBones.Count > 0)
|
||||
{
|
||||
WriteAvatarData();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/AvatarDataWriter.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/AvatarDataWriter.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 029bf22d15ab7214484688dd969f7d28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
378
Unity-Master/Assets/Scripts/AvatarSyncClient.cs
Normal file
378
Unity-Master/Assets/Scripts/AvatarSyncClient.cs
Normal file
@ -0,0 +1,378 @@
|
||||
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 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()
|
||||
{
|
||||
// Set up file paths
|
||||
syncFilesPath = Path.Combine(Application.dataPath, "Sync-Files");
|
||||
if (!Directory.Exists(syncFilesPath))
|
||||
{
|
||||
Directory.CreateDirectory(syncFilesPath);
|
||||
}
|
||||
|
||||
// Determine output file name
|
||||
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
|
||||
outputFilePath = Path.Combine(syncFilesPath, fileName);
|
||||
|
||||
Debug.Log($"Avatar Sync Client initialized");
|
||||
Debug.Log($"Server: {ServerUrl}");
|
||||
Debug.Log($"Target Player: {targetPlayer}");
|
||||
Debug.Log($"Output File: {outputFilePath}");
|
||||
Debug.Log($"Upload Mode: {(uploadLocalData ? $"Yes (as {uploadAsPlayer})" : "No")}");
|
||||
|
||||
// Start syncing if auto-sync is enabled
|
||||
if (autoSync)
|
||||
{
|
||||
StartSync();
|
||||
}
|
||||
|
||||
// Test connection
|
||||
StartCoroutine(TestConnection());
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
// Ensure valid values
|
||||
if (syncInterval < 0.01f)
|
||||
syncInterval = 0.01f;
|
||||
|
||||
if (serverPort < 1 || serverPort > 65535)
|
||||
serverPort = 8080;
|
||||
}
|
||||
|
||||
public void StartSync()
|
||||
{
|
||||
if (syncCoroutine == null)
|
||||
{
|
||||
syncCoroutine = StartCoroutine(SyncLoop());
|
||||
Debug.Log("Avatar sync started");
|
||||
}
|
||||
}
|
||||
|
||||
public void StopSync()
|
||||
{
|
||||
if (syncCoroutine != null)
|
||||
{
|
||||
StopCoroutine(syncCoroutine);
|
||||
syncCoroutine = null;
|
||||
Debug.Log("Avatar sync stopped");
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator TestConnection()
|
||||
{
|
||||
string url = $"{ServerUrl}/status";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 5; // 5 second timeout for connection test
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
isConnected = true;
|
||||
if (showConnectionStatus)
|
||||
{
|
||||
Debug.Log($"Successfully connected to avatar sync server at {ServerUrl}");
|
||||
}
|
||||
|
||||
// Try to parse server status
|
||||
try
|
||||
{
|
||||
var status = JsonConvert.DeserializeObject<Dictionary<string, object>>(request.downloadHandler.text);
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log($"Server status: {request.downloadHandler.text}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Could not parse server status: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isConnected = false;
|
||||
if (showConnectionStatus)
|
||||
{
|
||||
Debug.LogWarning($"Failed to connect to avatar sync server at {ServerUrl}: {request.error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator SyncLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Upload local data if enabled
|
||||
if (uploadLocalData)
|
||||
{
|
||||
yield return StartCoroutine(UploadLocalData());
|
||||
}
|
||||
|
||||
// Fetch remote player data
|
||||
yield return StartCoroutine(FetchPlayerData());
|
||||
|
||||
yield return new WaitForSeconds(syncInterval);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator FetchPlayerData()
|
||||
{
|
||||
string url = $"{ServerUrl}/{targetPlayer}";
|
||||
totalRequests++;
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 10; // 10 second timeout
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate JSON
|
||||
var avatarData = JsonConvert.DeserializeObject<AvatarSyncData>(request.downloadHandler.text);
|
||||
|
||||
if (avatarData != null)
|
||||
{
|
||||
// Write to local file
|
||||
File.WriteAllText(outputFilePath, request.downloadHandler.text);
|
||||
|
||||
successfulRequests++;
|
||||
lastSuccessfulSync = Time.time;
|
||||
isConnected = true;
|
||||
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log($"Successfully fetched and saved data for {targetPlayer}. Timestamp: {avatarData.timestamp}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Received null avatar data for {targetPlayer}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Error processing avatar data for {targetPlayer}: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
isConnected = false;
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Failed to fetch data for {targetPlayer}: {request.error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator UploadLocalData()
|
||||
{
|
||||
string localFilePath = Path.Combine(syncFilesPath, localDataFile);
|
||||
|
||||
if (!File.Exists(localFilePath))
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Local data file not found: {localFilePath}");
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
string jsonData = null;
|
||||
AvatarSyncData avatarData = null;
|
||||
|
||||
// Read and validate file outside of try block with yield
|
||||
try
|
||||
{
|
||||
jsonData = File.ReadAllText(localFilePath);
|
||||
avatarData = JsonConvert.DeserializeObject<AvatarSyncData>(jsonData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Error reading/parsing local data file: {e.Message}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (avatarData == null)
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning("Local avatar data is invalid, skipping upload");
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Network request without try-catch since we can't yield in try-catch
|
||||
string url = $"{ServerUrl}/{uploadAsPlayer}";
|
||||
|
||||
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
|
||||
{
|
||||
byte[] jsonBytes = System.Text.Encoding.UTF8.GetBytes(jsonData);
|
||||
request.uploadHandler = new UploadHandlerRaw(jsonBytes);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
request.timeout = 10;
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.Log($"Successfully uploaded data as {uploadAsPlayer}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (enableDebugMode)
|
||||
{
|
||||
Debug.LogWarning($"Failed to upload data as {uploadAsPlayer}: {request.error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods for runtime control
|
||||
public void SetServerAddress(string host, int port)
|
||||
{
|
||||
serverHost = host;
|
||||
serverPort = port;
|
||||
|
||||
// Test new connection
|
||||
StartCoroutine(TestConnection());
|
||||
}
|
||||
|
||||
public void SetTargetPlayer(string player)
|
||||
{
|
||||
targetPlayer = player;
|
||||
|
||||
// Update output file path
|
||||
string fileName = string.IsNullOrEmpty(localFileName) ? $"{targetPlayer}.json" : localFileName;
|
||||
outputFilePath = Path.Combine(syncFilesPath, fileName);
|
||||
}
|
||||
|
||||
public void ManualSync()
|
||||
{
|
||||
if (gameObject.activeInHierarchy)
|
||||
{
|
||||
StartCoroutine(FetchPlayerData());
|
||||
}
|
||||
}
|
||||
|
||||
// Get current stats for UI display
|
||||
public string GetConnectionStats()
|
||||
{
|
||||
return $"Connected: {isConnected}\n" +
|
||||
$"Success Rate: {(SuccessRate * 100):F1}%\n" +
|
||||
$"Total Requests: {totalRequests}\n" +
|
||||
$"Last Sync: {(Time.time - lastSuccessfulSync):F1}s ago";
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
StopSync();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopSync();
|
||||
}
|
||||
|
||||
// GUI for runtime debugging
|
||||
void OnGUI()
|
||||
{
|
||||
if (!enableDebugMode)
|
||||
return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 200));
|
||||
GUILayout.BeginVertical("box");
|
||||
|
||||
GUILayout.Label($"Avatar Sync Client - {targetPlayer}");
|
||||
GUILayout.Label($"Server: {ServerUrl}");
|
||||
GUILayout.Label($"Status: {(isConnected ? "Connected" : "Disconnected")}");
|
||||
GUILayout.Label($"Success Rate: {(SuccessRate * 100):F1}%");
|
||||
GUILayout.Label($"Last Sync: {(Time.time - lastSuccessfulSync):F1}s ago");
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Manual Sync"))
|
||||
{
|
||||
ManualSync();
|
||||
}
|
||||
if (GUILayout.Button("Test Connection"))
|
||||
{
|
||||
StartCoroutine(TestConnection());
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (syncCoroutine == null)
|
||||
{
|
||||
if (GUILayout.Button("Start Auto Sync"))
|
||||
{
|
||||
StartSync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("Stop Auto Sync"))
|
||||
{
|
||||
StopSync();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/AvatarSyncClient.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/AvatarSyncClient.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4724255e4e0e6f04e8d53c6ad4fb7ca1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
611
Unity-Master/Assets/Scripts/AvatarSyncComparison.cs
Normal file
611
Unity-Master/Assets/Scripts/AvatarSyncComparison.cs
Normal file
@ -0,0 +1,611 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Utility script to compare the old JSON system vs new UDP binary system
|
||||
/// Attach this to any GameObject and check the console for size comparisons
|
||||
/// </summary>
|
||||
public class AvatarSyncComparison : MonoBehaviour
|
||||
{
|
||||
[Header("Comparison Settings")]
|
||||
[SerializeField] private bool runComparisonOnStart = true;
|
||||
[SerializeField] private bool showDetailedBreakdown = true;
|
||||
[SerializeField] private bool saveToFile = true;
|
||||
[SerializeField] private string outputFileName = "avatar_sync_comparison.txt";
|
||||
|
||||
[Header("Test Data - Avatar Reference")]
|
||||
[SerializeField] private Transform testAvatar;
|
||||
[SerializeField] private UDPAvatarBroadcaster udpBroadcaster; // Get actual settings
|
||||
[SerializeField] private UDPAvatarReceiver udpReceiver; // Get actual settings
|
||||
|
||||
[Header("System Parameters")]
|
||||
[SerializeField] private float updateRate = 30f; // Hz - configurable refresh rate
|
||||
[SerializeField] private int oldSystemBoneCount = 80; // Calculated from avatar if available
|
||||
[SerializeField] private int oldSystemMeshCount = 4; // Calculated from avatar if available
|
||||
[SerializeField] private int oldSystemBlendShapesPerMesh = 52; // Calculated from avatar if available
|
||||
|
||||
[Header("Code Complexity (Lines of Code)")]
|
||||
[SerializeField] private int oldSystemTotalLines = 2300; // Can be calculated by scanning files
|
||||
[SerializeField] private int newSystemTotalLines = 800; // Can be calculated by scanning files
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (runComparisonOnStart)
|
||||
{
|
||||
// Add 5 second delay to allow UDPAvatarBroadcaster to fully initialize
|
||||
Invoke(nameof(DelayedStart), 5f);
|
||||
}
|
||||
}
|
||||
|
||||
void DelayedStart()
|
||||
{
|
||||
// Auto-calculate values from avatar if available
|
||||
CalculateAvatarParameters();
|
||||
RunComparison();
|
||||
}
|
||||
|
||||
void CalculateAvatarParameters()
|
||||
{
|
||||
// Get UDPAvatarBroadcaster from same GameObject if not assigned
|
||||
if (udpBroadcaster == null)
|
||||
{
|
||||
udpBroadcaster = GetComponent<UDPAvatarBroadcaster>();
|
||||
}
|
||||
|
||||
// Get actual values from UDP broadcaster
|
||||
if (udpBroadcaster != null)
|
||||
{
|
||||
updateRate = GetUDPBroadcasterUpdateRate();
|
||||
Debug.Log($"Got actual update rate from UDPAvatarBroadcaster: {updateRate} Hz");
|
||||
|
||||
// Use the avatar from the broadcaster if not set
|
||||
if (testAvatar == null)
|
||||
{
|
||||
testAvatar = GetUDPBroadcasterAvatarRoot();
|
||||
}
|
||||
}
|
||||
|
||||
if (testAvatar != null)
|
||||
{
|
||||
// Calculate actual bone count from avatar
|
||||
SkinnedMeshRenderer[] meshRenderers = testAvatar.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
HashSet<Transform> uniqueBones = new HashSet<Transform>();
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in meshRenderers)
|
||||
{
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
uniqueBones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueBones.Count > 0)
|
||||
{
|
||||
oldSystemBoneCount = uniqueBones.Count;
|
||||
}
|
||||
|
||||
// Calculate actual mesh and blend shape counts
|
||||
if (meshRenderers.Length > 0)
|
||||
{
|
||||
oldSystemMeshCount = meshRenderers.Length;
|
||||
|
||||
// Calculate average blend shapes per mesh
|
||||
int totalBlendShapes = 0;
|
||||
int meshesWithBlendShapes = 0;
|
||||
|
||||
foreach (var mesh in meshRenderers)
|
||||
{
|
||||
if (mesh.sharedMesh != null && mesh.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
totalBlendShapes += mesh.sharedMesh.blendShapeCount;
|
||||
meshesWithBlendShapes++;
|
||||
}
|
||||
}
|
||||
|
||||
if (meshesWithBlendShapes > 0)
|
||||
{
|
||||
oldSystemBlendShapesPerMesh = totalBlendShapes / meshesWithBlendShapes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Calculated Parameters: {oldSystemBoneCount} bones, {oldSystemMeshCount} meshes, {oldSystemBlendShapesPerMesh} blend shapes/mesh, {updateRate} Hz");
|
||||
}
|
||||
|
||||
float GetUDPBroadcasterUpdateRate()
|
||||
{
|
||||
if (udpBroadcaster == null) return updateRate;
|
||||
|
||||
// Use reflection to get the private updateRate field
|
||||
var field = typeof(UDPAvatarBroadcaster).GetField("updateRate",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
return (float)field.GetValue(udpBroadcaster);
|
||||
}
|
||||
|
||||
return updateRate; // Fallback to current value
|
||||
}
|
||||
|
||||
Transform GetUDPBroadcasterAvatarRoot()
|
||||
{
|
||||
if (udpBroadcaster == null) return null;
|
||||
|
||||
// Use reflection to get the private avatarRoot field
|
||||
var field = typeof(UDPAvatarBroadcaster).GetField("avatarRoot",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
return (Transform)field.GetValue(udpBroadcaster);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool GetUDPBroadcasterFullDataMode()
|
||||
{
|
||||
if (udpBroadcaster == null) return false;
|
||||
|
||||
// Use reflection to get the private fullDataMode field
|
||||
var field = typeof(UDPAvatarBroadcaster).GetField("fullDataMode",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
return (bool)field.GetValue(udpBroadcaster);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int GetUDPBroadcasterCurrentBoneCount()
|
||||
{
|
||||
if (udpBroadcaster == null) return 20;
|
||||
|
||||
// Use reflection to get the private currentBoneCount field
|
||||
var field = typeof(UDPAvatarBroadcaster).GetField("currentBoneCount",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
return (int)field.GetValue(udpBroadcaster);
|
||||
}
|
||||
|
||||
return 20; // Default optimized mode
|
||||
}
|
||||
|
||||
int GetUDPBroadcasterCurrentBlendShapeCount()
|
||||
{
|
||||
if (udpBroadcaster == null) return 10;
|
||||
|
||||
// Use reflection to get the private currentBlendShapeCount field
|
||||
var field = typeof(UDPAvatarBroadcaster).GetField("currentBlendShapeCount",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (field != null)
|
||||
{
|
||||
return (int)field.GetValue(udpBroadcaster);
|
||||
}
|
||||
|
||||
return 10; // Default optimized mode
|
||||
}
|
||||
|
||||
[ContextMenu("Run Size Comparison")]
|
||||
public void RunComparison()
|
||||
{
|
||||
Debug.Log("=== Avatar Sync System Comparison ===");
|
||||
|
||||
// Simulate old JSON system
|
||||
var jsonSize = CalculateJSONSystemSize();
|
||||
|
||||
// Simulate new UDP binary system
|
||||
var binarySize = CalculateUDPBinarySize();
|
||||
|
||||
// Calculate efficiency gains
|
||||
float compressionRatio = (float)jsonSize / binarySize;
|
||||
float bandwidthReduction = (1f - (float)binarySize / jsonSize) * 100f;
|
||||
|
||||
Debug.Log($"\n📊 SIZE COMPARISON:");
|
||||
Debug.Log($"Old JSON System: {jsonSize:N0} bytes ({jsonSize / 1024f:F1} KB)");
|
||||
Debug.Log($"New UDP Binary: {binarySize:N0} bytes ({binarySize / 1024f:F1} KB)");
|
||||
Debug.Log($"Compression Ratio: {compressionRatio:F1}x smaller");
|
||||
Debug.Log($"Bandwidth Reduction: {bandwidthReduction:F1}%");
|
||||
|
||||
if (showDetailedBreakdown)
|
||||
{
|
||||
ShowDetailedBreakdown();
|
||||
}
|
||||
|
||||
ShowNetworkImpact(jsonSize, binarySize);
|
||||
ShowSystemComplexity();
|
||||
|
||||
if (saveToFile)
|
||||
{
|
||||
SaveComparisonToFile(jsonSize, binarySize);
|
||||
}
|
||||
}
|
||||
|
||||
int CalculateJSONSystemSize()
|
||||
{
|
||||
// Recreate the old AvatarSyncData structure using calculated values
|
||||
var oldSystemData = new
|
||||
{
|
||||
rootTransform = new
|
||||
{
|
||||
worldPosition = new { x = 0f, y = 0f, z = 0f },
|
||||
worldRotation = new { x = 0f, y = 0f, z = 0f, w = 1f },
|
||||
localScale = new { x = 1f, y = 1f, z = 1f }
|
||||
},
|
||||
bones = GenerateOldSystemBones(oldSystemBoneCount),
|
||||
blendShapes = GenerateOldSystemBlendShapes(oldSystemMeshCount, oldSystemBlendShapesPerMesh),
|
||||
timestamp = Time.time
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(oldSystemData, Formatting.Indented);
|
||||
int jsonBytes = Encoding.UTF8.GetByteCount(json);
|
||||
|
||||
Debug.Log($"📄 Old System Structure: {oldSystemBoneCount} bones, {oldSystemMeshCount} meshes × {oldSystemBlendShapesPerMesh} blend shapes = {oldSystemMeshCount * oldSystemBlendShapesPerMesh} total blend shapes");
|
||||
Debug.Log($"📄 Sample JSON (first 200 chars):\n{json.Substring(0, Math.Min(200, json.Length))}...");
|
||||
|
||||
return jsonBytes;
|
||||
}
|
||||
|
||||
int CalculateUDPBinarySize()
|
||||
{
|
||||
// Get actual bone and blend shape counts from UDP system
|
||||
int actualBoneCount = GetNewSystemBoneCount();
|
||||
int actualBlendShapeCount = GetNewSystemBlendShapeCount();
|
||||
|
||||
// Calculate new UDP binary system size
|
||||
int headerSize = 1 + 4 + 4 + 1; // playerID + sequenceNumber + timestamp + isFullDataMode
|
||||
int rootTransformSize = (3 + 4 + 3) * 4; // 3 Vector3s + 1 Quaternion * 4 bytes per float
|
||||
int bonesSize = 4 + (actualBoneCount * (3 + 4) * 4); // bone count + bones * (position + rotation) * 4 bytes per float
|
||||
int blendShapesSize = 4 + (actualBlendShapeCount * 4); // blend shape count + weights * 4 bytes per float
|
||||
|
||||
int totalSize = headerSize + rootTransformSize + bonesSize + blendShapesSize;
|
||||
|
||||
Debug.Log($"🔢 New UDP Binary Structure:");
|
||||
Debug.Log($" Header: {headerSize} bytes");
|
||||
Debug.Log($" Root Transform: {rootTransformSize} bytes");
|
||||
Debug.Log($" Bones ({actualBoneCount}): {bonesSize} bytes");
|
||||
Debug.Log($" Blend Shapes ({actualBlendShapeCount}): {blendShapesSize} bytes");
|
||||
Debug.Log($" Total: {totalSize} bytes");
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
int GetNewSystemBoneCount()
|
||||
{
|
||||
// Get actual bone count from UDP broadcaster
|
||||
if (udpBroadcaster != null)
|
||||
{
|
||||
int actualBoneCount = GetUDPBroadcasterCurrentBoneCount();
|
||||
bool fullDataMode = GetUDPBroadcasterFullDataMode();
|
||||
|
||||
if (showDetailedBreakdown)
|
||||
{
|
||||
string mode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
|
||||
Debug.Log($"UDP Broadcaster is in {mode} mode with {actualBoneCount} bones");
|
||||
}
|
||||
|
||||
return actualBoneCount;
|
||||
}
|
||||
|
||||
// Default to optimized mode bone count
|
||||
return 20;
|
||||
}
|
||||
|
||||
int GetNewSystemBlendShapeCount()
|
||||
{
|
||||
// Get actual blend shape count from UDP broadcaster
|
||||
if (udpBroadcaster != null)
|
||||
{
|
||||
int actualBlendShapeCount = GetUDPBroadcasterCurrentBlendShapeCount();
|
||||
bool fullDataMode = GetUDPBroadcasterFullDataMode();
|
||||
|
||||
if (showDetailedBreakdown)
|
||||
{
|
||||
string mode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
|
||||
Debug.Log($"UDP Broadcaster is in {mode} mode with {actualBlendShapeCount} blend shapes");
|
||||
}
|
||||
|
||||
return actualBlendShapeCount;
|
||||
}
|
||||
|
||||
// Default to optimized mode blend shape count
|
||||
return 10;
|
||||
}
|
||||
|
||||
object[] GenerateOldSystemBones(int count)
|
||||
{
|
||||
object[] bones = new object[count];
|
||||
string[] boneNames = {
|
||||
"Hips", "Spine", "Spine1", "Spine2", "Neck", "Head", "HeadTop_End",
|
||||
"LeftEye", "RightEye", "LeftShoulder", "LeftArm", "LeftArmTwist",
|
||||
"LeftForeArm", "LeftForeArmTwist", "LeftHand", "LeftHandThumb1",
|
||||
"LeftHandThumb2", "LeftHandThumb3", "LeftHandThumb4", "LeftHandIndex0",
|
||||
"LeftHandIndex1", "LeftHandIndex2", "LeftHandIndex3", "LeftHandIndex4",
|
||||
"LeftHandMiddle0", "LeftHandMiddle1", "LeftHandMiddle2", "LeftHandMiddle3",
|
||||
"LeftHandMiddle4", "LeftHandRing0", "LeftHandRing1", "LeftHandRing2",
|
||||
"LeftHandRing3", "LeftHandRing4", "LeftHandPinky0", "LeftHandPinky1",
|
||||
"LeftHandPinky2", "LeftHandPinky3", "LeftHandPinky4", "LeftPalm",
|
||||
"RightShoulder", "RightArm", "RightArmTwist", "RightForeArm",
|
||||
"RightForeArmTwist", "RightHand", "RightHandThumb1", "RightHandThumb2",
|
||||
"RightHandThumb3", "RightHandThumb4", "RightHandIndex0", "RightHandIndex1",
|
||||
"RightHandIndex2", "RightHandIndex3", "RightHandIndex4", "RightHandMiddle0",
|
||||
"RightHandMiddle1", "RightHandMiddle2", "RightHandMiddle3", "RightHandMiddle4",
|
||||
"RightHandRing0", "RightHandRing1", "RightHandRing2", "RightHandRing3",
|
||||
"RightHandRing4", "RightHandPinky0", "RightHandPinky1", "RightHandPinky2",
|
||||
"RightHandPinky3", "RightHandPinky4", "RightPalm", "LeftUpLeg", "LeftLeg",
|
||||
"LeftFoot", "LeftToeBase", "LeftToe_End", "RightUpLeg", "RightLeg",
|
||||
"RightFoot", "RightToeBase", "RightToe_End"
|
||||
};
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
string boneName = i < boneNames.Length ? boneNames[i] : $"Bone_{i}";
|
||||
bones[i] = new
|
||||
{
|
||||
boneName = boneName,
|
||||
position = new { x = UnityEngine.Random.Range(-1f, 1f), y = UnityEngine.Random.Range(-1f, 1f), z = UnityEngine.Random.Range(-1f, 1f) },
|
||||
rotation = new { x = UnityEngine.Random.Range(-1f, 1f), y = UnityEngine.Random.Range(-1f, 1f), z = UnityEngine.Random.Range(-1f, 1f), w = UnityEngine.Random.Range(-1f, 1f) },
|
||||
scale = new { x = 1f, y = 1f, z = 1f }
|
||||
};
|
||||
}
|
||||
|
||||
return bones;
|
||||
}
|
||||
|
||||
object[] GenerateOldSystemBlendShapes(int meshCount, int weightsPerMesh)
|
||||
{
|
||||
object[] blendShapes = new object[meshCount];
|
||||
string[] meshNames = { "Renderer_Head", "Renderer_EyeLeft", "Renderer_EyeRight", "Renderer_Teeth" };
|
||||
|
||||
for (int i = 0; i < meshCount; i++)
|
||||
{
|
||||
float[] weights = new float[weightsPerMesh];
|
||||
for (int j = 0; j < weightsPerMesh; j++)
|
||||
{
|
||||
weights[j] = UnityEngine.Random.Range(0f, 100f);
|
||||
}
|
||||
|
||||
blendShapes[i] = new
|
||||
{
|
||||
meshName = i < meshNames.Length ? meshNames[i] : $"Mesh_{i}",
|
||||
weights = weights
|
||||
};
|
||||
}
|
||||
|
||||
return blendShapes;
|
||||
}
|
||||
|
||||
void ShowDetailedBreakdown()
|
||||
{
|
||||
int newSystemBones = GetNewSystemBoneCount();
|
||||
int newSystemBlendShapes = GetNewSystemBlendShapeCount();
|
||||
int totalOldBlendShapes = oldSystemMeshCount * oldSystemBlendShapesPerMesh;
|
||||
bool fullDataMode = GetUDPBroadcasterFullDataMode();
|
||||
string currentMode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
|
||||
|
||||
Debug.Log($"\n📋 DETAILED BREAKDOWN:");
|
||||
Debug.Log($"\nCurrent UDP System Configuration:");
|
||||
Debug.Log($" • Mode: {currentMode}");
|
||||
Debug.Log($" • Update Rate: {updateRate} Hz");
|
||||
Debug.Log($" • Bones: {newSystemBones}");
|
||||
Debug.Log($" • Blend Shapes: {newSystemBlendShapes}");
|
||||
|
||||
Debug.Log($"\nOld JSON System Issues:");
|
||||
Debug.Log($" • {oldSystemBoneCount} bones with full transform data");
|
||||
Debug.Log($" • {oldSystemMeshCount} meshes × {oldSystemBlendShapesPerMesh} blend shapes = {totalOldBlendShapes} facial weights");
|
||||
Debug.Log($" • Verbose JSON with field names repeated");
|
||||
Debug.Log($" • UTF-8 string encoding overhead");
|
||||
Debug.Log($" • HTTP headers and protocol overhead");
|
||||
Debug.Log($" • Requires Python server infrastructure");
|
||||
|
||||
Debug.Log($"\nNew UDP Binary Advantages:");
|
||||
if (fullDataMode)
|
||||
{
|
||||
Debug.Log($" • {newSystemBones} bones (FULL avatar data)");
|
||||
Debug.Log($" • {newSystemBlendShapes} facial blend shapes (ALL expressions)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($" • Only {newSystemBones} priority bones (most important)");
|
||||
Debug.Log($" • {newSystemBlendShapes} key facial blend shapes only");
|
||||
}
|
||||
Debug.Log($" • Compact binary format, no field names");
|
||||
Debug.Log($" • Variable-size packets with length prefixes");
|
||||
Debug.Log($" • Direct UDP broadcast, no server needed");
|
||||
Debug.Log($" • Predictable network usage");
|
||||
|
||||
float boneReduction = (1f - (float)newSystemBones / oldSystemBoneCount) * 100f;
|
||||
float blendShapeReduction = (1f - (float)newSystemBlendShapes / totalOldBlendShapes) * 100f;
|
||||
Debug.Log($"\nData Reduction vs Old System:");
|
||||
Debug.Log($" • Bones: {boneReduction:F1}% reduction ({oldSystemBoneCount} → {newSystemBones})");
|
||||
Debug.Log($" • Blend Shapes: {blendShapeReduction:F1}% reduction ({totalOldBlendShapes} → {newSystemBlendShapes})");
|
||||
}
|
||||
|
||||
void ShowNetworkImpact(int jsonSize, int binarySize)
|
||||
{
|
||||
Debug.Log($"\n🌐 NETWORK IMPACT (at {updateRate} Hz):");
|
||||
|
||||
float jsonBandwidthKBps = (jsonSize * updateRate) / 1024f;
|
||||
float binaryBandwidthKBps = (binarySize * updateRate) / 1024f;
|
||||
|
||||
Debug.Log($"Old System: {jsonBandwidthKBps:F1} KB/s per player");
|
||||
Debug.Log($"New System: {binaryBandwidthKBps:F1} KB/s per player");
|
||||
|
||||
// Calculate for 3 player setup
|
||||
int playerCount = 3;
|
||||
Debug.Log($"\n3 Player Setup:");
|
||||
Debug.Log($"Old System: {jsonBandwidthKBps * playerCount:F1} KB/s total");
|
||||
Debug.Log($"New System: {binaryBandwidthKBps * playerCount:F1} KB/s total");
|
||||
|
||||
Debug.Log($"\nData usage per hour (single player):");
|
||||
float oldMBPerHour = (jsonBandwidthKBps * 3600) / 1024;
|
||||
float newMBPerHour = (binaryBandwidthKBps * 3600) / 1024;
|
||||
Debug.Log($"Old System: {oldMBPerHour:F1} MB/hour");
|
||||
Debug.Log($"New System: {newMBPerHour:F1} MB/hour");
|
||||
Debug.Log($"Savings: {oldMBPerHour - newMBPerHour:F1} MB/hour ({((oldMBPerHour - newMBPerHour) / oldMBPerHour) * 100:F1}% reduction)");
|
||||
}
|
||||
|
||||
void ShowSystemComplexity()
|
||||
{
|
||||
float codeReduction = (1f - (float)newSystemTotalLines / oldSystemTotalLines) * 100f;
|
||||
|
||||
Debug.Log($"\n⚙️ SYSTEM COMPLEXITY:");
|
||||
Debug.Log($"\nOld System Components:");
|
||||
Debug.Log($" • avatar_sync_server.py");
|
||||
Debug.Log($" • AvatarSyncClient.cs");
|
||||
Debug.Log($" • AvatarDataUploader.cs");
|
||||
Debug.Log($" • AvatarDataDownloader.cs");
|
||||
Debug.Log($" • AvatarDataWriter.cs");
|
||||
Debug.Log($" • AvatarDataReader.cs");
|
||||
Debug.Log($" • Multiple serialization classes");
|
||||
Debug.Log($" • HTTP client/server infrastructure");
|
||||
Debug.Log($" Total: ~{oldSystemTotalLines:N0} lines of code");
|
||||
|
||||
Debug.Log($"\nNew System Components:");
|
||||
Debug.Log($" • UDPAvatarBroadcaster.cs");
|
||||
Debug.Log($" • UDPAvatarReceiver.cs");
|
||||
Debug.Log($" • Simple binary serialization");
|
||||
Debug.Log($" • No server infrastructure needed");
|
||||
Debug.Log($" Total: ~{newSystemTotalLines:N0} lines of code");
|
||||
|
||||
Debug.Log($"\n✅ New system is {codeReduction:F1}% less code and much simpler!");
|
||||
Debug.Log($"Code reduction: {oldSystemTotalLines:N0} → {newSystemTotalLines:N0} lines ({oldSystemTotalLines - newSystemTotalLines:N0} lines removed)");
|
||||
}
|
||||
|
||||
void SaveComparisonToFile(int jsonSize, int binarySize)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Calculate all metrics using dynamic values
|
||||
float compressionRatio = (float)jsonSize / binarySize;
|
||||
float bandwidthReduction = (1f - (float)binarySize / jsonSize) * 100f;
|
||||
float jsonBandwidthKBps = (jsonSize * updateRate) / 1024f;
|
||||
float binaryBandwidthKBps = (binarySize * updateRate) / 1024f;
|
||||
|
||||
// Get calculated counts
|
||||
int newSystemBones = GetNewSystemBoneCount();
|
||||
int newSystemBlendShapes = GetNewSystemBlendShapeCount();
|
||||
int totalOldBlendShapes = oldSystemMeshCount * oldSystemBlendShapesPerMesh;
|
||||
bool fullDataMode = GetUDPBroadcasterFullDataMode();
|
||||
string currentMode = fullDataMode ? "FULL DATA" : "OPTIMIZED";
|
||||
|
||||
// Create file content with just the data
|
||||
StringBuilder fileContent = new StringBuilder();
|
||||
fileContent.AppendLine("Avatar Sync System Comparison Results");
|
||||
fileContent.AppendLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// Size comparison
|
||||
fileContent.AppendLine("PACKET SIZE COMPARISON:");
|
||||
fileContent.AppendLine($"Old JSON System: {jsonSize:N0} bytes ({jsonSize / 1024f:F2} KB)");
|
||||
fileContent.AppendLine($"New UDP Binary: {binarySize:N0} bytes ({binarySize / 1024f:F2} KB)");
|
||||
fileContent.AppendLine($"Compression Ratio: {compressionRatio:F1}x smaller");
|
||||
fileContent.AppendLine($"Size Reduction: {bandwidthReduction:F1}%");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// Binary structure breakdown using calculated values
|
||||
int headerSize = 1 + 4 + 4 + 1; // playerID + sequenceNumber + timestamp + isFullDataMode
|
||||
int rootTransformSize = (3 + 4 + 3) * 4;
|
||||
int bonesSize = 4 + (newSystemBones * (3 + 4) * 4);
|
||||
int blendShapesSize = 4 + (newSystemBlendShapes * 4);
|
||||
|
||||
fileContent.AppendLine("UDP BINARY STRUCTURE:");
|
||||
fileContent.AppendLine($"Header: {headerSize} bytes");
|
||||
fileContent.AppendLine($"Root Transform: {rootTransformSize} bytes");
|
||||
fileContent.AppendLine($"Bones ({newSystemBones}): {bonesSize} bytes");
|
||||
fileContent.AppendLine($"Blend Shapes ({newSystemBlendShapes}): {blendShapesSize} bytes");
|
||||
fileContent.AppendLine($"Total: {binarySize} bytes");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// Bandwidth impact at configured rate
|
||||
fileContent.AppendLine($"BANDWIDTH USAGE ({updateRate} Hz):");
|
||||
fileContent.AppendLine($"Old System: {jsonBandwidthKBps:F1} KB/s per player");
|
||||
fileContent.AppendLine($"New System: {binaryBandwidthKBps:F1} KB/s per player");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// System details
|
||||
fileContent.AppendLine("SYSTEM DETAILS:");
|
||||
fileContent.AppendLine($"Update Rate: {updateRate} Hz");
|
||||
fileContent.AppendLine($"UDP Mode: {currentMode}");
|
||||
fileContent.AppendLine($"Old System: {oldSystemBoneCount} bones, {totalOldBlendShapes} blend shapes ({oldSystemMeshCount} meshes × {oldSystemBlendShapesPerMesh})");
|
||||
fileContent.AppendLine($"New System: {newSystemBones} bones, {newSystemBlendShapes} blend shapes");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// 3 Player setup bandwidth
|
||||
int players = 3;
|
||||
float oldTotal = jsonBandwidthKBps * players;
|
||||
float newTotal = binaryBandwidthKBps * players;
|
||||
fileContent.AppendLine("3 PLAYER SETUP BANDWIDTH:");
|
||||
fileContent.AppendLine($"Old System: {oldTotal:F1} KB/s total");
|
||||
fileContent.AppendLine($"New System: {newTotal:F1} KB/s total");
|
||||
fileContent.AppendLine($"Bandwidth Savings: {oldTotal - newTotal:F1} KB/s ({((oldTotal - newTotal) / oldTotal) * 100:F1}% reduction)");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// Data usage per hour
|
||||
float oldMBPerHour = (jsonBandwidthKBps * 3600) / 1024;
|
||||
float newMBPerHour = (binaryBandwidthKBps * 3600) / 1024;
|
||||
fileContent.AppendLine("DATA USAGE PER HOUR (single player):");
|
||||
fileContent.AppendLine($"Old System: {oldMBPerHour:F1} MB/hour");
|
||||
fileContent.AppendLine($"New System: {newMBPerHour:F1} MB/hour");
|
||||
fileContent.AppendLine($"Savings: {oldMBPerHour - newMBPerHour:F1} MB/hour ({((oldMBPerHour - newMBPerHour) / oldMBPerHour) * 100:F1}% reduction)");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// Code complexity
|
||||
float codeReduction = (1f - (float)newSystemTotalLines / oldSystemTotalLines) * 100f;
|
||||
fileContent.AppendLine("CODE COMPLEXITY:");
|
||||
fileContent.AppendLine($"Old System: ~{oldSystemTotalLines:N0} lines (6 components + Python server)");
|
||||
fileContent.AppendLine($"New System: ~{newSystemTotalLines:N0} lines (2 components, no server)");
|
||||
fileContent.AppendLine($"Code Reduction: {codeReduction:F1}% ({oldSystemTotalLines - newSystemTotalLines:N0} lines removed)");
|
||||
fileContent.AppendLine();
|
||||
|
||||
// Performance characteristics
|
||||
fileContent.AppendLine("PERFORMANCE CHARACTERISTICS:");
|
||||
fileContent.AppendLine("Old System: HTTP request/response, JSON parsing, server dependency");
|
||||
fileContent.AppendLine("New System: UDP broadcast, binary format, peer-to-peer");
|
||||
fileContent.AppendLine("Latency: Local network UDP (~1-10ms vs HTTP ~10-50ms)");
|
||||
fileContent.AppendLine("Infrastructure: None required (vs Python server)");
|
||||
|
||||
// Write to file
|
||||
string filePath = Path.Combine(Application.dataPath, outputFileName);
|
||||
File.WriteAllText(filePath, fileContent.ToString());
|
||||
|
||||
Debug.Log($"Comparison data saved to: {filePath}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to save comparison to file: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (GUI.Button(new Rect(10, Screen.height - 40, 200, 30), "Run Comparison"))
|
||||
{
|
||||
RunComparison();
|
||||
}
|
||||
|
||||
if (GUI.Button(new Rect(220, Screen.height - 40, 150, 30), "Save to File"))
|
||||
{
|
||||
var jsonSize = CalculateJSONSystemSize();
|
||||
var binarySize = CalculateUDPBinarySize();
|
||||
SaveComparisonToFile(jsonSize, binarySize);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/AvatarSyncComparison.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/AvatarSyncComparison.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44ddcc99fb37a434296ae65e9c186f43
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
142
Unity-Master/Assets/Scripts/LocalTestingSetup.md
Normal file
142
Unity-Master/Assets/Scripts/LocalTestingSetup.md
Normal file
@ -0,0 +1,142 @@
|
||||
# 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!
|
||||
7
Unity-Master/Assets/Scripts/LocalTestingSetup.md.meta
Normal file
7
Unity-Master/Assets/Scripts/LocalTestingSetup.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6945fe49cfac414eb744e574611562c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
66
Unity-Master/Assets/Scripts/README_AvatarSync.md
Normal file
66
Unity-Master/Assets/Scripts/README_AvatarSync.md
Normal file
@ -0,0 +1,66 @@
|
||||
# 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!
|
||||
7
Unity-Master/Assets/Scripts/README_AvatarSync.md.meta
Normal file
7
Unity-Master/Assets/Scripts/README_AvatarSync.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5cfc93a5dbeee345a3993c27049b134
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
178
Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md
Normal file
178
Unity-Master/Assets/Scripts/README_UDP_AvatarSync.md
Normal file
@ -0,0 +1,178 @@
|
||||
# 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!
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a81e1eb398089c45a8cf945c42c940c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
580
Unity-Master/Assets/Scripts/RPMFacialAnimator.cs
Normal file
580
Unity-Master/Assets/Scripts/RPMFacialAnimator.cs
Normal file
@ -0,0 +1,580 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class FacialExpression
|
||||
{
|
||||
public string name;
|
||||
public BlendShapeWeight[] blendShapeWeights;
|
||||
[Range(0.1f, 5f)]
|
||||
public float duration = 1f;
|
||||
[Range(0f, 2f)]
|
||||
public float holdTime = 0.5f;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class BlendShapeWeight
|
||||
{
|
||||
public string blendShapeName;
|
||||
[Range(0f, 1f)]
|
||||
public float weight = 0.5f;
|
||||
}
|
||||
|
||||
public class RPMFacialAnimator : MonoBehaviour
|
||||
{
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform avatarRoot;
|
||||
[SerializeField] private string headRendererName = "Renderer_Head";
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private bool autoPlay = true;
|
||||
[SerializeField] private bool loopAnimations = true;
|
||||
[SerializeField] private float timeBetweenExpressions = 2f;
|
||||
[SerializeField] private bool smoothTransitions = true;
|
||||
[SerializeField] private AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
[Header("Predefined Expressions")]
|
||||
[SerializeField] private FacialExpression[] facialExpressions;
|
||||
|
||||
[Header("Manual Controls")]
|
||||
[SerializeField] private bool enableManualControls = true;
|
||||
[Range(0f, 1f)] public float mouthSmile = 0f;
|
||||
[Range(0f, 1f)] public float mouthOpen = 0f;
|
||||
[Range(0f, 1f)] public float eyeBlinkLeft = 0f;
|
||||
[Range(0f, 1f)] public float eyeBlinkRight = 0f;
|
||||
[Range(0f, 1f)] public float browInnerUp = 0f;
|
||||
[Range(0f, 1f)] public float cheekPuff = 0f;
|
||||
[Range(0f, 1f)] public float jawOpen = 0f;
|
||||
|
||||
[Header("Auto Blink Settings")]
|
||||
[SerializeField] private bool enableAutoBlink = true;
|
||||
[SerializeField] private float blinkInterval = 3f;
|
||||
[SerializeField] private float blinkDuration = 0.15f;
|
||||
[SerializeField] private float blinkRandomness = 1f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool listAllBlendShapes = false;
|
||||
|
||||
private SkinnedMeshRenderer headRenderer;
|
||||
private Dictionary<string, int> blendShapeIndexMap;
|
||||
private Coroutine expressionCoroutine;
|
||||
private Coroutine blinkCoroutine;
|
||||
private bool isAnimating = false;
|
||||
|
||||
// RPM ARKit Blendshape names for reference
|
||||
private readonly string[] arkitBlendShapes = {
|
||||
// Eyes
|
||||
"eyeBlinkLeft", "eyeBlinkRight", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft",
|
||||
"eyeSquintLeft", "eyeWideLeft", "eyeLookDownRight", "eyeLookInRight", "eyeLookOutRight", "eyeLookUpRight",
|
||||
"eyeSquintRight", "eyeWideRight",
|
||||
// Jaw
|
||||
"jawForward", "jawLeft", "jawRight", "jawOpen",
|
||||
// Mouth
|
||||
"mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight", "mouthSmileLeft", "mouthSmileRight",
|
||||
"mouthFrownLeft", "mouthFrownRight", "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
|
||||
"mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", "mouthPressLeft", "mouthPressRight",
|
||||
"mouthLowerDownLeft", "mouthLowerDownRight", "mouthUpperUpLeft", "mouthUpperUpRight",
|
||||
// Brows
|
||||
"browDownLeft", "browDownRight", "browInnerUp", "browOuterUpLeft", "browOuterUpRight",
|
||||
// Cheeks
|
||||
"cheekPuff", "cheekSquintLeft", "cheekSquintRight",
|
||||
// Nose
|
||||
"noseSneerLeft", "noseSneerRight",
|
||||
// Tongue
|
||||
"tongueOut",
|
||||
// Additional
|
||||
"mouthOpen", "mouthSmile", "eyesClosed", "eyesLookUp", "eyesLookDown"
|
||||
};
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeAnimator();
|
||||
SetupDefaultExpressions();
|
||||
|
||||
if (autoPlay && !enableManualControls)
|
||||
{
|
||||
StartExpressionAnimation();
|
||||
}
|
||||
|
||||
if (enableAutoBlink)
|
||||
{
|
||||
StartAutoBlinking();
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeAnimator()
|
||||
{
|
||||
// Find avatar root if not assigned
|
||||
if (avatarRoot == null)
|
||||
{
|
||||
avatarRoot = transform;
|
||||
}
|
||||
|
||||
// Find the head renderer
|
||||
Transform headRendererTransform = FindChildRecursive(avatarRoot, headRendererName);
|
||||
|
||||
if (headRendererTransform == null)
|
||||
{
|
||||
Debug.LogError($"RPMFacialAnimator: Could not find child object with name '{headRendererName}' in avatar hierarchy.");
|
||||
return;
|
||||
}
|
||||
|
||||
headRenderer = headRendererTransform.GetComponent<SkinnedMeshRenderer>();
|
||||
|
||||
if (headRenderer == null)
|
||||
{
|
||||
Debug.LogError($"RPMFacialAnimator: '{headRendererName}' does not have a SkinnedMeshRenderer component.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build blend shape index map
|
||||
BuildBlendShapeIndexMap();
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"RPMFacialAnimator: Initialized with {blendShapeIndexMap.Count} blend shapes on {headRendererName}");
|
||||
}
|
||||
|
||||
if (listAllBlendShapes)
|
||||
{
|
||||
ListAllBlendShapes();
|
||||
}
|
||||
}
|
||||
|
||||
void BuildBlendShapeIndexMap()
|
||||
{
|
||||
blendShapeIndexMap = new Dictionary<string, int>();
|
||||
|
||||
if (headRenderer?.sharedMesh == null) return;
|
||||
|
||||
for (int i = 0; i < headRenderer.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
string blendShapeName = headRenderer.sharedMesh.GetBlendShapeName(i);
|
||||
blendShapeIndexMap[blendShapeName] = i;
|
||||
}
|
||||
}
|
||||
|
||||
void ListAllBlendShapes()
|
||||
{
|
||||
if (headRenderer?.sharedMesh == null) return;
|
||||
|
||||
Debug.Log("=== RPM Avatar Blend Shapes ===");
|
||||
for (int i = 0; i < headRenderer.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
string name = headRenderer.sharedMesh.GetBlendShapeName(i);
|
||||
bool isARKit = Array.Exists(arkitBlendShapes, bs => bs == name);
|
||||
Debug.Log($"Blend Shape {i}: {name} {(isARKit ? "(ARKit)" : "(Custom)")}");
|
||||
}
|
||||
}
|
||||
|
||||
Transform FindChildRecursive(Transform parent, string childName)
|
||||
{
|
||||
foreach (Transform child in parent)
|
||||
{
|
||||
if (child.name == childName)
|
||||
return child;
|
||||
|
||||
Transform found = FindChildRecursive(child, childName);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void SetupDefaultExpressions()
|
||||
{
|
||||
if (facialExpressions == null || facialExpressions.Length == 0)
|
||||
{
|
||||
facialExpressions = new FacialExpression[]
|
||||
{
|
||||
// Happy Expression
|
||||
new FacialExpression
|
||||
{
|
||||
name = "Happy",
|
||||
duration = 1.5f,
|
||||
holdTime = 1f,
|
||||
blendShapeWeights = new BlendShapeWeight[]
|
||||
{
|
||||
new BlendShapeWeight { blendShapeName = "mouthSmile", weight = 0.9f },
|
||||
new BlendShapeWeight { blendShapeName = "mouthSmileLeft", weight = 0.8f },
|
||||
new BlendShapeWeight { blendShapeName = "mouthSmileRight", weight = 0.8f },
|
||||
new BlendShapeWeight { blendShapeName = "cheekSquintLeft", weight = 0.5f },
|
||||
new BlendShapeWeight { blendShapeName = "cheekSquintRight", weight = 0.5f }
|
||||
}
|
||||
},
|
||||
|
||||
// Surprised Expression
|
||||
new FacialExpression
|
||||
{
|
||||
name = "Surprised",
|
||||
duration = 1f,
|
||||
holdTime = 0.8f,
|
||||
blendShapeWeights = new BlendShapeWeight[]
|
||||
{
|
||||
new BlendShapeWeight { blendShapeName = "mouthOpen", weight = 0.8f },
|
||||
new BlendShapeWeight { blendShapeName = "jawOpen", weight = 0.6f },
|
||||
new BlendShapeWeight { blendShapeName = "eyeWideLeft", weight = 1.0f },
|
||||
new BlendShapeWeight { blendShapeName = "eyeWideRight", weight = 1.0f },
|
||||
new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.9f },
|
||||
new BlendShapeWeight { blendShapeName = "browOuterUpLeft", weight = 0.7f },
|
||||
new BlendShapeWeight { blendShapeName = "browOuterUpRight", weight = 0.7f }
|
||||
}
|
||||
},
|
||||
|
||||
// Sad Expression
|
||||
new FacialExpression
|
||||
{
|
||||
name = "Sad",
|
||||
duration = 2f,
|
||||
holdTime = 1.5f,
|
||||
blendShapeWeights = new BlendShapeWeight[]
|
||||
{
|
||||
new BlendShapeWeight { blendShapeName = "mouthFrownLeft", weight = 0.8f },
|
||||
new BlendShapeWeight { blendShapeName = "mouthFrownRight", weight = 0.8f },
|
||||
new BlendShapeWeight { blendShapeName = "browDownLeft", weight = 0.6f },
|
||||
new BlendShapeWeight { blendShapeName = "browDownRight", weight = 0.6f },
|
||||
new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.5f }
|
||||
}
|
||||
},
|
||||
|
||||
// Angry Expression
|
||||
new FacialExpression
|
||||
{
|
||||
name = "Angry",
|
||||
duration = 1.2f,
|
||||
holdTime = 1f,
|
||||
blendShapeWeights = new BlendShapeWeight[]
|
||||
{
|
||||
new BlendShapeWeight { blendShapeName = "browDownLeft", weight = 1.0f },
|
||||
new BlendShapeWeight { blendShapeName = "browDownRight", weight = 1.0f },
|
||||
new BlendShapeWeight { blendShapeName = "eyeSquintLeft", weight = 0.7f },
|
||||
new BlendShapeWeight { blendShapeName = "eyeSquintRight", weight = 0.7f },
|
||||
new BlendShapeWeight { blendShapeName = "mouthFrownLeft", weight = 0.6f },
|
||||
new BlendShapeWeight { blendShapeName = "mouthFrownRight", weight = 0.6f },
|
||||
new BlendShapeWeight { blendShapeName = "noseSneerLeft", weight = 0.5f },
|
||||
new BlendShapeWeight { blendShapeName = "noseSneerRight", weight = 0.5f }
|
||||
}
|
||||
},
|
||||
|
||||
// Thinking Expression
|
||||
new FacialExpression
|
||||
{
|
||||
name = "Thinking",
|
||||
duration = 1.8f,
|
||||
holdTime = 2f,
|
||||
blendShapeWeights = new BlendShapeWeight[]
|
||||
{
|
||||
new BlendShapeWeight { blendShapeName = "browInnerUp", weight = 0.6f },
|
||||
new BlendShapeWeight { blendShapeName = "mouthPucker", weight = 0.5f },
|
||||
new BlendShapeWeight { blendShapeName = "eyeLookUpLeft", weight = 0.4f },
|
||||
new BlendShapeWeight { blendShapeName = "eyeLookUpRight", weight = 0.4f }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (enableManualControls && headRenderer != null)
|
||||
{
|
||||
ApplyManualControls();
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyManualControls()
|
||||
{
|
||||
SetBlendShapeWeight("mouthSmile", mouthSmile);
|
||||
SetBlendShapeWeight("mouthOpen", mouthOpen);
|
||||
SetBlendShapeWeight("eyeBlinkLeft", eyeBlinkLeft);
|
||||
SetBlendShapeWeight("eyeBlinkRight", eyeBlinkRight);
|
||||
SetBlendShapeWeight("browInnerUp", browInnerUp);
|
||||
SetBlendShapeWeight("cheekPuff", cheekPuff);
|
||||
SetBlendShapeWeight("jawOpen", jawOpen);
|
||||
}
|
||||
|
||||
public void SetBlendShapeWeight(string blendShapeName, float weight)
|
||||
{
|
||||
if (headRenderer == null || blendShapeIndexMap == null) return;
|
||||
|
||||
if (blendShapeIndexMap.TryGetValue(blendShapeName, out int index))
|
||||
{
|
||||
headRenderer.SetBlendShapeWeight(index, Mathf.Clamp(weight, 0f, 1f));
|
||||
}
|
||||
else if (showDebugInfo)
|
||||
{
|
||||
Debug.LogWarning($"RPMFacialAnimator: Blend shape '{blendShapeName}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public float GetBlendShapeWeight(string blendShapeName)
|
||||
{
|
||||
if (headRenderer == null || blendShapeIndexMap == null) return 0f;
|
||||
|
||||
if (blendShapeIndexMap.TryGetValue(blendShapeName, out int index))
|
||||
{
|
||||
return headRenderer.GetBlendShapeWeight(index);
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public void PlayExpression(string expressionName)
|
||||
{
|
||||
FacialExpression expression = Array.Find(facialExpressions, expr => expr.name == expressionName);
|
||||
if (expression != null)
|
||||
{
|
||||
StartCoroutine(PlayExpressionCoroutine(expression));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"RPMFacialAnimator: Expression '{expressionName}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayExpression(int expressionIndex)
|
||||
{
|
||||
if (expressionIndex >= 0 && expressionIndex < facialExpressions.Length)
|
||||
{
|
||||
StartCoroutine(PlayExpressionCoroutine(facialExpressions[expressionIndex]));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"RPMFacialAnimator: Expression index {expressionIndex} is out of range.");
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator PlayExpressionCoroutine(FacialExpression expression)
|
||||
{
|
||||
isAnimating = true;
|
||||
|
||||
// Store starting weights
|
||||
Dictionary<string, float> startWeights = new Dictionary<string, float>();
|
||||
foreach (var blendWeight in expression.blendShapeWeights)
|
||||
{
|
||||
startWeights[blendWeight.blendShapeName] = GetBlendShapeWeight(blendWeight.blendShapeName);
|
||||
}
|
||||
|
||||
// Animate to target weights
|
||||
float elapsedTime = 0f;
|
||||
while (elapsedTime < expression.duration)
|
||||
{
|
||||
float progress = elapsedTime / expression.duration;
|
||||
float curveValue = transitionCurve.Evaluate(progress);
|
||||
|
||||
foreach (var blendWeight in expression.blendShapeWeights)
|
||||
{
|
||||
float startWeight = startWeights.ContainsKey(blendWeight.blendShapeName) ?
|
||||
startWeights[blendWeight.blendShapeName] : 0f;
|
||||
float targetWeight = blendWeight.weight;
|
||||
float currentWeight = Mathf.Lerp(startWeight, targetWeight, curveValue);
|
||||
|
||||
SetBlendShapeWeight(blendWeight.blendShapeName, currentWeight);
|
||||
}
|
||||
|
||||
elapsedTime += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Hold the expression
|
||||
if (expression.holdTime > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(expression.holdTime);
|
||||
}
|
||||
|
||||
// Return to neutral
|
||||
elapsedTime = 0f;
|
||||
while (elapsedTime < expression.duration)
|
||||
{
|
||||
float progress = elapsedTime / expression.duration;
|
||||
float curveValue = transitionCurve.Evaluate(progress);
|
||||
|
||||
foreach (var blendWeight in expression.blendShapeWeights)
|
||||
{
|
||||
float currentWeight = Mathf.Lerp(blendWeight.weight, 0f, curveValue);
|
||||
SetBlendShapeWeight(blendWeight.blendShapeName, currentWeight);
|
||||
}
|
||||
|
||||
elapsedTime += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure all weights are reset to 0
|
||||
foreach (var blendWeight in expression.blendShapeWeights)
|
||||
{
|
||||
SetBlendShapeWeight(blendWeight.blendShapeName, 0f);
|
||||
}
|
||||
|
||||
isAnimating = false;
|
||||
}
|
||||
|
||||
public void StartExpressionAnimation()
|
||||
{
|
||||
if (expressionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(expressionCoroutine);
|
||||
}
|
||||
expressionCoroutine = StartCoroutine(ExpressionAnimationLoop());
|
||||
}
|
||||
|
||||
public void StopExpressionAnimation()
|
||||
{
|
||||
if (expressionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(expressionCoroutine);
|
||||
expressionCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator ExpressionAnimationLoop()
|
||||
{
|
||||
while (loopAnimations && !enableManualControls)
|
||||
{
|
||||
if (facialExpressions.Length > 0 && !isAnimating)
|
||||
{
|
||||
int randomIndex = UnityEngine.Random.Range(0, facialExpressions.Length);
|
||||
yield return StartCoroutine(PlayExpressionCoroutine(facialExpressions[randomIndex]));
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(timeBetweenExpressions);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartAutoBlinking()
|
||||
{
|
||||
if (blinkCoroutine != null)
|
||||
{
|
||||
StopCoroutine(blinkCoroutine);
|
||||
}
|
||||
blinkCoroutine = StartCoroutine(AutoBlinkLoop());
|
||||
}
|
||||
|
||||
public void StopAutoBlinking()
|
||||
{
|
||||
if (blinkCoroutine != null)
|
||||
{
|
||||
StopCoroutine(blinkCoroutine);
|
||||
blinkCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator AutoBlinkLoop()
|
||||
{
|
||||
while (enableAutoBlink)
|
||||
{
|
||||
// Wait for random interval
|
||||
float waitTime = blinkInterval + UnityEngine.Random.Range(-blinkRandomness, blinkRandomness);
|
||||
yield return new WaitForSeconds(Mathf.Max(0.5f, waitTime));
|
||||
|
||||
// Blink
|
||||
yield return StartCoroutine(BlinkCoroutine());
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator BlinkCoroutine()
|
||||
{
|
||||
// Close eyes
|
||||
float elapsedTime = 0f;
|
||||
float halfDuration = blinkDuration * 0.5f;
|
||||
|
||||
while (elapsedTime < halfDuration)
|
||||
{
|
||||
float progress = elapsedTime / halfDuration;
|
||||
float blinkWeight = Mathf.Lerp(0f, 1f, progress);
|
||||
|
||||
SetBlendShapeWeight("eyeBlinkLeft", blinkWeight);
|
||||
SetBlendShapeWeight("eyeBlinkRight", blinkWeight);
|
||||
|
||||
elapsedTime += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Open eyes
|
||||
elapsedTime = 0f;
|
||||
while (elapsedTime < halfDuration)
|
||||
{
|
||||
float progress = elapsedTime / halfDuration;
|
||||
float blinkWeight = Mathf.Lerp(1f, 0f, progress);
|
||||
|
||||
SetBlendShapeWeight("eyeBlinkLeft", blinkWeight);
|
||||
SetBlendShapeWeight("eyeBlinkRight", blinkWeight);
|
||||
|
||||
elapsedTime += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Ensure eyes are fully open
|
||||
SetBlendShapeWeight("eyeBlinkLeft", 0f);
|
||||
SetBlendShapeWeight("eyeBlinkRight", 0f);
|
||||
}
|
||||
|
||||
public void ResetAllBlendShapes()
|
||||
{
|
||||
if (headRenderer == null || blendShapeIndexMap == null) return;
|
||||
|
||||
foreach (var kvp in blendShapeIndexMap)
|
||||
{
|
||||
headRenderer.SetBlendShapeWeight(kvp.Value, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API Methods
|
||||
public void Smile() => PlayExpression("Happy");
|
||||
public void Surprise() => PlayExpression("Surprised");
|
||||
public void Frown() => PlayExpression("Sad");
|
||||
public void Angry() => PlayExpression("Angry");
|
||||
public void Think() => PlayExpression("Thinking");
|
||||
|
||||
public void SetAvatarRoot(Transform newRoot)
|
||||
{
|
||||
avatarRoot = newRoot;
|
||||
InitializeAnimator();
|
||||
}
|
||||
|
||||
public void SetManualControlMode(bool enabled)
|
||||
{
|
||||
enableManualControls = enabled;
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
// Stop auto-expressions
|
||||
StopExpressionAnimation();
|
||||
ResetAllBlendShapes();
|
||||
}
|
||||
else if (autoPlay)
|
||||
{
|
||||
// Restart auto-expressions
|
||||
StartExpressionAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
// Update manual controls in real-time during edit mode
|
||||
if (Application.isPlaying && enableManualControls)
|
||||
{
|
||||
// Stop auto-expressions when manual controls are enabled
|
||||
if (expressionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(expressionCoroutine);
|
||||
expressionCoroutine = null;
|
||||
}
|
||||
|
||||
// Reset any ongoing animations
|
||||
isAnimating = false;
|
||||
|
||||
ApplyManualControls();
|
||||
}
|
||||
else if (Application.isPlaying && autoPlay && !enableManualControls)
|
||||
{
|
||||
// Restart auto-expressions if manual controls are disabled
|
||||
if (expressionCoroutine == null)
|
||||
{
|
||||
StartExpressionAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/RPMFacialAnimator.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/RPMFacialAnimator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8b4d07caa16f0e4b81188eb3dc971c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
471
Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs
Normal file
471
Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs
Normal file
@ -0,0 +1,471 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
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 enableBroadcast = true;
|
||||
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform avatarRoot;
|
||||
[SerializeField] private byte playerID = 1; // Unique ID for this player (1-255)
|
||||
[SerializeField] private float updateRate = 30f; // Updates per second
|
||||
|
||||
[Header("Data Selection Mode")]
|
||||
[SerializeField] private bool fullDataMode = false; // Send ALL bones and blend shapes
|
||||
[SerializeField] private bool showFullModeWarning = true;
|
||||
|
||||
[Header("Optimized Mode - Core Bones")]
|
||||
[SerializeField] private string[] priorityBones = {
|
||||
"Hips", "Spine", "Spine1", "Spine2", "Neck", "Head",
|
||||
"LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand",
|
||||
"RightShoulder", "RightArm", "RightForeArm", "RightHand",
|
||||
"LeftUpLeg", "LeftLeg", "LeftFoot",
|
||||
"RightUpLeg", "RightLeg", "RightFoot"
|
||||
};
|
||||
|
||||
[Header("Facial Animation")]
|
||||
[SerializeField] private bool includeFacialData = true;
|
||||
[SerializeField] private int maxBlendShapes = 10; // Most important blend shapes only (optimized mode)
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool logPacketSize = false;
|
||||
|
||||
private UdpClient udpClient;
|
||||
private IPEndPoint broadcastEndPoint;
|
||||
private Dictionary<string, Transform> boneCache;
|
||||
private List<Transform> allBones; // For full data mode
|
||||
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
|
||||
private SkinnedMeshRenderer headMesh;
|
||||
private float lastUpdateTime;
|
||||
private uint sequenceNumber = 0;
|
||||
private int currentBoneCount;
|
||||
private int currentBlendShapeCount;
|
||||
|
||||
// Avatar data structure - supports both optimized and full data modes
|
||||
private struct CompactAvatarData
|
||||
{
|
||||
public byte playerID; // 1 byte
|
||||
public uint sequenceNumber; // 4 bytes
|
||||
public uint timestamp; // 4 bytes (Unity time as uint milliseconds)
|
||||
public bool isFullDataMode; // 1 byte - indicates if this is full or optimized data
|
||||
|
||||
// Root transform (40 bytes)
|
||||
public Vector3 rootPosition; // 12 bytes
|
||||
public Quaternion rootRotation; // 16 bytes
|
||||
public Vector3 rootScale; // 12 bytes
|
||||
|
||||
// Variable-length bone data
|
||||
// Optimized: 20 bones * 28 bytes = 560 bytes
|
||||
// Full mode: All bones * 28 bytes (can be 80+ bones = 2240+ bytes)
|
||||
public BoneTransform[] bones;
|
||||
|
||||
// Variable-length blend shape data
|
||||
// Optimized: 10 floats = 40 bytes
|
||||
// Full mode: All blend shapes = up to 200+ floats = 800+ bytes
|
||||
public float[] blendShapes;
|
||||
|
||||
// Total optimized: ~650 bytes, Full mode: potentially 3000+ bytes
|
||||
}
|
||||
|
||||
private struct BoneTransform
|
||||
{
|
||||
public Vector3 position; // 12 bytes
|
||||
public Quaternion rotation; // 16 bytes
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeNetworking();
|
||||
CacheAvatarComponents();
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"UDP Avatar Broadcaster initialized - Player {playerID}");
|
||||
Debug.Log($"Broadcasting to {broadcastAddress}:{broadcastPort}");
|
||||
Debug.Log($"Priority bones: {priorityBones.Length}");
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeNetworking()
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient();
|
||||
udpClient.EnableBroadcast = true;
|
||||
broadcastEndPoint = new IPEndPoint(IPAddress.Parse(broadcastAddress), broadcastPort);
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log("UDP client initialized successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to initialize UDP client: {e.Message}");
|
||||
enableBroadcast = false;
|
||||
}
|
||||
}
|
||||
|
||||
void CacheAvatarComponents()
|
||||
{
|
||||
if (avatarRoot == null)
|
||||
avatarRoot = transform;
|
||||
|
||||
boneCache = new Dictionary<string, Transform>();
|
||||
allBones = new List<Transform>();
|
||||
allMeshes = new List<SkinnedMeshRenderer>();
|
||||
|
||||
Transform[] allChildren = avatarRoot.GetComponentsInChildren<Transform>();
|
||||
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full data mode: Cache ALL bones with SkinnedMeshRenderer references
|
||||
SkinnedMeshRenderer[] meshRenderers = avatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
HashSet<Transform> uniqueBones = new HashSet<Transform>();
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in meshRenderers)
|
||||
{
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
uniqueBones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allBones.AddRange(uniqueBones);
|
||||
currentBoneCount = allBones.Count;
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Full data mode: Cached {currentBoneCount} bones");
|
||||
|
||||
// Show warning about packet size
|
||||
if (showFullModeWarning)
|
||||
{
|
||||
int estimatedSize = CalculateFullModePacketSize();
|
||||
if (estimatedSize > 1400) // Close to UDP limit
|
||||
{
|
||||
Debug.LogWarning($"Full data mode packet size: ~{estimatedSize} bytes. Consider optimized mode for better network performance.");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: Cache priority bones only
|
||||
foreach (Transform child in allChildren)
|
||||
{
|
||||
foreach (string boneName in priorityBones)
|
||||
{
|
||||
if (child.name.Equals(boneName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!boneCache.ContainsKey(boneName))
|
||||
{
|
||||
boneCache[boneName] = child;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Cached priority bone: {boneName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
currentBoneCount = priorityBones.Length;
|
||||
}
|
||||
|
||||
// Cache facial animation data
|
||||
if (includeFacialData)
|
||||
{
|
||||
SkinnedMeshRenderer[] meshes = avatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full mode: collect all meshes with blend shapes
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.sharedMesh != null && mesh.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
allMeshes.Add(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total blend shape count
|
||||
currentBlendShapeCount = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
{
|
||||
currentBlendShapeCount += mesh.sharedMesh.blendShapeCount;
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Full data mode: Found {allMeshes.Count} meshes with {currentBlendShapeCount} total blend shapes");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: find head mesh only
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.name.ToLower().Contains("head") || mesh.name.ToLower().Contains("face"))
|
||||
{
|
||||
headMesh = mesh;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Found head mesh: {mesh.name} with {mesh.sharedMesh.blendShapeCount} blend shapes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentBlendShapeCount = maxBlendShapes;
|
||||
}
|
||||
}
|
||||
|
||||
string modeStr = fullDataMode ? "FULL" : "OPTIMIZED";
|
||||
Debug.Log($"{modeStr} mode: {currentBoneCount} bones, {currentBlendShapeCount} blend shapes");
|
||||
}
|
||||
|
||||
int CalculateFullModePacketSize()
|
||||
{
|
||||
int headerSize = 1 + 4 + 4 + 1; // playerID + sequence + timestamp + isFullDataMode
|
||||
int rootTransformSize = (3 + 4 + 3) * 4; // position + rotation + scale
|
||||
int bonesSize = 4 + (currentBoneCount * 7 * 4); // bone count + bones * (pos + rot) * float size
|
||||
int blendShapesSize = 4 + (currentBlendShapeCount * 4); // blend shape count + weights * float size
|
||||
return headerSize + rootTransformSize + bonesSize + blendShapesSize;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!enableBroadcast || udpClient == null)
|
||||
return;
|
||||
|
||||
// Rate-limited updates
|
||||
if (Time.time - lastUpdateTime >= 1f / updateRate)
|
||||
{
|
||||
BroadcastAvatarData();
|
||||
lastUpdateTime = Time.time;
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastAvatarData()
|
||||
{
|
||||
try
|
||||
{
|
||||
CompactAvatarData data = new CompactAvatarData
|
||||
{
|
||||
playerID = playerID,
|
||||
sequenceNumber = sequenceNumber++,
|
||||
timestamp = (uint)(Time.time * 1000), // Convert to milliseconds
|
||||
isFullDataMode = fullDataMode,
|
||||
|
||||
rootPosition = avatarRoot.position,
|
||||
rootRotation = avatarRoot.rotation,
|
||||
rootScale = avatarRoot.localScale,
|
||||
|
||||
bones = new BoneTransform[currentBoneCount],
|
||||
blendShapes = new float[currentBlendShapeCount]
|
||||
};
|
||||
|
||||
// Pack bone data based on mode
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full mode: pack all cached bones
|
||||
for (int i = 0; i < allBones.Count; i++)
|
||||
{
|
||||
Transform bone = allBones[i];
|
||||
data.bones[i] = new BoneTransform
|
||||
{
|
||||
position = bone.localPosition,
|
||||
rotation = bone.localRotation
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: pack priority bones
|
||||
for (int i = 0; i < priorityBones.Length; i++)
|
||||
{
|
||||
if (boneCache.TryGetValue(priorityBones[i], out Transform bone))
|
||||
{
|
||||
data.bones[i] = new BoneTransform
|
||||
{
|
||||
position = bone.localPosition,
|
||||
rotation = bone.localRotation
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default values for missing bones
|
||||
data.bones[i] = new BoneTransform
|
||||
{
|
||||
position = Vector3.zero,
|
||||
rotation = Quaternion.identity
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pack facial blend shape data based on mode
|
||||
if (includeFacialData)
|
||||
{
|
||||
if (fullDataMode)
|
||||
{
|
||||
// Full mode: collect all blend shapes from all meshes
|
||||
int blendShapeIndex = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
{
|
||||
if (mesh != null && mesh.sharedMesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
if (blendShapeIndex < data.blendShapes.Length)
|
||||
{
|
||||
data.blendShapes[blendShapeIndex] = mesh.GetBlendShapeWeight(i) / 100f; // Normalize to 0-1
|
||||
blendShapeIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: use head mesh only
|
||||
if (headMesh != null && headMesh.sharedMesh != null)
|
||||
{
|
||||
int blendShapeCount = Mathf.Min(maxBlendShapes, headMesh.sharedMesh.blendShapeCount);
|
||||
for (int i = 0; i < blendShapeCount; i++)
|
||||
{
|
||||
data.blendShapes[i] = headMesh.GetBlendShapeWeight(i) / 100f; // Normalize to 0-1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize to bytes
|
||||
byte[] packetData = SerializeCompactData(data);
|
||||
|
||||
// Send UDP packet
|
||||
udpClient.Send(packetData, packetData.Length, broadcastEndPoint);
|
||||
|
||||
if (logPacketSize && sequenceNumber % 30 == 0) // Log every 30 packets (about once per second)
|
||||
{
|
||||
string modeStr = fullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Sent {modeStr} packet #{sequenceNumber}, size: {packetData.Length} bytes, bones: {currentBoneCount}, blend shapes: {currentBlendShapeCount}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to broadcast avatar data: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
byte[] SerializeCompactData(CompactAvatarData data)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream())
|
||||
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||
{
|
||||
// Header
|
||||
writer.Write(data.playerID);
|
||||
writer.Write(data.sequenceNumber);
|
||||
writer.Write(data.timestamp);
|
||||
writer.Write(data.isFullDataMode);
|
||||
|
||||
// Root transform
|
||||
WriteVector3(writer, data.rootPosition);
|
||||
WriteQuaternion(writer, data.rootRotation);
|
||||
WriteVector3(writer, data.rootScale);
|
||||
|
||||
// Variable-length bones
|
||||
writer.Write(data.bones.Length); // Write bone count first
|
||||
for (int i = 0; i < data.bones.Length; i++)
|
||||
{
|
||||
WriteVector3(writer, data.bones[i].position);
|
||||
WriteQuaternion(writer, data.bones[i].rotation);
|
||||
}
|
||||
|
||||
// Variable-length blend shapes
|
||||
writer.Write(data.blendShapes.Length); // Write blend shape count first
|
||||
for (int i = 0; i < data.blendShapes.Length; i++)
|
||||
{
|
||||
writer.Write(data.blendShapes[i]);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
void WriteVector3(BinaryWriter writer, Vector3 vector)
|
||||
{
|
||||
writer.Write(vector.x);
|
||||
writer.Write(vector.y);
|
||||
writer.Write(vector.z);
|
||||
}
|
||||
|
||||
void WriteQuaternion(BinaryWriter writer, Quaternion quaternion)
|
||||
{
|
||||
writer.Write(quaternion.x);
|
||||
writer.Write(quaternion.y);
|
||||
writer.Write(quaternion.z);
|
||||
writer.Write(quaternion.w);
|
||||
}
|
||||
|
||||
public void SetPlayerID(byte newPlayerID)
|
||||
{
|
||||
playerID = newPlayerID;
|
||||
}
|
||||
|
||||
public void SetUpdateRate(float newRate)
|
||||
{
|
||||
updateRate = Mathf.Clamp(newRate, 1f, 120f);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (udpClient != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void OnApplicationPause(bool pauseStatus)
|
||||
{
|
||||
enableBroadcast = !pauseStatus;
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showDebugInfo) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 10, 300, 240));
|
||||
GUILayout.Label($"UDP Broadcaster - Player {playerID}");
|
||||
GUILayout.Label($"Mode: {(fullDataMode ? "FULL DATA" : "OPTIMIZED")}");
|
||||
GUILayout.Label($"Status: {(enableBroadcast ? "Broadcasting" : "Stopped")}");
|
||||
GUILayout.Label($"Sequence: {sequenceNumber}");
|
||||
GUILayout.Label($"Update Rate: {updateRate:F1} Hz");
|
||||
GUILayout.Label($"Bones: {currentBoneCount}");
|
||||
GUILayout.Label($"Blend Shapes: {currentBlendShapeCount}");
|
||||
|
||||
if (fullDataMode)
|
||||
{
|
||||
int estimatedSize = CalculateFullModePacketSize();
|
||||
GUILayout.Label($"Est. Packet Size: {estimatedSize} bytes");
|
||||
if (estimatedSize > 1400)
|
||||
{
|
||||
GUI.color = Color.yellow;
|
||||
GUILayout.Label("⚠️ Large packet size!");
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button(enableBroadcast ? "Stop Broadcasting" : "Start Broadcasting"))
|
||||
{
|
||||
enableBroadcast = !enableBroadcast;
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/UDPAvatarBroadcaster.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ff57f597890c204caa2168f723f8d93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
710
Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs
Normal file
710
Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs
Normal file
@ -0,0 +1,710 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
public class UDPAvatarReceiver : MonoBehaviour
|
||||
{
|
||||
[Header("Network Configuration")]
|
||||
[SerializeField] private int listenPort = 8080;
|
||||
[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
|
||||
|
||||
[Header("Avatar Configuration")]
|
||||
[SerializeField] private Transform targetAvatarRoot;
|
||||
[SerializeField] private bool smoothTransitions = true;
|
||||
[SerializeField] private float transitionSpeed = 10f;
|
||||
|
||||
[Header("Data Selection - Core Bones (Optimized Mode)")]
|
||||
[SerializeField] private string[] priorityBones = {
|
||||
"Hips", "Spine", "Spine1", "Spine2", "Neck", "Head",
|
||||
"LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand",
|
||||
"RightShoulder", "RightArm", "RightForeArm", "RightHand",
|
||||
"LeftUpLeg", "LeftLeg", "LeftFoot",
|
||||
"RightUpLeg", "RightLeg", "RightFoot"
|
||||
};
|
||||
|
||||
[Header("Synchronization Options")]
|
||||
[SerializeField] private bool syncRootPosition = true;
|
||||
[SerializeField] private bool syncRootRotation = true;
|
||||
[SerializeField] private bool syncRootScale = false;
|
||||
[SerializeField] private bool syncFacialData = true;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugInfo = false;
|
||||
[SerializeField] private bool logReceivedPackets = false;
|
||||
|
||||
private UdpClient udpClient;
|
||||
private Thread udpListenerThread;
|
||||
private bool threadRunning = false;
|
||||
private Dictionary<string, Transform> boneCache;
|
||||
private List<Transform> allBones; // For full data mode
|
||||
private List<SkinnedMeshRenderer> allMeshes; // For full blend shapes
|
||||
private SkinnedMeshRenderer headMesh;
|
||||
|
||||
// Latest received data
|
||||
private CompactAvatarData lastReceivedData;
|
||||
private bool hasNewData = false;
|
||||
private object dataLock = new object();
|
||||
|
||||
// Stats
|
||||
private uint lastSequenceNumber = 0;
|
||||
private int packetsReceived = 0;
|
||||
private int packetsDropped = 0;
|
||||
private float lastPacketTime = 0f;
|
||||
|
||||
// Avatar data structure - supports both optimized and full data modes (matches broadcaster)
|
||||
private struct CompactAvatarData
|
||||
{
|
||||
public byte playerID;
|
||||
public uint sequenceNumber;
|
||||
public uint timestamp;
|
||||
public bool isFullDataMode; // indicates if this is full or optimized data
|
||||
|
||||
public Vector3 rootPosition;
|
||||
public Quaternion rootRotation;
|
||||
public Vector3 rootScale;
|
||||
|
||||
public BoneTransform[] bones; // Variable-length
|
||||
public float[] blendShapes; // Variable-length
|
||||
}
|
||||
|
||||
private struct BoneTransform
|
||||
{
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (targetAvatarRoot == null)
|
||||
targetAvatarRoot = transform;
|
||||
|
||||
CacheAvatarComponents();
|
||||
|
||||
if (enableReceiver)
|
||||
{
|
||||
StartUDPListener();
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log($"UDP Avatar Receiver initialized");
|
||||
Debug.Log($"Listening on port {listenPort}");
|
||||
Debug.Log($"Target Player ID: {(targetPlayerID == 0 ? "Any" : targetPlayerID.ToString())}");
|
||||
}
|
||||
}
|
||||
|
||||
void CacheAvatarComponents()
|
||||
{
|
||||
boneCache = new Dictionary<string, Transform>();
|
||||
allBones = new List<Transform>();
|
||||
allMeshes = new List<SkinnedMeshRenderer>();
|
||||
|
||||
Transform[] allChildren = targetAvatarRoot.GetComponentsInChildren<Transform>();
|
||||
|
||||
// Cache priority bones for optimized mode
|
||||
foreach (Transform child in allChildren)
|
||||
{
|
||||
foreach (string boneName in priorityBones)
|
||||
{
|
||||
if (child.name.Equals(boneName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!boneCache.ContainsKey(boneName))
|
||||
{
|
||||
boneCache[boneName] = child;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Cached priority bone: {boneName}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache ALL bones for full data mode
|
||||
SkinnedMeshRenderer[] meshRenderers = targetAvatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
HashSet<Transform> uniqueBones = new HashSet<Transform>();
|
||||
|
||||
foreach (SkinnedMeshRenderer smr in meshRenderers)
|
||||
{
|
||||
if (smr.bones != null)
|
||||
{
|
||||
foreach (Transform bone in smr.bones)
|
||||
{
|
||||
if (bone != null)
|
||||
{
|
||||
uniqueBones.Add(bone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allBones.AddRange(uniqueBones);
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Cached {allBones.Count} total bones for full data mode");
|
||||
|
||||
// Cache facial animation data
|
||||
if (syncFacialData)
|
||||
{
|
||||
SkinnedMeshRenderer[] meshes = targetAvatarRoot.GetComponentsInChildren<SkinnedMeshRenderer>();
|
||||
|
||||
// Find head mesh for optimized mode
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.name.ToLower().Contains("head") || mesh.name.ToLower().Contains("face"))
|
||||
{
|
||||
headMesh = mesh;
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"Found head mesh: {mesh.name} with {mesh.sharedMesh.blendShapeCount} blend shapes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache all meshes with blend shapes for full data mode
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (mesh.sharedMesh != null && mesh.sharedMesh.blendShapeCount > 0)
|
||||
{
|
||||
allMeshes.Add(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
int totalBlendShapes = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
totalBlendShapes += mesh.sharedMesh.blendShapeCount;
|
||||
Debug.Log($"Found {allMeshes.Count} meshes with {totalBlendShapes} total blend shapes for full data mode");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Receiver ready: {boneCache.Count}/{priorityBones.Length} priority bones, {allBones.Count} total bones");
|
||||
}
|
||||
|
||||
void StartUDPListener()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
// Create UDP client with port reuse for local testing
|
||||
udpClient = new UdpClient();
|
||||
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, listenPort));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard UDP client binding
|
||||
udpClient = new UdpClient(listenPort);
|
||||
}
|
||||
|
||||
threadRunning = true;
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerThread));
|
||||
udpListenerThread.IsBackground = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
if (showDebugInfo)
|
||||
Debug.Log($"UDP Avatar Receiver started on port {listenPort} (filtering binary avatar data only, Port sharing: {allowPortSharing})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
Debug.LogWarning($"Failed to start UDP listener with port sharing: {e.Message}");
|
||||
Debug.LogWarning("Trying with different port...");
|
||||
TryAlternativePort();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Failed to start UDP listener: {e.Message}");
|
||||
enableReceiver = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TryAlternativePort()
|
||||
{
|
||||
// Try a few alternative ports for local testing
|
||||
int[] alternativePorts = { 8081, 8082, 8083, 8084, 8085 };
|
||||
|
||||
foreach (int port in alternativePorts)
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient(port);
|
||||
threadRunning = true;
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerThread));
|
||||
udpListenerThread.IsBackground = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
Debug.Log($"UDP listener started on alternative port {port}");
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Try next port
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogError("Failed to start UDP listener on any available port");
|
||||
enableReceiver = false;
|
||||
}
|
||||
|
||||
void UDPListenerThread()
|
||||
{
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
|
||||
|
||||
while (threadRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] data = udpClient.Receive(ref remoteEndPoint);
|
||||
|
||||
if (data.Length > 0 && IsAvatarData(data))
|
||||
{
|
||||
CompactAvatarData avatarData = DeserializeCompactData(data);
|
||||
|
||||
// Check if this is from the target player (0 means accept from any player)
|
||||
if (targetPlayerID == 0 || avatarData.playerID == targetPlayerID)
|
||||
{
|
||||
// Check for packet loss
|
||||
if (avatarData.sequenceNumber > lastSequenceNumber + 1)
|
||||
{
|
||||
packetsDropped += (int)(avatarData.sequenceNumber - lastSequenceNumber - 1);
|
||||
}
|
||||
|
||||
lastSequenceNumber = avatarData.sequenceNumber;
|
||||
packetsReceived++;
|
||||
|
||||
// Store the new data (thread-safe)
|
||||
lock (dataLock)
|
||||
{
|
||||
lastReceivedData = avatarData;
|
||||
hasNewData = true;
|
||||
}
|
||||
|
||||
if (logReceivedPackets && packetsReceived % 30 == 0)
|
||||
{
|
||||
string modeStr = avatarData.isFullDataMode ? "FULL" : "OPT";
|
||||
Debug.Log($"Received {modeStr} packet #{avatarData.sequenceNumber} from player {avatarData.playerID}, size: {data.Length} bytes, bones: {avatarData.bones?.Length ?? 0}, blend shapes: {avatarData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (threadRunning) // Only log errors if we're supposed to be running
|
||||
{
|
||||
Debug.LogError($"UDP receive error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAvatarData(byte[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Avatar data should be binary and typically larger than JSON experiment messages
|
||||
if (data.Length < 20) // Avatar data should have at least header info
|
||||
return false;
|
||||
|
||||
// Check if it looks like text/JSON (experiment control message)
|
||||
// JSON messages will be valid UTF-8 text starting with '{'
|
||||
try
|
||||
{
|
||||
string text = Encoding.UTF8.GetString(data);
|
||||
if (text.TrimStart().StartsWith("{") && text.Contains("\"command\""))
|
||||
{
|
||||
// This is likely an experiment control message, not avatar data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If it fails to decode as UTF-8, it's likely binary avatar data
|
||||
}
|
||||
|
||||
// Additional check: avatar data should start with a reasonable playerID (0-255)
|
||||
// and have a structure that makes sense
|
||||
using (MemoryStream stream = new MemoryStream(data))
|
||||
using (BinaryReader reader = new BinaryReader(stream))
|
||||
{
|
||||
// Basic structure check - should be able to read at least the header
|
||||
if (data.Length >= 17) // byte + uint32 + uint32 + bool + 12 bytes for Vector3
|
||||
{
|
||||
byte playerID = reader.ReadByte();
|
||||
uint sequenceNumber = reader.ReadUInt32();
|
||||
uint timestamp = reader.ReadUInt32();
|
||||
bool isFullDataMode = reader.ReadBoolean();
|
||||
|
||||
// Basic sanity checks
|
||||
if (playerID <= 10 && sequenceNumber < uint.MaxValue / 2) // Reasonable values
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any parsing fails, assume it's not valid avatar data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CompactAvatarData DeserializeCompactData(byte[] data)
|
||||
{
|
||||
using (MemoryStream stream = new MemoryStream(data))
|
||||
using (BinaryReader reader = new BinaryReader(stream))
|
||||
{
|
||||
CompactAvatarData result = new CompactAvatarData
|
||||
{
|
||||
// Header
|
||||
playerID = reader.ReadByte(),
|
||||
sequenceNumber = reader.ReadUInt32(),
|
||||
timestamp = reader.ReadUInt32(),
|
||||
isFullDataMode = reader.ReadBoolean(),
|
||||
|
||||
// Root transform
|
||||
rootPosition = ReadVector3(reader),
|
||||
rootRotation = ReadQuaternion(reader),
|
||||
rootScale = ReadVector3(reader)
|
||||
};
|
||||
|
||||
// Variable-length bones
|
||||
int boneCount = reader.ReadInt32();
|
||||
result.bones = new BoneTransform[boneCount];
|
||||
for (int i = 0; i < boneCount; i++)
|
||||
{
|
||||
result.bones[i] = new BoneTransform
|
||||
{
|
||||
position = ReadVector3(reader),
|
||||
rotation = ReadQuaternion(reader)
|
||||
};
|
||||
}
|
||||
|
||||
// Variable-length blend shapes
|
||||
int blendShapeCount = reader.ReadInt32();
|
||||
result.blendShapes = new float[blendShapeCount];
|
||||
for (int i = 0; i < blendShapeCount; i++)
|
||||
{
|
||||
result.blendShapes[i] = reader.ReadSingle();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 ReadVector3(BinaryReader reader)
|
||||
{
|
||||
return new Vector3(
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle()
|
||||
);
|
||||
}
|
||||
|
||||
Quaternion ReadQuaternion(BinaryReader reader)
|
||||
{
|
||||
return new Quaternion(
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle()
|
||||
);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!enableReceiver)
|
||||
return;
|
||||
|
||||
// Apply new data if available
|
||||
lock (dataLock)
|
||||
{
|
||||
if (hasNewData)
|
||||
{
|
||||
ApplyAvatarData(lastReceivedData);
|
||||
hasNewData = false;
|
||||
lastPacketTime = Time.time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyAvatarData(CompactAvatarData data)
|
||||
{
|
||||
// Apply root transform
|
||||
if (syncRootPosition)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.position = Vector3.Lerp(
|
||||
targetAvatarRoot.position,
|
||||
data.rootPosition,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.position = data.rootPosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRootRotation)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.rotation = Quaternion.Lerp(
|
||||
targetAvatarRoot.rotation,
|
||||
data.rootRotation,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.rotation = data.rootRotation;
|
||||
}
|
||||
}
|
||||
|
||||
if (syncRootScale)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
targetAvatarRoot.localScale = Vector3.Lerp(
|
||||
targetAvatarRoot.localScale,
|
||||
data.rootScale,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAvatarRoot.localScale = data.rootScale;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply bone transforms based on data mode
|
||||
if (data.isFullDataMode)
|
||||
{
|
||||
// Full data mode: apply to all bones based on order
|
||||
for (int i = 0; i < Mathf.Min(data.bones.Length, allBones.Count); i++)
|
||||
{
|
||||
Transform bone = allBones[i];
|
||||
if (bone != null)
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
bone.localPosition = Vector3.Lerp(
|
||||
bone.localPosition,
|
||||
data.bones[i].position,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
|
||||
bone.localRotation = Quaternion.Lerp(
|
||||
bone.localRotation,
|
||||
data.bones[i].rotation,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
bone.localPosition = data.bones[i].position;
|
||||
bone.localRotation = data.bones[i].rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: apply to priority bones
|
||||
for (int i = 0; i < Mathf.Min(data.bones.Length, priorityBones.Length); i++)
|
||||
{
|
||||
if (boneCache.TryGetValue(priorityBones[i], out Transform bone))
|
||||
{
|
||||
if (smoothTransitions)
|
||||
{
|
||||
bone.localPosition = Vector3.Lerp(
|
||||
bone.localPosition,
|
||||
data.bones[i].position,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
|
||||
bone.localRotation = Quaternion.Lerp(
|
||||
bone.localRotation,
|
||||
data.bones[i].rotation,
|
||||
Time.deltaTime * transitionSpeed
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
bone.localPosition = data.bones[i].position;
|
||||
bone.localRotation = data.bones[i].rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply facial blend shapes based on data mode
|
||||
if (syncFacialData)
|
||||
{
|
||||
if (data.isFullDataMode)
|
||||
{
|
||||
// Full data mode: apply all blend shapes to all meshes
|
||||
int blendShapeIndex = 0;
|
||||
foreach (var mesh in allMeshes)
|
||||
{
|
||||
if (mesh != null && mesh.sharedMesh != null)
|
||||
{
|
||||
for (int i = 0; i < mesh.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
if (blendShapeIndex < data.blendShapes.Length)
|
||||
{
|
||||
float targetWeight = data.blendShapes[blendShapeIndex] * 100f; // Convert back to 0-100 range
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
float currentWeight = mesh.GetBlendShapeWeight(i);
|
||||
float newWeight = Mathf.Lerp(currentWeight, targetWeight, Time.deltaTime * transitionSpeed);
|
||||
mesh.SetBlendShapeWeight(i, newWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
mesh.SetBlendShapeWeight(i, targetWeight);
|
||||
}
|
||||
blendShapeIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optimized mode: apply to head mesh only
|
||||
if (headMesh != null && headMesh.sharedMesh != null)
|
||||
{
|
||||
int blendShapeCount = Mathf.Min(data.blendShapes.Length, headMesh.sharedMesh.blendShapeCount);
|
||||
for (int i = 0; i < blendShapeCount; i++)
|
||||
{
|
||||
float targetWeight = data.blendShapes[i] * 100f; // Convert back to 0-100 range
|
||||
|
||||
if (smoothTransitions)
|
||||
{
|
||||
float currentWeight = headMesh.GetBlendShapeWeight(i);
|
||||
float newWeight = Mathf.Lerp(currentWeight, targetWeight, Time.deltaTime * transitionSpeed);
|
||||
headMesh.SetBlendShapeWeight(i, newWeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
headMesh.SetBlendShapeWeight(i, targetWeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTargetPlayerID(byte playerID)
|
||||
{
|
||||
targetPlayerID = playerID;
|
||||
}
|
||||
|
||||
public bool IsReceivingData()
|
||||
{
|
||||
return Time.time - lastPacketTime < 1f; // Consider "receiving" if we got data in the last second
|
||||
}
|
||||
|
||||
public float GetPacketLossRate()
|
||||
{
|
||||
int totalExpected = packetsReceived + packetsDropped;
|
||||
return totalExpected > 0 ? (float)packetsDropped / totalExpected : 0f;
|
||||
}
|
||||
|
||||
void StopUDPListener()
|
||||
{
|
||||
threadRunning = false;
|
||||
|
||||
if (udpClient != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient.Dispose();
|
||||
udpClient = null;
|
||||
}
|
||||
|
||||
if (udpListenerThread != null)
|
||||
{
|
||||
udpListenerThread.Join(1000); // Wait up to 1 second for thread to finish
|
||||
udpListenerThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopUDPListener();
|
||||
}
|
||||
|
||||
void OnApplicationPause(bool pauseStatus)
|
||||
{
|
||||
if (pauseStatus)
|
||||
{
|
||||
StopUDPListener();
|
||||
}
|
||||
else if (enableReceiver)
|
||||
{
|
||||
StartUDPListener();
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showDebugInfo) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(320, 10, 300, 260));
|
||||
GUILayout.Label($"UDP Avatar Receiver (Binary Only)");
|
||||
GUILayout.Label($"Status: {(IsReceivingData() ? "Receiving" : "No Data")}");
|
||||
|
||||
// Show last received data mode
|
||||
if (hasNewData)
|
||||
{
|
||||
string modeStr = lastReceivedData.isFullDataMode ? "FULL DATA" : "OPTIMIZED";
|
||||
GUILayout.Label($"Last Data Mode: {modeStr}");
|
||||
GUILayout.Label($"Bones Received: {lastReceivedData.bones?.Length ?? 0}");
|
||||
GUILayout.Label($"Blend Shapes: {lastReceivedData.blendShapes?.Length ?? 0}");
|
||||
}
|
||||
|
||||
GUILayout.Label($"Listen Port: {GetActualListenPort()} (shared)");
|
||||
GUILayout.Label($"Target Player: {(targetPlayerID == 0 ? "Any" : targetPlayerID.ToString())}");
|
||||
GUILayout.Label($"Packets: {packetsReceived} received, {packetsDropped} dropped");
|
||||
GUILayout.Label($"Packet Loss: {GetPacketLossRate():P1}");
|
||||
GUILayout.Label($"Last Sequence: {lastSequenceNumber}");
|
||||
GUILayout.Label($"Cached: {boneCache.Count} priority, {allBones.Count} total bones");
|
||||
GUILayout.Label($"Port Sharing: {allowPortSharing}");
|
||||
|
||||
if (GUILayout.Button(enableReceiver ? "Stop Receiver" : "Start Receiver"))
|
||||
{
|
||||
enableReceiver = !enableReceiver;
|
||||
if (enableReceiver)
|
||||
StartUDPListener();
|
||||
else
|
||||
StopUDPListener();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
int GetActualListenPort()
|
||||
{
|
||||
if (udpClient?.Client?.LocalEndPoint != null)
|
||||
{
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
}
|
||||
return listenPort;
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/UDPAvatarReceiver.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abba62a4530fda74cb765538e25a69de
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
839
Unity-Master/Assets/Scripts/VRExperimentController.cs
Normal file
839
Unity-Master/Assets/Scripts/VRExperimentController.cs
Normal file
@ -0,0 +1,839 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
/// <summary>
|
||||
/// VR Experiment Controller
|
||||
/// Listens for UDP messages from the supervisor and manages avatars and objects
|
||||
/// based on the current experimental condition.
|
||||
/// </summary>
|
||||
public class VRExperimentController : MonoBehaviour
|
||||
{
|
||||
[Header("Network Settings")]
|
||||
[SerializeField] private int udpPort = 1221;
|
||||
[SerializeField] private bool allowPortSharing = true; // For local testing with multiple components
|
||||
|
||||
[Header("Avatar Assignments")]
|
||||
[SerializeField] private GameObject helpfulAvatar;
|
||||
[SerializeField] private GameObject demotivatingAvatar;
|
||||
[SerializeField] private GameObject nonInteractiveAvatar;
|
||||
[SerializeField] private GameObject practiceAvatar;
|
||||
|
||||
[Header("Object Assignments")]
|
||||
[SerializeField] private GameObject brickObject;
|
||||
[SerializeField] private GameObject paperclipObject;
|
||||
[SerializeField] private GameObject ropeObject;
|
||||
[SerializeField] private GameObject bookObject;
|
||||
|
||||
[Header("Debug Settings")]
|
||||
[SerializeField] private bool enableDebugLogging = true;
|
||||
|
||||
// Network components
|
||||
private UdpClient udpClient;
|
||||
private Thread udpListenerThread;
|
||||
private bool isListening = false;
|
||||
|
||||
// Current experiment state
|
||||
private string currentConditionType = "";
|
||||
private string currentObjectType = "";
|
||||
private int currentConditionIndex = -1;
|
||||
private bool practiceRoundActive = false;
|
||||
|
||||
// Message queue for thread-safe communication
|
||||
private Queue<string> messageQueue = new Queue<string>();
|
||||
private readonly object queueLock = new object();
|
||||
|
||||
// Object mapping for easy access
|
||||
private Dictionary<string, GameObject> objectMap;
|
||||
private Dictionary<string, GameObject> avatarMap;
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeObjectMaps();
|
||||
StartUDPListener();
|
||||
|
||||
// Initially deactivate all objects and avatars
|
||||
DeactivateAllObjects();
|
||||
DeactivateAllAvatars();
|
||||
|
||||
LogMessage("VR Experiment Controller started. Listening on port 1221 (shared with avatar sync). Waiting for supervisor commands...");
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Process messages from the UDP thread in the main thread
|
||||
ProcessMessageQueue();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
StopUDPListener();
|
||||
}
|
||||
|
||||
void OnApplicationPause(bool pauseStatus)
|
||||
{
|
||||
if (pauseStatus)
|
||||
{
|
||||
LogMessage("Application paused - stopping UDP listener");
|
||||
StopUDPListener();
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage("Application unpaused - restarting UDP listener");
|
||||
StartUDPListener();
|
||||
}
|
||||
}
|
||||
|
||||
void OnApplicationFocus(bool hasFocus)
|
||||
{
|
||||
// Don't stop UDP listener when losing focus - supervisor GUI needs to communicate
|
||||
// Only restart if we had stopped for some reason
|
||||
if (hasFocus && !isListening)
|
||||
{
|
||||
LogMessage("Application regained focus - restarting UDP listener");
|
||||
StartUDPListener();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize object and avatar mappings for easy access
|
||||
/// </summary>
|
||||
private void InitializeObjectMaps()
|
||||
{
|
||||
// Initialize object map
|
||||
objectMap = new Dictionary<string, GameObject>
|
||||
{
|
||||
{ "Brick", brickObject },
|
||||
{ "Paperclip", paperclipObject },
|
||||
{ "Rope", ropeObject },
|
||||
{ "Book", bookObject }
|
||||
};
|
||||
|
||||
// Initialize avatar map
|
||||
avatarMap = new Dictionary<string, GameObject>
|
||||
{
|
||||
{ "Helpful", helpfulAvatar },
|
||||
{ "Demotivating", demotivatingAvatar },
|
||||
{ "Non-Interactive", nonInteractiveAvatar }
|
||||
};
|
||||
|
||||
// Validate assignments
|
||||
ValidateAssignments();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate that all required objects and avatars are assigned
|
||||
/// </summary>
|
||||
private void ValidateAssignments()
|
||||
{
|
||||
bool hasErrors = false;
|
||||
|
||||
// Check objects
|
||||
foreach (var kvp in objectMap)
|
||||
{
|
||||
if (kvp.Value == null)
|
||||
{
|
||||
LogError($"Object not assigned: {kvp.Key}");
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check avatars
|
||||
foreach (var kvp in avatarMap)
|
||||
{
|
||||
if (kvp.Value == null)
|
||||
{
|
||||
LogError($"Avatar not assigned: {kvp.Key}");
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check practice avatar
|
||||
if (practiceAvatar == null)
|
||||
{
|
||||
LogError("Practice avatar not assigned!");
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
// Check non-interactive avatar
|
||||
if (nonInteractiveAvatar == null)
|
||||
{
|
||||
LogError("Non-interactive avatar not assigned!");
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
LogError("Please assign all required objects and avatars in the inspector!");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage("All objects and avatars properly assigned.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start listening for UDP messages
|
||||
/// </summary>
|
||||
private void StartUDPListener()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
// Create UDP client with port reuse for local testing
|
||||
udpClient = new UdpClient();
|
||||
udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, udpPort));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard UDP client binding
|
||||
udpClient = new UdpClient(udpPort);
|
||||
}
|
||||
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerLoop));
|
||||
udpListenerThread.IsBackground = true;
|
||||
isListening = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
LogMessage($"UDP Experiment Control Listener started on port {udpPort} (filtering JSON messages only, Port sharing: {allowPortSharing})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (allowPortSharing)
|
||||
{
|
||||
LogError($"Failed to start UDP listener with port sharing: {e.Message}");
|
||||
LogMessage("Trying with different port...");
|
||||
TryAlternativePort();
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError($"Failed to start UDP listener: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try alternative ports for local testing
|
||||
/// </summary>
|
||||
private void TryAlternativePort()
|
||||
{
|
||||
// Try a few alternative ports for local testing
|
||||
int[] alternativePorts = { 1222, 1223, 1224, 1225, 1226 };
|
||||
|
||||
foreach (int port in alternativePorts)
|
||||
{
|
||||
try
|
||||
{
|
||||
udpClient = new UdpClient(port);
|
||||
udpListenerThread = new Thread(new ThreadStart(UDPListenerLoop));
|
||||
udpListenerThread.IsBackground = true;
|
||||
isListening = true;
|
||||
udpListenerThread.Start();
|
||||
|
||||
LogMessage($"UDP Experiment Control Listener started on alternative port {port}");
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Try next port
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
LogError("Failed to start UDP listener on any available port");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening for UDP messages
|
||||
/// </summary>
|
||||
private void StopUDPListener()
|
||||
{
|
||||
isListening = false;
|
||||
|
||||
if (udpClient != null)
|
||||
{
|
||||
udpClient.Close();
|
||||
udpClient = null;
|
||||
}
|
||||
|
||||
if (udpListenerThread != null && udpListenerThread.IsAlive)
|
||||
{
|
||||
udpListenerThread.Join(1000); // Wait up to 1 second
|
||||
if (udpListenerThread.IsAlive)
|
||||
{
|
||||
udpListenerThread.Abort();
|
||||
}
|
||||
udpListenerThread = null;
|
||||
}
|
||||
|
||||
LogMessage("UDP Listener stopped");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UDP listener loop (runs in separate thread)
|
||||
/// </summary>
|
||||
private void UDPListenerLoop()
|
||||
{
|
||||
while (isListening)
|
||||
{
|
||||
try
|
||||
{
|
||||
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, udpPort);
|
||||
byte[] data = udpClient.Receive(ref remoteEndPoint);
|
||||
|
||||
// Check if this looks like a JSON experiment control message
|
||||
if (IsExperimentControlMessage(data))
|
||||
{
|
||||
string message = Encoding.UTF8.GetString(data);
|
||||
|
||||
// Add message to queue for main thread processing
|
||||
lock (queueLock)
|
||||
{
|
||||
messageQueue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
// If it's not an experiment control message (likely avatar data), ignore it
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (isListening) // Only log if we're still supposed to be listening
|
||||
{
|
||||
LogError($"UDP Listener error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the received data is an experiment control message (JSON) vs avatar data (binary)
|
||||
/// </summary>
|
||||
private bool IsExperimentControlMessage(byte[] data)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Experiment control messages are JSON and should be reasonably small
|
||||
if (data.Length > 1024) // Avatar data is typically much larger
|
||||
return false;
|
||||
|
||||
// Try to decode as UTF-8 text
|
||||
string text = Encoding.UTF8.GetString(data);
|
||||
|
||||
// Check if it looks like JSON (starts with '{' and contains "command")
|
||||
if (text.TrimStart().StartsWith("{") && text.Contains("\"command\""))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't decode as UTF-8 or any other error, it's probably binary avatar data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process messages from the queue in the main thread
|
||||
/// </summary>
|
||||
private void ProcessMessageQueue()
|
||||
{
|
||||
lock (queueLock)
|
||||
{
|
||||
while (messageQueue.Count > 0)
|
||||
{
|
||||
string message = messageQueue.Dequeue();
|
||||
ProcessUDPMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process incoming UDP message
|
||||
/// </summary>
|
||||
private void ProcessUDPMessage(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogMessage($"Received UDP message: {message}");
|
||||
|
||||
// Parse JSON message
|
||||
var messageData = JsonConvert.DeserializeObject<Dictionary<string, object>>(message);
|
||||
|
||||
if (messageData.ContainsKey("command"))
|
||||
{
|
||||
string command = messageData["command"].ToString();
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "start_condition":
|
||||
case "next_condition":
|
||||
HandleConditionChange(messageData);
|
||||
break;
|
||||
case "start_practice_round":
|
||||
HandlePracticeRoundStart(messageData);
|
||||
break;
|
||||
case "end_practice_round":
|
||||
HandlePracticeRoundEnd(messageData);
|
||||
break;
|
||||
case "disable_all":
|
||||
HandleDisableAll(messageData);
|
||||
break;
|
||||
default:
|
||||
LogMessage($"Unknown command received: {command}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError($"Failed to process UDP message: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle practice round start command
|
||||
/// </summary>
|
||||
private void HandlePracticeRoundStart(Dictionary<string, object> messageData)
|
||||
{
|
||||
try
|
||||
{
|
||||
int duration = messageData.ContainsKey("duration") ? Convert.ToInt32(messageData["duration"]) : 300;
|
||||
LogMessage($"Practice round started. Duration: {duration} seconds");
|
||||
|
||||
practiceRoundActive = true;
|
||||
|
||||
// Deactivate all objects and regular avatars
|
||||
DeactivateAllObjects();
|
||||
DeactivateAllAvatars();
|
||||
|
||||
// Activate only the practice avatar
|
||||
if (practiceAvatar != null)
|
||||
{
|
||||
practiceAvatar.SetActive(true);
|
||||
LogMessage("Practice avatar activated");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError("Practice avatar not assigned!");
|
||||
}
|
||||
|
||||
// Clear current condition state
|
||||
currentConditionType = "";
|
||||
currentObjectType = "";
|
||||
currentConditionIndex = -1;
|
||||
|
||||
LogMessage("Practice round active - only practice avatar is present");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError($"Failed to handle practice round start: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle practice round end command
|
||||
/// </summary>
|
||||
private void HandlePracticeRoundEnd(Dictionary<string, object> messageData)
|
||||
{
|
||||
try
|
||||
{
|
||||
string reason = messageData.ContainsKey("reason") ? messageData["reason"].ToString() : "unknown";
|
||||
LogMessage($"Practice round ended. Reason: {reason}");
|
||||
|
||||
practiceRoundActive = false;
|
||||
|
||||
// Deactivate practice avatar
|
||||
if (practiceAvatar != null)
|
||||
{
|
||||
practiceAvatar.SetActive(false);
|
||||
LogMessage("Practice avatar deactivated");
|
||||
}
|
||||
|
||||
// Ensure all objects and avatars are deactivated
|
||||
DeactivateAllObjects();
|
||||
DeactivateAllAvatars();
|
||||
|
||||
LogMessage("Practice round completed - all avatars and objects disabled");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError($"Failed to handle practice round end: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle disable all command (when timer expires)
|
||||
/// </summary>
|
||||
private void HandleDisableAll(Dictionary<string, object> messageData)
|
||||
{
|
||||
try
|
||||
{
|
||||
string reason = messageData.ContainsKey("reason") ? messageData["reason"].ToString() : "unknown";
|
||||
LogMessage($"Disable all command received. Reason: {reason}");
|
||||
|
||||
// Deactivate all objects and avatars
|
||||
DeactivateAllObjects();
|
||||
DeactivateAllAvatars();
|
||||
|
||||
// Deactivate practice avatar if active
|
||||
if (practiceAvatar != null)
|
||||
{
|
||||
practiceAvatar.SetActive(false);
|
||||
}
|
||||
|
||||
// Clear current condition state
|
||||
currentConditionType = "";
|
||||
currentObjectType = "";
|
||||
practiceRoundActive = false;
|
||||
|
||||
LogMessage("All objects and avatars disabled - block finished");
|
||||
|
||||
// Trigger condition changed event
|
||||
OnConditionChanged();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError($"Failed to handle disable all command: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle condition change messages
|
||||
/// </summary>
|
||||
private void HandleConditionChange(Dictionary<string, object> messageData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Extract condition data
|
||||
currentConditionType = messageData["condition_type"].ToString();
|
||||
currentObjectType = messageData["object_type"].ToString();
|
||||
currentConditionIndex = Convert.ToInt32(messageData["condition_index"]);
|
||||
|
||||
LogMessage($"Condition changed to: {currentConditionType} with {currentObjectType} (Index: {currentConditionIndex})");
|
||||
|
||||
// End practice round if it was active
|
||||
if (practiceRoundActive)
|
||||
{
|
||||
practiceRoundActive = false;
|
||||
if (practiceAvatar != null)
|
||||
{
|
||||
practiceAvatar.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the scene based on the new condition
|
||||
UpdateSceneForCondition();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogError($"Failed to handle condition change: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the scene based on the current condition
|
||||
/// </summary>
|
||||
private void UpdateSceneForCondition()
|
||||
{
|
||||
// First, deactivate everything
|
||||
DeactivateAllObjects();
|
||||
DeactivateAllAvatars();
|
||||
|
||||
// Deactivate practice avatar
|
||||
if (practiceAvatar != null)
|
||||
{
|
||||
practiceAvatar.SetActive(false);
|
||||
}
|
||||
|
||||
// Activate the required object
|
||||
if (objectMap.ContainsKey(currentObjectType) && objectMap[currentObjectType] != null)
|
||||
{
|
||||
objectMap[currentObjectType].SetActive(true);
|
||||
LogMessage($"Activated object: {currentObjectType}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError($"Object not found or not assigned: {currentObjectType}");
|
||||
}
|
||||
|
||||
// Activate the required avatar (only for non-control conditions)
|
||||
if (currentConditionType != "Control")
|
||||
{
|
||||
if (avatarMap.ContainsKey(currentConditionType) && avatarMap[currentConditionType] != null)
|
||||
{
|
||||
avatarMap[currentConditionType].SetActive(true);
|
||||
LogMessage($"Activated avatar: {currentConditionType}");
|
||||
|
||||
// Handle non-interactive avatar behavior
|
||||
if (currentConditionType == "Non-Interactive")
|
||||
{
|
||||
HandleNonInteractiveAvatar();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError($"Avatar not found or not assigned: {currentConditionType}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage("Control condition - no avatar activated");
|
||||
}
|
||||
|
||||
// Trigger any additional condition-specific behavior
|
||||
OnConditionChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle non-interactive avatar behavior
|
||||
/// </summary>
|
||||
private void HandleNonInteractiveAvatar()
|
||||
{
|
||||
// The non-interactive avatar is a dedicated avatar that should not respond to interactions
|
||||
// You can add specific behavior here, such as:
|
||||
// - Disabling interaction scripts
|
||||
// - Setting the avatar to a passive state
|
||||
// - Disabling speech or gesture systems
|
||||
|
||||
LogMessage("Non-interactive avatar activated - avatar will not respond to interactions");
|
||||
|
||||
// Example: Disable interaction components if they exist
|
||||
var interactionComponents = avatarMap["Non-Interactive"].GetComponents<MonoBehaviour>();
|
||||
foreach (var component in interactionComponents)
|
||||
{
|
||||
// Disable specific interaction scripts (adjust based on your actual component names)
|
||||
if (component.GetType().Name.Contains("Interaction") ||
|
||||
component.GetType().Name.Contains("Speech") ||
|
||||
component.GetType().Name.Contains("Gesture"))
|
||||
{
|
||||
component.enabled = false;
|
||||
LogMessage($"Disabled interaction component: {component.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivate all objects
|
||||
/// </summary>
|
||||
private void DeactivateAllObjects()
|
||||
{
|
||||
foreach (var kvp in objectMap)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
{
|
||||
kvp.Value.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivate all avatars
|
||||
/// </summary>
|
||||
private void DeactivateAllAvatars()
|
||||
{
|
||||
foreach (var kvp in avatarMap)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
{
|
||||
kvp.Value.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when condition changes - override this for custom behavior
|
||||
/// </summary>
|
||||
protected virtual void OnConditionChanged()
|
||||
{
|
||||
// Send Unity event or trigger custom behavior here
|
||||
// This can be overridden by derived classes for specific experiment needs
|
||||
|
||||
if (practiceRoundActive)
|
||||
{
|
||||
LogMessage("Practice round active - only practice avatar is present");
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage($"Condition change complete. Current state: {currentConditionType} - {currentObjectType}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the actual UDP port being used
|
||||
/// </summary>
|
||||
private int GetActualListenPort()
|
||||
{
|
||||
if (udpClient?.Client?.LocalEndPoint != null)
|
||||
{
|
||||
return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
|
||||
}
|
||||
return udpPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current condition information
|
||||
/// </summary>
|
||||
public string GetCurrentCondition()
|
||||
{
|
||||
if (practiceRoundActive)
|
||||
{
|
||||
return "Practice Round Active";
|
||||
}
|
||||
return $"{currentConditionType} - {currentObjectType} (Index: {currentConditionIndex})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific object is currently active
|
||||
/// </summary>
|
||||
public bool IsObjectActive(string objectType)
|
||||
{
|
||||
if (objectMap.ContainsKey(objectType) && objectMap[objectType] != null)
|
||||
{
|
||||
return objectMap[objectType].activeInHierarchy;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific avatar is currently active
|
||||
/// </summary>
|
||||
public bool IsAvatarActive(string avatarType)
|
||||
{
|
||||
if (avatarMap.ContainsKey(avatarType) && avatarMap[avatarType] != null)
|
||||
{
|
||||
return avatarMap[avatarType].activeInHierarchy;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if practice round is active
|
||||
/// </summary>
|
||||
public bool IsPracticeRoundActive()
|
||||
{
|
||||
return practiceRoundActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log message with timestamp
|
||||
/// </summary>
|
||||
private void LogMessage(string message)
|
||||
{
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
Debug.Log($"[VRExperimentController] {DateTime.Now:HH:mm:ss} - {message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log error message
|
||||
/// </summary>
|
||||
private void LogError(string message)
|
||||
{
|
||||
Debug.LogError($"[VRExperimentController] {DateTime.Now:HH:mm:ss} - ERROR: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manual test methods for debugging (can be called from inspector)
|
||||
/// </summary>
|
||||
[ContextMenu("Test Helpful Condition")]
|
||||
public void TestHelpfulCondition()
|
||||
{
|
||||
var testMessage = new Dictionary<string, object>
|
||||
{
|
||||
{"command", "start_condition"},
|
||||
{"condition_type", "Helpful"},
|
||||
{"object_type", "Brick"},
|
||||
{"condition_index", 0}
|
||||
};
|
||||
HandleConditionChange(testMessage);
|
||||
}
|
||||
|
||||
[ContextMenu("Test Demotivating Condition")]
|
||||
public void TestDemotivatingCondition()
|
||||
{
|
||||
var testMessage = new Dictionary<string, object>
|
||||
{
|
||||
{"command", "start_condition"},
|
||||
{"condition_type", "Demotivating"},
|
||||
{"object_type", "Paperclip"},
|
||||
{"condition_index", 1}
|
||||
};
|
||||
HandleConditionChange(testMessage);
|
||||
}
|
||||
|
||||
[ContextMenu("Test Control Condition")]
|
||||
public void TestControlCondition()
|
||||
{
|
||||
var testMessage = new Dictionary<string, object>
|
||||
{
|
||||
{"command", "start_condition"},
|
||||
{"condition_type", "Control"},
|
||||
{"object_type", "Rope"},
|
||||
{"condition_index", 2}
|
||||
};
|
||||
HandleConditionChange(testMessage);
|
||||
}
|
||||
|
||||
[ContextMenu("Test Non-Interactive Condition")]
|
||||
public void TestNonInteractiveCondition()
|
||||
{
|
||||
var testMessage = new Dictionary<string, object>
|
||||
{
|
||||
{"command", "start_condition"},
|
||||
{"condition_type", "Non-Interactive"},
|
||||
{"object_type", "Book"},
|
||||
{"condition_index", 3}
|
||||
};
|
||||
HandleConditionChange(testMessage);
|
||||
}
|
||||
|
||||
[ContextMenu("Test Practice Round")]
|
||||
public void TestPracticeRound()
|
||||
{
|
||||
var testMessage = new Dictionary<string, object>
|
||||
{
|
||||
{"command", "start_practice_round"},
|
||||
{"duration", 300}
|
||||
};
|
||||
HandlePracticeRoundStart(testMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional debug GUI for monitoring experiment controller status
|
||||
/// </summary>
|
||||
void OnGUI()
|
||||
{
|
||||
if (!enableDebugLogging) return;
|
||||
|
||||
GUILayout.BeginArea(new Rect(10, 10, 350, 250));
|
||||
GUILayout.Label($"VR Experiment Controller (JSON Only)");
|
||||
GUILayout.Label($"Status: {(isListening ? "Listening" : "Stopped")}");
|
||||
GUILayout.Label($"Listen Port: {GetActualListenPort()}{(GetActualListenPort() != udpPort ? " (alt)" : "")}");
|
||||
GUILayout.Label($"Port Sharing: {allowPortSharing}");
|
||||
GUILayout.Label($"Practice Round: {(practiceRoundActive ? "Active" : "Inactive")}");
|
||||
GUILayout.Label($"Current Condition: {currentConditionType}");
|
||||
GUILayout.Label($"Current Object: {currentObjectType}");
|
||||
GUILayout.Label($"Condition Index: {currentConditionIndex}");
|
||||
GUILayout.Label($"Has Condition: {!string.IsNullOrEmpty(currentConditionType)}");
|
||||
|
||||
if (GUILayout.Button(isListening ? "Stop Listener" : "Start Listener"))
|
||||
{
|
||||
if (isListening)
|
||||
StopUDPListener();
|
||||
else
|
||||
StartUDPListener();
|
||||
}
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
11
Unity-Master/Assets/Scripts/VRExperimentController.cs.meta
Normal file
11
Unity-Master/Assets/Scripts/VRExperimentController.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f08db2249db6c4346a66269aa272394c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user