Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/space-walking.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Data is saved to `SpaceWalkingScores.csv`, with one row per played game. Values
- `endTime`: the game end time in format HH:MM:ss (local time - see startTime description)
- `gameCompleted`: whether this game was completed. If they exited early, this will be false.
- `timeLimitSeconds`: the time limit set for this game in seconds
- `gameDurationSeconds`: how long the game was played in seconds. If the game was played through to completion this will be equal to timeLimitSeconds; if they exited early, it will be less.
- `gameDurationSeconds`: how long the game was played (rounded to the nearest second). If the game was played through to completion this will be equal to timeLimitSeconds; if they exited early, it will be less.
- `nCompleteSteps`: number of completed steps during this game. One complete step = a step out and back to the centre.
- `headTurnsActive`: whether head turn arrows were active for this game (only active at highest difficulty level)
- `nCompleteHeadTurns`: number of completed head turns (will be 0 if `headTurnsActive` is false)
Expand Down
33 changes: 31 additions & 2 deletions docs/star-collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,46 @@ top of the screen.

## Main objects / values to edit during play testing

- **StarCollectorManager**: values related to the overall win condition of the game
- **StarCollectorManager**: values related to the overall win condition of the game, and head speed save data
- Min / max time limit in seconds
- Time limit increment in seconds (if time limit upgrade % is met)
- Number of games in a row that must meet the upgrade % to increase the time limit
- The length of the time window used to evaluate player performance
- The % of stars that must be collected to increase speed or time limit
- The maximum number of head yaw readings to keep in the buffer at one time (these are used for the head speed save data)
- The minimum number of head yaw readings needed to calculate a head speed
- The number of seconds to calculate head yaw speed over

- **StarGenerator**: values related to generation of the wave of stars
- Min, max and base star speed
- The amount the star speed increases per upgrade
- Shape of the wave (e.g. width, swerve, star sampling)

- **Ship**: values related to ship movement
- The amount the ship moves per degree of head movement (X By Degrees)
- The amount the ship moves per degree of head movement (X By Degrees)

## Save data

Data is saved to `StarCollectorScores.csv`, with one row per played game. Values are:

- `gameNumber`: a unique id per played star collector game
- `sessionNumber`: the session this game was played in (corresponds to sessionNumber in [`SessionSummary.csv`](./session-summary.md))
- `date`: the date of the game session in format YYYY-MM-DD
- `startTime`: the game start time in format HH:MM:ss. This is the local time (e.g. if your computer is set to UK time - this is UK time).
- `endTime`: the game end time in format HH:MM:ss (local time - see startTime description)
- `gameCompleted`: whether this game was completed. If they exited early, this will be false.
- `timeLimitSeconds`: the time limit set for this game in seconds
- `gameDurationSeconds`: how long the game was played (rounded to the nearest second). If the game was played through to completion this will be equal to timeLimitSeconds; if they exited early, it will be less.
- `nStarsCollected`: the number of stars collected during the game
- `percentStarsCollected`: the percent of all stars collected during the game, rounded to 2 decimal places.
- `adaptiveLevel`: an integer (1 or above) representing the current difficulty level. Every time the game time limit is increased, this level increases by one.
- `finalStarFallSpeed`: Speed of falling stars (unity units per second) at the end of the game.

**Note**: all head speed measures below are calculated as follows. Every `samplingIntervalSeconds` (a configurable parameter in `StarCollectorManager`), the absolute differences of head yaw angle between consecutive readings in the time period are summed together and divided by the total difference in time to give a speed in degrees per second.
If the player goes out of range at any point in those `samplingIntervalSeconds` (i.e. the tracker can no longer detect them), then that speed measurement is discarded.

At the end of the game, all sampled speed measurements are used to calculate the summary statistics below:

- `headSpeedDegPerSecMean`: Mean head yaw speed (left-right rotation) measured in degrees per second. Rounded to 2 decimal places.
- `headSpeedDegPerSecPeak`: Peak head yaw speed (left-right rotation) measured in degrees per second. Rounded to 2 decimal places.
- `headSpeedDegPerSecSD`: Standard deviation of head yaw speed (left-right rotation) measured in degrees per second. Rounded to 2 decimal places.
2 changes: 1 addition & 1 deletion docs/star-seek.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ Data is saved to `StarSeekScores.csv`, with one row per played game. Values are:
- `endTime`: the game end time in format HH:MM:ss (local time - see startTime description)
- `gameCompleted`: whether this game was completed. If they exited early, this will be false.
- `timeLimitSeconds`: the time limit set for this game in seconds
- `gameDurationSeconds`: how long the game was played in seconds. If the game was played through to completion this will be equal to timeLimitSeconds; if they exited early, it will be less.
- `gameDurationSeconds`: how long the game was played (rounded to the nearest second). If the game was played through to completion this will be equal to timeLimitSeconds; if they exited early, it will be less.
- `nStarsCollected`: the number of stars collected during the game
- `adaptiveLevel`: an integer (1 or above) representing the current difficulty level. Every time the time limit is increased, this level increases by one.
11 changes: 10 additions & 1 deletion projects/AstroBalance/Assets/Scripts/CountdownTimer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,23 @@ public void StopCountdown()
{
UpdateTimerText(0);
timerRunning = false;
timeRemaining = 0;
}

