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
9 changes: 4 additions & 5 deletions docs/rocket-launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ then a timer decrements until launch time is achieved.
- Launch time: The starting launch time in seconds. May be increased by adaptive difficulty features below.
- **Head Movement Variables**: Determine the amount of head movement required to decrement the timer.
- Head Pose Buffer Capacity (n) and speed time (s): head speed is measured as the average change in pitch or yaw over the time period speed time. The buffer will need to be sufficiently large to support the time based on game frame rate.
- Shake Speed Reduction: Because it is possible (for me at least) to shake my head quicker than I can nod, there is a scaling factor between the head speeds required for shaking or nodding. Setting to 0.5 for example means that the player must shake their head twice as fast as nodding to achieve the same effect.
- Minimum head speed (pitch or yaw) required to reduce the launch timer. The yaw speed is set higher than pitch, because it is possible (for me at least) to shake my head quicker than I can nod.
- **Steady Gaze Variables**: Determine how steady the gave must be to decrement the timer.
- Timer Duration: How long (in seconds) the player must maintain a steady gaze to increment the count down code display.
- Gaze Pose Buffer Capacity (n) and gaze time (s), gaze steadiness is measured as the standard deviation of gaze over gaze time seconds. The buffer will need to be sufficiently large to support the time based on game frame rate.
- Gaze Tolerance - the allowable gaze standard deviation to be steady. Smaller number will require steadier gaze. This may be reduced by the adaptive difficulty settings below.
- Target Object if this is set you are required to look at that object, if not gaze can be anywhere on screen but must be steady.
- Target Object - if this is set you are required to look at that object, if not gaze can be anywhere on screen but must be steady. The size of the target object will be matched to the gaze tolerance.

- **Adaptive difficulty variables**
- Max previous games: The maximum number of previous games to retrieve to determine experience based difficulty
Expand Down Expand Up @@ -43,13 +43,12 @@ then a timer decrements until launch time is achieved.

## Adaptive difficulty

Difficulty is increased between games by reducing the size of the gaze target, reducing the gaze tolerance, and increasing the overall launch time (explained below). The intention is that the gaze tolerance should always match the size of the gaze target, however this hasn't been verified through play testing yet. Note: all parameters mentioned below can be adjusted on `LaunchControl`.
Difficulty is increased between games by reducing the gaze tolerance, and increasing the overall launch time (explained below). Note: all parameters mentioned below can be adjusted on `LaunchControl`.

At the start of each game, a scaling factor is calculated as:

`adaptiveDifficulty` * ( (`maxPreviousGames` + `nGames`) / `maxPreviousGames`)

`nGames` is the total number of rocket launch games completed by the player so far (up to a maximum of `maxPreviousGames`). The scaling factor is then applied:
- the size of the gaze target (the box displaying the launch code numbers) is divided by the scaling factor. This makes it smaller after more games have been played.
- the gaze tolerance (i.e. the tolerance in unity coordinates that gaze needs to stay within) is divided by the scaling factor. This means the player must keep their gaze closer to the target after more games have been played.
- the gaze tolerance (i.e. the tolerance in unity coordinates that gaze needs to stay within) is divided by the scaling factor. This will also scale the gaze target (the box displaying the launch code numbers) to match. This means the player must keep their gaze closer to the target after more games have been played.
- the launch time is multiplied by the scaling factor. This means the player must move their head while looking at the target for longer to launch the rocket.
2 changes: 1 addition & 1 deletion projects/AstroBalance/Assets/Scenes/RocketLaunch.unity
Original file line number Diff line number Diff line change
Expand Up @@ -21548,7 +21548,7 @@ PrefabInstance:
objectReference: {fileID: 522649600}
- target: {fileID: 8902890511493098178, guid: 00c1b36d25d21214eb8e6df02c079dea, type: 3}
propertyPath: gazeTolerance
value: 300
value: 2
objectReference: {fileID: 0}
- target: {fileID: 8902890511493098178, guid: 00c1b36d25d21214eb8e6df02c079dea, type: 3}
propertyPath: gazeStatusText
Expand Down
82 changes: 56 additions & 26 deletions projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ public class LaunchControl : MonoBehaviour
[SerializeField, Tooltip("The time in seconds to measure head speed over.")]
private float speedTime = 2.0f;

