851 lines
26 KiB
C#
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();
|
|
}
|
|
} |