/// <summary>
/// Get remaining time in seconds
/// </summary>
public float GetTimeRemaining()
{
return timeRemaining;
return timeRemaining < 0 ? 0 : timeRemaining;
}

/// <summary>
/// Get elapsed time in seconds (time limit - time remaining)
/// </summary>
public float GetElapsedTime()
{
return timeLimit - GetTimeRemaining();
}

/// <summary>
Expand Down
22 changes: 22 additions & 0 deletions projects/AstroBalance/Assets/Scripts/MathsUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public static class MathsUtilities
{
public static int RoundToNearestInt(float number)
{
return Mathf.FloorToInt(number + 0.5f);
}

public static float RoundTo2DecimalPlaces(float number)
{
return (float)Mathf.FloorToInt((number * 100) + 0.5f) / 100;
}

public static float StandardDeviation(List<float> numbers)
{
float average = numbers.Average();
return Mathf.Sqrt(numbers.Average(v => Mathf.Pow(v - average, 2)));
}
}
2 changes: 2 additions & 0 deletions projects/AstroBalance/Assets/Scripts/MathsUtilities.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -252,18 +252,9 @@ private void SaveGameData(bool gameComplete)
// Update save data for this game
gameData.gameCompleted = gameComplete;
gameData.timeLimitSeconds = timeLimit;

float remainingTime = timer.GetTimeRemaining();
if (remainingTime > 0)
{
gameData.gameDurationSeconds = Mathf.FloorToInt(timeLimit - remainingTime + 0.5f);
}
else
{
gameData.gameDurationSeconds = timeLimit;
}

gameData.gameDurationSeconds = MathsUtilities.RoundToNearestInt(timer.GetElapsedTime());
gameData.LogEndTime();

gameData.nCompleteSteps = stepScore;
gameData.headTurnsActive = headTurnsActive;
gameData.nCompleteHeadTurns = headTurnScore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
public class StarCollectorData : GameData
{
public int timeLimitSeconds;
public int gameDurationSeconds;
public int nStarsCollected;
public float percentStarsCollected;
public int adaptiveLevel;
public float finalStarFallSpeed;
public float headSpeedDegPerSecMean;
public float headSpeedDegPerSecPeak;
public float headSpeedDegPerSecSD;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using TMPro;
using Tobii.GameIntegration.Net;
Comment thread
K-Meech marked this conversation as resolved.
using UnityEngine;

public class StarCollectorManager : MonoBehaviour
Expand Down Expand Up @@ -49,30 +50,55 @@ public class StarCollectorManager : MonoBehaviour
]
private int timeLimitUpgradePercent = 60;

[
SerializeField,
Tooltip("Maximum number of head yaw readings to keep in the buffer at one time")
]
private int maxNItemsInBuffer = 100;

[
SerializeField,
Tooltip("The minimum number of head yaw readings needed to calculate a head speed")
]
private int minNItemsForSpeed = 5;

[SerializeField, Tooltip("The number of seconds to calculate head yaw speed over")]
private float samplingIntervalSeconds = 0.5f;

private TextMeshProUGUI winText;
private Tracker tracker;
private int timeLimit;
private int score; // stars collected over whole game
private int missed; // stars missed over whole game
private bool gameActive = true;

private float windowStart;
private float speedWindowStart; // start time of speed upgrade window
private int scoreInTimeWindow = 0; // stars collected in time window
private int missedInTimeWindow = 0; // stars missed in time window
private string saveFilename = "StarCollectorScores";
private StarCollectorData gameData;

private float bufferWindowStart; // start time of buffer update window
private bool outOfRangeInWindow = false; // whether the player went out of range of the tracker during this window
private HeadAngleBuffer headYawBuffer;
private List<float> headYawSpeeds = new List<float>();

// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
ChooseGameTimeLimit();

winText = winScreen.GetComponentInChildren<TextMeshProUGUI>();
tracker = FindFirstObjectByType<Tracker>();
gameData = new StarCollectorData();
score = 0;
scoreText.text = score.ToString();

headYawBuffer = new HeadAngleBuffer(maxNItemsInBuffer, minNItemsForSpeed);
timer.StartCountdown(timeLimit);

windowStart = Time.time;
speedWindowStart = Time.time;
bufferWindowStart = Time.time;
}

/// <summary>
Expand Down Expand Up @@ -129,9 +155,18 @@ void Update()
return;
}

// Keep track of head yaw angles on every update
UpdateHeadYawBuffer();

// Every 'samplingIntervalSeconds', record the speed averaged over that time period
if (Time.time - bufferWindowStart >= samplingIntervalSeconds)
{
RecordHeadSpeed();
}