[SerializeField, Tooltip("The minimum head speed required to reduce the launch timer.")]
private float minimumSpeed = 20;
[SerializeField, Tooltip("The minimum head pitch speed required to reduce the launch timer.")]
private float minimumSpeedPitch = 20;

[
SerializeField,
Tooltip(
"A pitch/yaw scale factor, as in general I can shake my head faster than I can nod."
"The minimum head yaw speed required to reduce the launch timer. Usually set higher than pitch, as I can shake my head faster than I can nod"
)
]
private float shakeSpeedReduction = 0.5f;
private float minimumSpeedYaw = 40;

[Header("Steady Gaze Variables")]
[SerializeField, Tooltip("Time between new random numbers in seconds.")]
Expand All @@ -41,7 +41,12 @@ public class LaunchControl : MonoBehaviour
[SerializeField, Tooltip("The time in seconds that the gaze should be steady for.")]
private float gazeTime = 3.0f;

[SerializeField, Tooltip("The tolerance in unity coordinates that gaze needs to stay within.")]
[
SerializeField,
Tooltip(
"The tolerance in unity coordinates that gaze needs to stay within (the targetObject is scaled to match)"
)
]
private float gazeTolerance = 3.0f;

[SerializeField, Tooltip("The game object the user is supposed to look at.")]
Expand Down Expand Up @@ -96,6 +101,7 @@ public class LaunchControl : MonoBehaviour
// head speed parameters
private HeadPoseBuffer headPoseBuffer;
private bool usePitch; //true if we're using pitch speed, false if we're using yaw speed.
private float minimumSpeed; // minimum head speed required for this game
private RocketLaunchData gameData;
private float rocketSpeed;
private int minDataRequired = 2; // we need at least 2 data points to calculate a speed or steadiness
Expand All @@ -111,8 +117,8 @@ public class LaunchControl : MonoBehaviour

private TextMeshProUGUI winText;

// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
// Awake is called once when the script instance is loaded
void Awake()
{
rocketSpeed = 0f;
winText = winScreen.GetComponentInChildren<TextMeshProUGUI>();
Expand All @@ -128,7 +134,6 @@ void Start()

adaptiveDifficulty *=
((float)maxPreviousGames + (float)lastGameData.Count()) / (float)maxPreviousGames;
targetObject.GetComponent<SpriteRenderer>().transform.localScale /= adaptiveDifficulty;
gazeTolerance /= adaptiveDifficulty;
launchTime *= adaptiveDifficulty;

Expand All @@ -140,14 +145,43 @@ void Start()
{
usePitch = !lastGameData.Last().pitch;
}
minimumSpeed = usePitch ? minimumSpeedPitch : minimumSpeedYaw;

InitialiseTarget();

headPoseBuffer = new HeadPoseBuffer(headPoseBufferCapacity, minDataRequired);
instructionsText.text = usePitch
? "Nod your head and repeat the code to launch the rocket!"
: "Shake your head and repeat the code to launch the rocket!";
gameData = new RocketLaunchData();
timeToLaunch = (float)launchTime * adaptiveDifficulty;
timeToLaunch = launchTime;
gazeBuffer = new GazeBuffer(gazeBufferCapacity, minDataRequired);
}

// Start is called once before the first execution of Update after the MonoBehaviour is created
private void Start()
{
gameData = new RocketLaunchData();
}

/// <summary>
/// Initialise sprite of target, and scale size to match gaze tolerance
/// </summary>
private void InitialiseTarget()
{
targetObject.SetActive(false);
incrementCountDownCode();

// Match width and height of target to gaze tolerance
Renderer targetRenderer = targetObject.transform.GetComponent<Renderer>();
float targetObjectWidth = targetRenderer.bounds.extents.x;
float targetObjectHeight = targetRenderer.bounds.extents.y;
Vector3 targetScale = targetRenderer.transform.localScale;
targetScale.Scale(
new Vector3(gazeTolerance / targetObjectWidth, gazeTolerance / targetObjectWidth, 1)
);
targetRenderer.transform.localScale = targetScale;

targetObject.SetActive(true);
}

