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;
+ }
+}