// At end of time window, assess performance and update the difficulty
// of the game
if (Time.time - windowStart >= difficultyWindowSeconds)
if (Time.time - speedWindowStart >= difficultyWindowSeconds)
{
UpdateDifficulty();
}
Expand All @@ -143,6 +178,47 @@ void Update()
}
}

/// <summary>
/// Add latest head yaw angle to buffer
/// </summary>
private void UpdateHeadYawBuffer()
{
// If the player goes out of range of the tracker
if (!tracker.isPlayerDetected())
{
outOfRangeInWindow = true;
}
else
{
HeadPose headPose = tracker.getHeadPose();
HeadYawItem headYaw = new HeadYawItem(headPose);
headYawBuffer.addIfNew(headYaw);
}
}

/// <summary>
/// Record the latest head yaw speed
/// </summary>
private void RecordHeadSpeed()
{
// Only record speeds if the player was in range of the tracker for the whole window.
// (otherwise, if they've been out of range for a while, we may be calculating the speed of quite old data in the buffer)
if (!outOfRangeInWindow)
{
float headSpeed = headYawBuffer.getSpeed(samplingIntervalSeconds);

// If there aren't enough recorded head yaw angles yet, the returned speed is zero.
// We don't want to include these readings in the overall averages.
if (headSpeed > 0)
{
headYawSpeeds.Add(headSpeed);
}
}

outOfRangeInWindow = false;
bufferWindowStart = Time.time;
}

/// <summary>
/// Dynamically update the difficulty of the game based on player performance.
///
Expand All @@ -164,7 +240,7 @@ private void UpdateDifficulty()
starGenerator.DecreaseSpeed();
}

windowStart = Time.time;
speedWindowStart = Time.time;
scoreInTimeWindow = 0;
missedInTimeWindow = 0;
}
Expand Down Expand Up @@ -220,26 +296,63 @@ private void EndGame()
winScreen.SetActive(true);

// Save game details to file
SaveGameData();
SaveGameData(true);
}
}

private void SaveGameData()
private void OnDestroy()
{
float totalStars = score + missed;
float percentCollected = ((float)score / totalStars) * 100;
// If the scene is exited early (e.g. with the exit button), then save this
// partial game's data
if (gameActive)
{
SaveGameData(false);
}
}

private void SaveGameData(bool gameComplete)
{
// Update save data for this game
gameData.gameCompleted = true;
gameData.gameCompleted = gameComplete;
gameData.timeLimitSeconds = timeLimit;
gameData.nStarsCollected = score;
gameData.percentStarsCollected = percentCollected;
gameData.gameDurationSeconds = MathsUtilities.RoundToNearestInt(timer.GetElapsedTime());
gameData.LogEndTime();

float totalStars = score + missed;
float percentCollected = ((float)score / totalStars) * 100;
gameData.nStarsCollected = score;
gameData.percentStarsCollected = MathsUtilities.RoundTo2DecimalPlaces(percentCollected);
gameData.adaptiveLevel =
1 + Mathf.CeilToInt((timeLimit - minTimeLimit) / timeLimitIncrement);
gameData.finalStarFallSpeed = starGenerator.GetStarSpeed();

if (headYawSpeeds.Count() == 0)
{
gameData.headSpeedDegPerSecPeak = 0;
gameData.headSpeedDegPerSecMean = 0;
gameData.headSpeedDegPerSecSD = 0;
}
else
{
gameData.headSpeedDegPerSecPeak = MathsUtilities.RoundTo2DecimalPlaces(
headYawSpeeds.Max()
);
gameData.headSpeedDegPerSecMean = MathsUtilities.RoundTo2DecimalPlaces(
headYawSpeeds.Average()
);
float standardDeviation = MathsUtilities.StandardDeviation(headYawSpeeds);
gameData.headSpeedDegPerSecSD = MathsUtilities.RoundTo2DecimalPlaces(standardDeviation);
}

SaveGameData<StarCollectorData> saveData = new(saveFilename);
gameData.sessionNumber = CaptureSessionData.CurrentSessionNumber();
gameData.gameNumber = saveData.GetNextGameNumber();
saveData.Save(gameData);

// Update save data for this session
CaptureSessionData.MarkGameAsComplete("nCompleteStarCollectorGames");
if (gameComplete)
{
CaptureSessionData.MarkGameAsComplete("nCompleteStarCollectorGames");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ private void UpdateSpeed(float increment)
baseStarSpeed = nextSpeed;
}

Debug.Log($"Updated star speed to {nextSpeed}");
Debug.Log($"Updated star speed to {baseStarSpeed}");
}

public void StopGeneration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ float guessTime
trialData.trialNumber = GetNextTrialNumber();
trialData.responseCorrect = guessCorrect;
trialData.sequenceLength = sequenceLength;
trialData.responseTimeSeconds = (float)Mathf.FloorToInt((guessTime * 100) + 0.5f) / 100; // round to 2 decimal places
trialData.responseTimeSeconds = MathsUtilities.RoundTo2DecimalPlaces(guessTime);
gameData.Add(trialData);

// Update score text, and end condition if guess was correct
Expand Down
Loading