diff --git a/Assets/UXF/Scripts/Etc/UXFDataTable.cs b/Assets/UXF/Scripts/Etc/UXFDataTable.cs index 0eaf51e0..fbdf7dbc 100644 --- a/Assets/UXF/Scripts/Etc/UXFDataTable.cs +++ b/Assets/UXF/Scripts/Etc/UXFDataTable.cs @@ -73,6 +73,38 @@ public static UXFDataTable FromCSV(string[] csvLines) return table; } + /// + /// Build a table from lines of TSV text. + /// + /// + /// + public static UXFDataTable FromTSV(string[] tsvLines) + { + string[] headers = tsvLines[0].Split('\t'); + var table = new UXFDataTable(tsvLines.Length - 1, headers); + + // traverse down rows + for (int i = 1; i < tsvLines.Length; i++) + { + string[] values = tsvLines[i].Split('\t'); + + // if last line, just 1 item in the row, and it is blank, then ignore it + if (i == tsvLines.Length - 1 && values.Length == 1 && values[0].Trim() == string.Empty ) break; + + // check if number of columns is correct + if (values.Length != headers.Length) throw new Exception($"TSV line {i} has {values.Length} columns, but expected {headers.Length}"); + + // build across the row + var row = new UXFDataRow(); + for (int j = 0; j < values.Length; j++) + row.Add((headers[j], values[j].Trim('\"'))); + + table.AddCompleteRow(row); + } + + return table; + } + /// /// Add a complete row to the table /// diff --git a/Assets/UXF/Scripts/SessionBuilders/TSVExperimentBuilder.cs b/Assets/UXF/Scripts/SessionBuilders/TSVExperimentBuilder.cs new file mode 100644 index 00000000..875d7ab3 --- /dev/null +++ b/Assets/UXF/Scripts/SessionBuilders/TSVExperimentBuilder.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.IO; + +namespace UXF +{ + public class TSVExperimentBuilder : MonoBehaviour, IExperimentBuilder + { + + [Tooltip("The name key in the settings that contains the name of the trial specification file.")] + [SerializeField] private string tsvFileKey = "trial_specification_name"; + [Tooltip("Enable to copy all settings from each trial in the TSV file to the the trial results output.")] + [SerializeField] private bool copyToResults = true; + + /// + /// Reads a TSV from filepath as specified in tsvFileKey in the settings. + /// The TSV file is used to generate trials row-by-row, assigning a setting per column. + /// + /// + public void BuildExperiment(Session session) + { + // check if settings contains the tsv file name + if (!session.settings.ContainsKey(tsvFileKey)) + { + throw new Exception($"TSV file name not specified in settings. Please specify a TSV file name in the settings with key \"{tsvFileKey}\"."); + } + + // get the tsv file name + string tsvName = session.settings.GetString(tsvFileKey); + + // check if the file exists + string tsvPath = Path.GetFullPath(Path.Combine(Application.streamingAssetsPath, tsvName)); + if (!File.Exists(tsvPath)) + { + throw new Exception($"TSV file at \"{tsvPath}\" does not exist!"); + } + + // read the tsv file + string[] tsvLines = File.ReadAllLines(tsvPath); + + // parse as table + var table = UXFDataTable.FromTSV(tsvLines); + + // build the experiment. + // this adds a new trial to the session for each row in the table + // the trial will be created with the settings from the values from the table + // if "block_num" is specified in the table, the trial will be added to the block with that number + session.BuildFromTable(table, copyToResults); + } + } + +} diff --git a/Assets/UXF/Scripts/Trackers/ScreenRayTracker.cs b/Assets/UXF/Scripts/Trackers/ScreenRayTracker.cs new file mode 100644 index 00000000..0c8a6df6 --- /dev/null +++ b/Assets/UXF/Scripts/Trackers/ScreenRayTracker.cs @@ -0,0 +1,156 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using UXF; +using System.Linq; + +/// +/// Attach this component to any gameobject (e.g. an empty one) and assign it in the trackedObjects field in an ExperimentSession to record +/// if ray casted from camera is hitting anything. NOTE: Update Type must be set to MANUAL. +/// Please provide coordinates in form of lists named ray_x & ray_y in your .json file. For example \n\"ray_x\": [0.5],\n\"ray_y\": [0.5]\nif you want to have one ray in the middle of the screen. Multiple rays can be provide with this method." +/// +public class ScreenRayTracker : Tracker +{ + // Public vars + [Header("Necessary Input")] + public Camera cam; + public Session session; + + [Header("Optional Input")] + [Tooltip("Enable if you want to visualise the rays in the scene view and the console output.")] + public bool debugMode = true; + [Tooltip("The max distance the ray should check for collisions. For further information see manual of Physics.Raycast.")] + public float distance = Mathf.Infinity; + + [Tooltip("Set to true if you want to use a LayerMask for the rays (see maunual of LayerMask.GetMask). Note you also need to set Layer Mask Names in this case with one or more layers that you want to use.")] + public bool useLayerMask = false; + [Tooltip("Provide the names of the layers for the mask.")] + public string[] layerMaskNames; + + // Private vars + private List objectDetected = new List(); + private UXFDataRow currentRow; + private bool recording = false; + private int layerMask; + private string noObjectString = "NA"; + private int numRays; + private List x = new List(); + private List y = new List(); + + // Start calc + void Start(){ + // Create layer mask + if(useLayerMask) + { + layerMask = LayerMask.GetMask(layerMaskNames); + } else + { + layerMask = ~0; // Set to everything as no mask is wanted. + } + + // Start the recoding + StartCoroutine(RecordRoutine()); + } + + /// + /// Gets coordinates for the rays in screen space from .json file and prints screen resolution + /// + public void GetRayCoordinates() + { + // Coordinates + x = session.settings.GetFloatList("ray_x"); + y = session.settings.GetFloatList("ray_y"); + + // Screen resolution + Debug.Log(Screen.currentResolution); + } + + /// + /// Starts the recording. This method needs to be added to [UXF_Rig] events called On Trial Begin + /// + public void StartRecording() + { + recording = true; + } + + /// + /// Stops the recording. This method needs to be added to [UXF_Rig] events called On Trial End + /// + public void StopRecording() + { + recording = false; + } + + IEnumerator RecordRoutine() + { + while (true){ + if (recording){ + objectDetected = Ray2DetectObjects(x, y, cam); + for(int i = 0; i < numRays; i++){ + // When no object was detected save only if saveNoObject is true + if(objectDetected[i] != noObjectString) + { + var values = new UXFDataRow(); + values.Add(("rayIndex", i)); + values.Add(("x", x[i])); + values.Add(("y", y[i])); + values.Add(("objectDetected", objectDetected[i])); + currentRow = values; + RecordRow(); // record for each ray + currentRow = null; + + } + } + } + yield return null; // wait until next frame + } + } + + /// + /// Set headers and measurment descriptor + /// + public override string MeasurementDescriptor => "ObjectsOnScreenTracker"; + public override IEnumerable CustomHeader => new string[] {"rayIndex", "x", "y", "objectDetected"}; + + protected override UXFDataRow GetCurrentValues() + { + return currentRow; + } + + /// + /// Function that casts the rays and detects the hits. + /// + List Ray2DetectObjects(List x, List y, Camera cam) + { + // Get number of rays + numRays = y.Count; + + // Create var to reset the variable + List nameOfObjects = new List(); + + for (int i = 0; i < numRays; i++){ + // Cast the ray and add to list + Ray ray = cam.ViewportPointToRay(new Vector3(x[i], y[i], 0)); + + // Display ray for debugging + if(debugMode){ + Debug.DrawRay(ray.origin, ray.direction * 50, Color.red); + } + + // Raycast and check if something is hit + RaycastHit hit1; + if (Physics.Raycast(ray, out hit1, distance, layerMask)){ + if(debugMode){ + Debug.DrawRay(ray.origin, ray.direction * 50, Color.green); + Debug.Log("I'm looking at " + hit1.transform.name + " with ray " + i); + } + // Add name of GameObject that was hit + nameOfObjects.Add(hit1.transform.name); + } else { + // Add noObjectString becuase no object was hit by ray + nameOfObjects.Add(noObjectString); + } + } + return nameOfObjects; + } +}