diff --git a/docs/rocket-launch.md b/docs/rocket-launch.md index 455683f..44a49e1 100644 --- a/docs/rocket-launch.md +++ b/docs/rocket-launch.md @@ -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 @@ -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. diff --git a/projects/AstroBalance/Assets/Scenes/RocketLaunch.unity b/projects/AstroBalance/Assets/Scenes/RocketLaunch.unity index 53a05a0..444043e 100644 --- a/projects/AstroBalance/Assets/Scenes/RocketLaunch.unity +++ b/projects/AstroBalance/Assets/Scenes/RocketLaunch.unity @@ -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 diff --git a/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchControl.cs b/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchControl.cs index 35cba0a..02548a7 100644 --- a/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchControl.cs +++ b/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchControl.cs @@ -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.")] @@ -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.")] @@ -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 @@ -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(); @@ -128,7 +134,6 @@ void Start() adaptiveDifficulty *= ((float)maxPreviousGames + (float)lastGameData.Count()) / (float)maxPreviousGames; - targetObject.GetComponent().transform.localScale /= adaptiveDifficulty; gazeTolerance /= adaptiveDifficulty; launchTime *= adaptiveDifficulty; @@ -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(); + } + + /// + /// Initialise sprite of target, and scale size to match gaze tolerance + /// + private void InitialiseTarget() + { + targetObject.SetActive(false); incrementCountDownCode(); + + // Match width and height of target to gaze tolerance + Renderer targetRenderer = targetObject.transform.GetComponent(); + 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 @@ -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 @@ -196,16 +228,8 @@ void Update() // use centre of bounds in case the target object is not centred targetX = targetObject.transform.GetComponent().bounds.center.x; targetY = targetObject.transform.GetComponent().bounds.center.y; - Vector2 gazeTol = new Vector2( - targetObject.transform.GetComponent().bounds.extents.x, - targetObject.transform.GetComponent().bounds.extents.y - ); - gazeIsSteady = gazeBuffer.gazeSteady( - gazeTime, - gazeTolerance * gazeTol.magnitude, - targetX, - targetY - ); + + gazeIsSteady = gazeBuffer.gazeSteady(gazeTime, gazeTolerance, targetX, targetY); } else { @@ -245,6 +269,11 @@ public float HeadSpeed get => headSpeed; } + public GameObject TargetObject + { + get => targetObject; + } + /// /// Adds latest tracking data to buffers and returns latest gaze information /// @@ -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 diff --git a/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchProgressRing.cs b/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchProgressRing.cs index 6426f2c..9bcd622 100644 --- a/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchProgressRing.cs +++ b/projects/AstroBalance/Assets/Scripts/RocketLaunch/LaunchProgressRing.cs @@ -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; @@ -30,6 +28,33 @@ void Start() } fillImage.color = fillColor; + + FitToCountdown(); + } + + /// + /// Fit the progress ring position and size to the countdown target + /// + private void FitToCountdown() + { + GameObject targetObject = countdownController.TargetObject; + fillImage.transform.position = Camera.main.WorldToScreenPoint( + targetObject.transform.position + ); + + Renderer targetRenderer = targetObject.transform.GetComponent(); + 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