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
12 changes: 11 additions & 1 deletion docs/star-seek.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion projects/AstroBalance/Assets/Scenes/StarSeek.unity
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions projects/AstroBalance/Assets/Scripts/SaveData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ public T GetLastGameData()
return savedGames.LastOrDefault();
}

/// <summary>
/// Get data from the last n played games.
/// </summary>
/// <param name="nGames">Number of games to retrieve</param>
public IEnumerable<T> GetLastNGamesData(int nGames)
{
return savedGames.TakeLast(nGames);
}

public void Save()
{
string json = JsonUtility.ToJson(this, true);
Expand Down
2 changes: 1 addition & 1 deletion projects/AstroBalance/Assets/Scripts/StarSeek/LockedOn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
122 changes: 101 additions & 21 deletions projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
Expand All @@ -8,55 +9,134 @@ public class StarSeekGenerator : MonoBehaviour
[SerializeField, Tooltip("Star prefab to generate")]
private GameObject starPrefab;

private List<Vector2> spawnLocations = new List<Vector2>
{
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<Vector2> gridPositionsToExclude = new List<Vector2>();

[SerializeField, Tooltip("Min distance between spawned stars"), Range(0, 9)]
private int minDistance = 7;

private List<Vector2> spawnLocations = new List<Vector2>();
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()
/// <summary>
/// Create a grid of spawn locations, excluding those in 'gridPositionsToExclude'
/// </summary>
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;
}
}

/// <summary>
/// Add spawn location, if it is not in 'gridPositionsToExclude'
/// </summary>
/// <param name="worldPosition">Unity world position to spawn</param>
/// <param name="gridPosition">Position in grid (column, row)</param>
private void AddSpawnLocation(Vector2 worldPosition, Vector2 gridPosition)
{
foreach (Vector2 excludeLocation in gridPositionsToExclude)
{
if (gridPosition == excludeLocation)
{
return;
}
}

spawnLocations.Add(worldPosition);
}

private void SpawnStar()
{
List<Vector2> possibleLocations = new List<Vector2>();

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<int> 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
void Update()
{
if (currentStar.IsDestroyed())
{
spawnStar();
SpawnStar();
}
}
}
95 changes: 93 additions & 2 deletions projects/AstroBalance/Assets/Scripts/StarSeek/StarSeekManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;

Expand All @@ -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;
Expand All @@ -25,12 +50,62 @@ public class StarSeekManager : MonoBehaviour
void Start()
{
winText = winScreen.GetComponentInChildren<TextMeshProUGUI>();
ChooseGameTimeLimit();

score = 0;
scoreText.text = score.ToString();
gameData = new StarSeekData();
timer.StartCountdown(timeLimit);
}

/// <summary>
/// Load previous game data (if any), and choose time limit for this game based
/// on prior perfomance.
/// </summary>
private void ChooseGameTimeLimit()
{
SaveData<StarSeekData> saveData = new(saveFilename);
IEnumerable<StarSeekData> 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()
{
Expand Down Expand Up @@ -77,4 +152,20 @@ private void SaveGameData()
SaveData<StarSeekData> 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;
}
}
}