// Update is called once per frame
Expand Down Expand Up @@ -181,10 +215,8 @@ void Update()
else
{
headSpeed =
(
headPoseBuffer.getSpeed(speedTime, HeadPoseAxis.Yaw)
- headPoseBuffer.getSpeed(speedTime, HeadPoseAxis.Pitch)
) * shakeSpeedReduction;
headPoseBuffer.getSpeed(speedTime, HeadPoseAxis.Yaw)
- headPoseBuffer.getSpeed(speedTime, HeadPoseAxis.Pitch);
}
headSpeed = Mathf.Max(0, headSpeed); // Clamp to zero to avoid negative speeds

Expand All @@ -196,16 +228,8 @@ void Update()
// use centre of bounds in case the target object is not centred
targetX = targetObject.transform.GetComponent<Renderer>().bounds.center.x;
targetY = targetObject.transform.GetComponent<Renderer>().bounds.center.y;
Vector2 gazeTol = new Vector2(
targetObject.transform.GetComponent<Renderer>().bounds.extents.x,
targetObject.transform.GetComponent<Renderer>().bounds.extents.y
);
gazeIsSteady = gazeBuffer.gazeSteady(
gazeTime,
gazeTolerance * gazeTol.magnitude,
targetX,
targetY
);

gazeIsSteady = gazeBuffer.gazeSteady(gazeTime, gazeTolerance, targetX, targetY);
}
else
{
Expand Down Expand Up @@ -245,6 +269,11 @@ public float HeadSpeed
get => headSpeed;
}

public GameObject TargetObject
{
get => targetObject;
}

/// <summary>
/// Adds latest tracking data to buffers and returns latest gaze information
/// </summary>
Expand All @@ -263,8 +292,9 @@ private GazeItem AddToBuffers()
headPose.Rotation.RollDegrees = 0f;
headPose.TimeStampMicroSeconds = (long)(Time.timeSinceLevelLoad * 1000000);

gazeItem.gazePoint.X = mousePos.x;
gazeItem.gazePoint.Y = mousePos.y;
Vector3 mousePoseWorld = Camera.main.ScreenToWorldPoint(mousePos);
gazeItem.gazePoint.X = mousePoseWorld.x;
gazeItem.gazePoint.Y = mousePoseWorld.y;
gazeItem.gazePoint.TimeStampMicroSeconds = (long)(Time.timeSinceLevelLoad * 1000000);
}
else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System.Collections;
using TMPro;
using Tobii.GameIntegration.Net;
using UnityEngine;
using UnityEngine.UI;

public class LaunchProgressRing : MonoBehaviour
{
[SerializeField, Tooltip("Arrow fill colour")]
[SerializeField, Tooltip("Ring fill colour")]
private Color fillColor = Color.red;

LaunchControl countdownController;
Expand All @@ -30,6 +28,33 @@ void Start()
}

fillImage.color = fillColor;

FitToCountdown();
}

/// <summary>
/// Fit the progress ring position and size to the countdown target
/// </summary>
private void FitToCountdown()
{
GameObject targetObject = countdownController.TargetObject;
fillImage.transform.position = Camera.main.WorldToScreenPoint(
targetObject.transform.position
);

Renderer targetRenderer = targetObject.transform.GetComponent<Renderer>();
float targetObjectWidth = 2 * targetRenderer.bounds.extents.x;
float targetObjectHeight = 2 * targetRenderer.bounds.extents.y;

// length of 1 unity world unit in this screen space
float scalingFactor = Vector3.Distance(
Camera.main.WorldToScreenPoint(new Vector3(0, 0, 0)),
Camera.main.WorldToScreenPoint(new Vector3(1, 0, 0))
);
float requiredWidth = (targetObjectWidth * scalingFactor) / fillImage.canvas.scaleFactor;
float requiredHeight = (targetObjectHeight * scalingFactor) / fillImage.canvas.scaleFactor;

fillImage.rectTransform.sizeDelta = new Vector2(requiredWidth, requiredHeight);
}

// Update is called once per frame
Expand Down