Files
Master-Arbeit-Tom-Hempel/Unity-Master/Assets/Scripts/VRExperimentController.cs

851 lines
26 KiB
C#

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 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<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()
{
// 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();
}
}
/// <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();
}
}