Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create ScreenRayTracker.cs #129

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Assets/UXF/Scripts/Etc/UXFDataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ public static UXFDataTable FromCSV(string[] csvLines)
return table;
}

/// <summary>
/// Build a table from lines of TSV text.
/// </summary>
/// <param name="tsvLines"></param>
/// <returns></returns>
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;
}
Comment on lines +76 to +106
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be a separate Pull Request, and made generic with the CSV version to accept a delimiter, rather than copying the code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely agree :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, just changing the experimenter builder is not enough because then what might happen is that a variable such as a string that is to be displayed to the participant contains and a comma, which UXF then tries to save, which doesn't work if the the output file is an .csv. I currently have work-around where I just replace a comma with an underscore before it is saved but tihis prevents me to copy the data from the input file to the result file.


/// <summary>
/// Add a complete row to the table
/// </summary>
Expand Down
54 changes: 54 additions & 0 deletions Assets/UXF/Scripts/SessionBuilders/TSVExperimentBuilder.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
/// <param name="session"></param>
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);
}
}

}
Comment on lines +7 to +54
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with this, it should be generic with specified delimiter

150 changes: 150 additions & 0 deletions Assets/UXF/Scripts/Trackers/ScreenRayTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UXF;
using System.Linq;

/// <summary>
/// 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.
/// </summary>
public class ScreenRayTracker : Tracker {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in UXF namespace

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use consistent brace style (opening brace { on new line)

// Public vars
[Header("Necessary Input")]
public Camera cam;
public Session session;
Comment on lines +16 to +17
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems problematic, other trackers do not need reference to the session.


[Header("Ray coordinates")]
[TextArea(10, 10)]
public string HowToUse = "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.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be here, better in the wiki


[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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use a LayerMask type


// Private vars
private List<string> objectDetected = new List<string>();
private UXFDataRow currentRow;
private bool recording = false;
private int layerMask;
private string noObjectString = "NA";
private int numRays;
private List<float> x = new List<float>();
private List<float> y = new List<float>();

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

/// <summary>
/// Gets coordinates for the rays in screen space from .json file and prints screen resolution
/// </summary>
public void GetRayCoordinates(){
// Coordinates
x = session.settings.GetFloatList("ray_x");
y = session.settings.GetFloatList("ray_y");
Comment on lines +61 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these would be better as serialized/public fields, UXF components should just "work" without some special items in your settings


// Screen resolution
Debug.Log(Screen.currentResolution);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove or put in debugMode

}

/// <summary>
/// Starts the recording. This method needs to be added to [UXF_Rig] events called On Trial Begin
/// </summary>
public void StartRecording(){
recording = true;
}

/// <summary>
/// Stops the recording. This method needs to be added to [UXF_Rig] events called On Trial End
/// </summary>
public void StopRecording(){
recording = false;
}

IEnumerator RecordRoutine(){
while (true){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use LateUpdate, FixedUpdate, etc?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means it ignores the TrackerUpdateType updateType - see the Tracker class

}
}

/// <summary>
/// Set headers and measurment descriptor
/// </summary>
public override string MeasurementDescriptor => "ObjectsOnScreenTracker";
public override IEnumerable<string> CustomHeader => new string[] { "rayIndex", "x", "y", "objectDetected"};

// Get values
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is not useful

protected override UXFDataRow GetCurrentValues(){
return currentRow;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means we will end up recording the values twice?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhmm, strange in the result files each row is unique as far as I can see.

Copy link
Member

@jackbrookes jackbrookes Sep 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if you use updateType that is not manual?


/// <summary>
/// Function to detect objects on screen by rays
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is not useful

/// </summary>
List<string> ray2detectObjects(List<float> x, List<float> y, Camera cam){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methods should be PascalCase

// Get number of rays
numRays = y.Count;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if x & y are different lengths?


// Create var to reset the variable
List<string> nameOfObjects = new List<string>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to create a field, and clear it, rather than make a new list each time


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