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; /// /// VR Experiment Controller /// Listens for UDP messages from the supervisor and manages avatars and objects /// based on the current experimental condition. /// public class VRExperimentController : MonoBehaviour { [Header("Network Settings")] [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; private int udpPort; // 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 messageQueue = new Queue(); private readonly object queueLock = new object(); // Object mapping for easy access private Dictionary objectMap; private Dictionary avatarMap; void Start() { // Get network config from global instance var cfg = NetworkConfig.Instance; if (cfg != null) { udpPort = cfg.port; } else { Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder."); udpPort = 1221; } 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(); } } /// /// Initialize object and avatar mappings for easy access /// private void InitializeObjectMaps() { // Initialize object map objectMap = new Dictionary { { "Brick", brickObject }, { "Paperclip", paperclipObject }, { "Rope", ropeObject }, { "Book", bookObject } }; // Initialize avatar map avatarMap = new Dictionary { { "Helpful", helpfulAvatar }, { "Demotivating", demotivatingAvatar }, { "Non-Interactive", nonInteractiveAvatar } }; // Validate assignments ValidateAssignments(); } /// /// Validate that all required objects and avatars are assigned /// 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."); } } /// /// Start listening for UDP messages /// 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}"); } } } /// /// Try alternative ports for local testing /// 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"); } /// /// Stop listening for UDP messages /// 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"); } /// /// UDP listener loop (runs in separate thread) /// 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}"); } } } } /// /// Check if the received data is an experiment control message (JSON) vs avatar data (binary) /// 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; } } /// /// Process messages from the queue in the main thread /// private void ProcessMessageQueue() { lock (queueLock) { while (messageQueue.Count > 0) { string message = messageQueue.Dequeue(); ProcessUDPMessage(message); } } } /// /// Process incoming UDP message /// private void ProcessUDPMessage(string message) { try { LogMessage($"Received UDP message: {message}"); // Parse JSON message var messageData = JsonConvert.DeserializeObject>(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}"); } } /// /// Handle practice round start command /// private void HandlePracticeRoundStart(Dictionary 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}"); } } /// /// Handle practice round end command /// private void HandlePracticeRoundEnd(Dictionary 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}"); } } /// /// Handle disable all command (when timer expires) /// private void HandleDisableAll(Dictionary 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}"); } } /// /// Handle condition change messages /// private void HandleConditionChange(Dictionary 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}"); } } /// /// Update the scene based on the current condition /// 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(); } /// /// Handle non-interactive avatar behavior /// 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(); 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}"); } } } /// /// Deactivate all objects /// private void DeactivateAllObjects() { foreach (var kvp in objectMap) { if (kvp.Value != null) { kvp.Value.SetActive(false); } } } /// /// Deactivate all avatars /// private void DeactivateAllAvatars() { foreach (var kvp in avatarMap) { if (kvp.Value != null) { kvp.Value.SetActive(false); } } } /// /// Called when condition changes - override this for custom behavior /// 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}"); } } /// /// Get the actual UDP port being used /// private int GetActualListenPort() { if (udpClient?.Client?.LocalEndPoint != null) { return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; } return udpPort; } /// /// Get current condition information /// public string GetCurrentCondition() { if (practiceRoundActive) { return "Practice Round Active"; } return $"{currentConditionType} - {currentObjectType} (Index: {currentConditionIndex})"; } /// /// Check if a specific object is currently active /// public bool IsObjectActive(string objectType) { if (objectMap.ContainsKey(objectType) && objectMap[objectType] != null) { return objectMap[objectType].activeInHierarchy; } return false; } /// /// Check if a specific avatar is currently active /// public bool IsAvatarActive(string avatarType) { if (avatarMap.ContainsKey(avatarType) && avatarMap[avatarType] != null) { return avatarMap[avatarType].activeInHierarchy; } return false; } /// /// Check if practice round is active /// public bool IsPracticeRoundActive() { return practiceRoundActive; } /// /// Log message with timestamp /// private void LogMessage(string message) { if (enableDebugLogging) { Debug.Log($"[VRExperimentController] {DateTime.Now:HH:mm:ss} - {message}"); } } /// /// Log error message /// private void LogError(string message) { Debug.LogError($"[VRExperimentController] {DateTime.Now:HH:mm:ss} - ERROR: {message}"); } /// /// Manual test methods for debugging (can be called from inspector) /// [ContextMenu("Test Helpful Condition")] public void TestHelpfulCondition() { var testMessage = new Dictionary { {"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 { {"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 { {"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 { {"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 { {"command", "start_practice_round"}, {"duration", 300} }; HandlePracticeRoundStart(testMessage); } /// /// Optional debug GUI for monitoring experiment controller status /// 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(); } }