diff --git a/docs/star-seek.md b/docs/star-seek.md
index 07bb013..c0661cd 100644
--- a/docs/star-seek.md
+++ b/docs/star-seek.md
@@ -5,7 +5,17 @@ The star seek mini-game uses gaze + head position to collect stars that appear a
## Main objects / values to edit during play testing
- **StarSeekManager**: values related to the overall win condition of the game
- - Game time limit
+ - Minimum game time limit (seconds)
+ - Maximum game time limit (seconds)
+ - How many seconds to increase the time limit, if the upgrade rate is met
+ - Upgrade rate: (number of stars collected / game time limit) i.e. average stars collected per second, must be above this value to increase the time limit of future games.
+ - Number of games in a row that must meet the upgrade rate
+
+- **StarGenerator**: values related to spawning stars
+ - Min distance between stars and the edge of the screen
+ - Number of rows + columns in star spawn grid
+ - Grid positions to exclude from star spawning (e.g. those that overlap with UI elements)
+ - Min distance between spawned stars
- **Prefabs/StarSeekStar**: values related to 'locking on' to a star
- Time required to collect a star (with both gaze + head pose crosshair aligned)
diff --git a/projects/AstroBalance/Assets/Scenes/StarSeek.unity b/projects/AstroBalance/Assets/Scenes/StarSeek.unity
index c7e6d3a..f81c42f 100644
--- a/projects/AstroBalance/Assets/Scenes/StarSeek.unity
+++ b/projects/AstroBalance/Assets/Scenes/StarSeek.unity
@@ -151,7 +151,11 @@ MonoBehaviour:
scoreText: {fileID: 730211112}
timer: {fileID: 740804264}
winScreen: {fileID: 159571674}
- timeLimit: 120
+ minTimeLimit: 60
+ maxTimeLimit: 180
+ timeLimitIncrement: 60
+ timeLimitUpgradeRate: 0.3
+ nGamesToUpgrade: 3
--- !u!4 &5281182
Transform:
m_ObjectHideFlags: 0
@@ -407,6 +411,14 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
starPrefab: {fileID: 7594402928058972626, guid: 8a1f0e2645d064545887a1468814863e, type: 3}
+ edgeOffset: 1
+ nRows: 4
+ nColumns: 6
+ gridPositionsToExclude:
+ - {x: 0, y: 3}
+ - {x: 5, y: 3}
+ - {x: 5, y: 0}
+ minDistance: 7
--- !u!4 &609642610
Transform:
m_ObjectHideFlags: 0
diff --git a/projects/AstroBalance/Assets/Scripts/SaveData.cs b/projects/AstroBalance/Assets/Scripts/SaveData.cs
index 1b90890..d51c487 100644
--- a/projects/AstroBalance/Assets/Scripts/SaveData.cs
+++ b/projects/AstroBalance/Assets/Scripts/SaveData.cs
@@ -45,6 +45,15 @@ public T GetLastGameData()
return savedGames.LastOrDefault();
}
+ ///
+ /// Get data from the last n played games.
+ ///
+ /// Number of games to retrieve
+ public IEnumerable GetLastNGamesData(int nGames)
+ {
+ return savedGames.TakeLast(nGames);
+ }
+
public void Save()
{
string json = JsonUtility.ToJson(this, true);
diff --git a/projects/AstroBalance/Assets/Scripts/StarSeek/LockedOn.cs b/projects/AstroBalance/Assets/Scripts/StarSeek/LockedOn.cs
index 1afaadc..9e7b522 100644
--- a/projects/AstroBalance/Assets/Scripts/StarSeek/LockedOn.cs
+++ b/projects/AstroBalance/Assets/Scripts/StarSeek/LockedOn.cs
@@ -5,7 +5,7 @@
public class LockedOn : MonoBehaviour
{
[SerializeField, Tooltip("Number of seconds required to collect star")]
- private float doubleLockTime = 2;
+ private float doubleLockTime = 0.5f;
[SerializeField, Tooltip("Particle system to be shown on star collection")]
private GameObject collectEffect;
diff --git a/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekGenerator.cs b/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekGenerator.cs
index c338705..bb73867 100644
--- a/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekGenerator.cs
+++ b/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekGenerator.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
@@ -8,47 +9,126 @@ public class StarSeekGenerator : MonoBehaviour
[SerializeField, Tooltip("Star prefab to generate")]
private GameObject starPrefab;
- private List spawnLocations = new List
- {
- new Vector2(-8, 0), // left
- new Vector2(8, 0), // right
- new Vector2(0, 4), // up
- new Vector2(0, -4), // down
- };
+ [SerializeField, Tooltip("Min distance between stars and the edge of the screen.")]
+ private int edgeOffset = 1;
+
+ [SerializeField, Tooltip("Number of rows in star spawn grid")]
+ private int nRows = 4;
+
+ [SerializeField, Tooltip("Number of columns in star spawn grid")]
+ private int nColumns = 6;
+
+ [
+ SerializeField,
+ Tooltip(
+ "Positions in the spawn grid to exclude (e.g. overlapping with UI elements). (0, 0) is the bottom left star and (nColumns - 1, nRows - 1) is the top right star."
+ )
+ ]
+ private List gridPositionsToExclude = new List();
+
+ [SerializeField, Tooltip("Min distance between spawned stars"), Range(0, 9)]
+ private int minDistance = 7;
+
+ private List spawnLocations = new List();
private GameObject currentStar;
- private int lastSpawnLocationIndex = -1;
+ private bool firstSpawn = true;
+ private Vector2 lastSpawnLocation;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
- spawnStar();
+ FillSpawnLocations();
+ SpawnStar();
}
- private void spawnStar()
+ ///
+ /// Create a grid of spawn locations, excluding those in 'gridPositionsToExclude'
+ ///
+ private void FillSpawnLocations()
{
- int chosenIndex;
- if (lastSpawnLocationIndex == -1)
+ Vector2 gridBottomLeft =
+ Camera.main.ViewportToWorldPoint(new Vector2(0, 0))
+ + new Vector3(edgeOffset, edgeOffset, 0);
+ Vector2 gridTopRight =
+ Camera.main.ViewportToWorldPoint(new Vector2(1, 1))
+ - new Vector3(edgeOffset, edgeOffset, 0);
+
+ float xSpacing = (gridTopRight.x - gridBottomLeft.x) / (nColumns - 1);
+ float ySpacing = (gridTopRight.y - gridBottomLeft.y) / (nRows - 1);
+
+ float xLocation = gridBottomLeft.x;
+ float yLocation = gridBottomLeft.y;
+ for (int i = 0; i < nColumns; i++)
+ {
+ for (int j = 0; j < nRows; j++)
+ {
+ AddSpawnLocation(new Vector2(xLocation, yLocation), new Vector2(i, j));
+ yLocation += ySpacing;
+ }
+
+ xLocation += xSpacing;
+ yLocation = gridBottomLeft.y;
+ }
+ }
+
+ ///
+ /// Add spawn location, if it is not in 'gridPositionsToExclude'
+ ///
+ /// Unity world position to spawn
+ /// Position in grid (column, row)
+ private void AddSpawnLocation(Vector2 worldPosition, Vector2 gridPosition)
+ {
+ foreach (Vector2 excludeLocation in gridPositionsToExclude)
+ {
+ if (gridPosition == excludeLocation)
+ {
+ return;
+ }
+ }
+
+ spawnLocations.Add(worldPosition);
+ }
+
+ private void SpawnStar()
+ {
+ List possibleLocations = new List();
+
+ if (firstSpawn)
{
// If this is our first time generating a star, choose from all locations
- chosenIndex = Random.Range(0, spawnLocations.Count);
+ possibleLocations = spawnLocations;
+ firstSpawn = false;
}
else
{
- // If we have spawned a star previously, make sure it moves to a new location
- IEnumerable indexes = Enumerable.Range(0, spawnLocations.Count);
- indexes = indexes.Except(new int[] { lastSpawnLocationIndex });
+ // If we have spawned a star previously, choose a new location that is at
+ // least minDistance away
+ foreach (Vector2 spawnLocation in spawnLocations)
+ {
+ float distance = Vector2.Distance(spawnLocation, lastSpawnLocation);
+ if (distance >= minDistance)
+ {
+ possibleLocations.Add(spawnLocation);
+ }
+ }
+ }
- chosenIndex = indexes.ElementAt(Random.Range(0, indexes.Count()));
+ if (possibleLocations.Count() == 0)
+ {
+ throw new InvalidOperationException(
+ "No valid spawn locations available - try decreasing minDistance."
+ );
}
- // Spawn a star at the randomly chosen location
- Vector2 chosenLocation = spawnLocations.ElementAt(chosenIndex);
+ // Spawn a star at a randomly chosen location
+ int chosenIndex = UnityEngine.Random.Range(0, possibleLocations.Count);
+ Vector2 chosenLocation = possibleLocations.ElementAt(chosenIndex);
currentStar = Instantiate(
starPrefab,
new Vector3(chosenLocation.x, chosenLocation.y, 0),
Quaternion.identity
);
- lastSpawnLocationIndex = chosenIndex;
+ lastSpawnLocation = chosenLocation;
}
// Update is called once per frame
@@ -56,7 +136,7 @@ void Update()
{
if (currentStar.IsDestroyed())
{
- spawnStar();
+ SpawnStar();
}
}
}
diff --git a/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekManager.cs b/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekManager.cs
index 0a3789f..93b95a5 100644
--- a/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekManager.cs
+++ b/projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekManager.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Linq;
using TMPro;
using UnityEngine;
@@ -12,10 +14,33 @@ public class StarSeekManager : MonoBehaviour
[SerializeField, Tooltip("Screen shown upon winning the game")]
private GameObject winScreen;
- [SerializeField, Tooltip("Game time limit in seconds")]
- private int timeLimit = 120;
+ [SerializeField, Tooltip("Minimum game time limit in seconds")]
+ private int minTimeLimit = 60;
+
+ [SerializeField, Tooltip("Maximum game time limit in seconds")]
+ private int maxTimeLimit = 180;
+
+ [SerializeField, Tooltip("Time limit increase if timeLimitUpgradeRate is met")]
+ private int timeLimitIncrement = 60;
+
+ [
+ SerializeField,
+ Tooltip(
+ "(number of stars collected / game time limit) i.e. average stars collected per second - must be above this value to increase the time limit of future games."
+ )
+ ]
+ private float timeLimitUpgradeRate = 0.3f;
+
+ [
+ SerializeField,
+ Tooltip(
+ "Number of games in a row that must meet timeLimitUpgradeRate to increase the time limit"
+ )
+ ]
+ private int nGamesToUpgrade = 3;
private int score;
+ private int timeLimit;
private TextMeshProUGUI winText;
private bool gameActive = true;
private StarSeekData gameData;
@@ -25,12 +50,62 @@ public class StarSeekManager : MonoBehaviour
void Start()
{
winText = winScreen.GetComponentInChildren();
+ ChooseGameTimeLimit();
+
score = 0;
scoreText.text = score.ToString();
gameData = new StarSeekData();
timer.StartCountdown(timeLimit);
}
+ ///
+ /// Load previous game data (if any), and choose time limit for this game based
+ /// on prior perfomance.
+ ///
+ private void ChooseGameTimeLimit()
+ {
+ SaveData saveData = new(saveFilename);
+ IEnumerable lastNGamesData = saveData.GetLastNGamesData(nGamesToUpgrade);
+
+ if (lastNGamesData.Count() < nGamesToUpgrade)
+ {
+ SetTimeLimit(minTimeLimit);
+ return;
+ }
+
+ // Upgrade if all the last n games have the same time limit + meet the upgrade
+ // rate. If it's a mix of time limits, then we haven't played enough games at
+ // this level yet to progress.
+ int nGamesAtUpgradeRate = 0;
+ bool allSameTimeLimit = true;
+ int lastTimeLimit = lastNGamesData.Last().timeLimitSeconds;
+
+ foreach (StarSeekData data in lastNGamesData)
+ {
+ float starRate = (float)data.nStarsCollected / (float)data.timeLimitSeconds;
+
+ if (data.timeLimitSeconds != lastTimeLimit)
+ {
+ allSameTimeLimit = false;
+ break;
+ }
+
+ if (starRate >= timeLimitUpgradeRate)
+ {
+ nGamesAtUpgradeRate++;
+ }
+ }
+
+ if (allSameTimeLimit && nGamesAtUpgradeRate >= nGamesToUpgrade)
+ {
+ SetTimeLimit(lastTimeLimit + timeLimitIncrement);
+ }
+ else
+ {
+ SetTimeLimit(lastTimeLimit);
+ }
+ }
+
// Update is called once per frame
void Update()
{
@@ -77,4 +152,20 @@ private void SaveGameData()
SaveData saveData = new(saveFilename);
saveData.SaveGameData(gameData);
}
+
+ private void SetTimeLimit(int limit)
+ {
+ if (limit > maxTimeLimit)
+ {
+ timeLimit = maxTimeLimit;
+ }
+ else if (limit < minTimeLimit)
+ {
+ timeLimit = minTimeLimit;
+ }
+ else
+ {
+ timeLimit = limit;
+ }
+ }
}