diff --git a/Assets/Scripts/Configuration.cs b/Assets/Scripts/Configuration.cs index 0dc70f0..972ba6f 100644 --- a/Assets/Scripts/Configuration.cs +++ b/Assets/Scripts/Configuration.cs @@ -1,6 +1,7 @@ using System.IO; using static ShootAR.Spawner; using UnityEngine; +using System.Xml; namespace ShootAR { /// @@ -11,7 +12,10 @@ public class Configuration { private static Configuration instance; public static Configuration Instance { get { - if (instance == null) instance = new Configuration(); + if (instance == null) { + instance = new Configuration(); + instance.Initialize(); + } return instance; } @@ -29,6 +33,21 @@ public int SpawnPatternSlot { spawnPatternSlot = value; UnsavedChanges = true; + // Get how many levels are in the spawn pattern: + int numberOfLevels = 0; + using ( + XmlReader element = XmlReader.Create(Configuration.Instance.SpawnPatternFile) + ) { + element.MoveToContent(); + element.ReadToDescendant("level"); + do { + numberOfLevels++; + } while (element.ReadToNextSibling("level")); + + NumberOfLevels = numberOfLevels; + } + // --- + OnSlotChanged?.Invoke(); } } @@ -44,6 +63,9 @@ public string SpawnPatternFile { get => $"{patternsDir.FullName}/{SpawnPattern}.xml"; } + /// The total number of levels defined in the selected pattern file. + public int NumberOfLevels { get; private set; } + private bool soundMuted = false; public bool SoundMuted { @@ -105,8 +127,11 @@ public float Volume { /// File where high-scores are stored. public FileInfo Highscores { get; private set; } - ///Constructor that extracts values from config file - private Configuration() { + /* Putting the initialization code in the constructor causes it to fall into a + recursive loop of trying to create the singleton object every time a member of the + instance object is called. Moving the code out of the constructor and calling it + after the object has already been created solves this issue. */ + private void Initialize() { patternsDir = new DirectoryInfo(Path.Combine( Application.persistentDataPath, PATTERNS_DIR @@ -127,19 +152,6 @@ private Configuration() { HIGHSCORES_DIR )); - /* Read config file before calling CreateFile to avoid needlessly - * reading the same default values from the just-created config file. */ - if (configFile.Exists) { - using (BinaryReader reader = new BinaryReader(configFile.OpenRead())) { - /* The order that the data are read must be the same as the - * the order they are stored. */ - SoundMuted = reader.ReadBoolean(); - BgmMuted = reader.ReadBoolean(); - Volume = reader.ReadSingle(); - SpawnPatternSlot = reader.ReadInt32(); - } - } - CreateFiles(); // Read names of spawn patterns from file and fill up SpawnPatterns. @@ -167,6 +179,16 @@ private Configuration() { if (SpawnPatternSlot >= SpawnPatterns.Length) SpawnPatternSlot = 0; + // Read config file + using (BinaryReader reader = new BinaryReader(configFile.OpenRead())) { + /* The order that the data are read must be the same as + * the order they are stored. */ + SoundMuted = reader.ReadBoolean(); + BgmMuted = reader.ReadBoolean(); + Volume = reader.ReadSingle(); + SpawnPatternSlot = reader.ReadInt32(); + } + Highscores = new FileInfo(Path.Combine( highscoresDir.FullName, @@ -194,7 +216,7 @@ public void SaveSettings() { using (BinaryWriter writer = new BinaryWriter(configFile.OpenWrite())) { /* The order the data is written must be the same as - * the order the constructor reads them. */ + * the order they are read. */ writer.Write(SoundMuted); writer.Write(BgmMuted); writer.Write(Volume); diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index d34b3f8..73d6995 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -5,7 +5,7 @@ using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; -using System.Collections; +using System.Xml; namespace ShootAR { @@ -221,11 +221,16 @@ private void AdvanceLevel() { Debug.Log($"Advancing to level {gameState.Level}"); #endif + /* If the current level exceeds the total number of defined levels, + * translate the index to the equivalent level in pattern file. */ + int l = gameState.Level % Configuration.Instance.NumberOfLevels; + int roundedLevel = l == 0 ? Configuration.Instance.NumberOfLevels : l; + // Configuring spawners - Stack[] patterns - = Spawner.ParseSpawnPattern(Configuration.Instance.SpawnPatternFile); + Dictionary> patterns + = Spawner.ParseSpawnPattern(Configuration.Instance.SpawnPatternFile, roundedLevel); - Spawner.SpawnerFactory(patterns, 0, ref spawnerGroups, ref stashedSpawners); + Spawner.SpawnerFactory(patterns, ref spawnerGroups, ref stashedSpawners); int totalEnemies = 0; foreach (var group in spawnerGroups) { @@ -242,18 +247,18 @@ private void AdvanceLevel() { /* Player should always have enough ammo to play the next * round. If they already have more than enough, they get * points. */ - ulong difference = (ulong)(player.Ammo - totalEnemies); + int difference = player.Ammo - totalEnemies; if (difference > 0) - scoreManager.AddScore(difference * 10); + scoreManager.AddScore((ulong)difference * 10); else if (difference < 0) { /* If it is before the 1st round, give player more bullets * so they are allowed to miss shots. */ const float bonusBullets = 0.55f; if (gameState.Level == 1) { - difference *= (ulong)bonusBullets; + difference = (int)(difference * bonusBullets); } - player.Ammo += (difference < int.MaxValue) ? -(int)difference : int.MaxValue; + player.Ammo += -difference; } gameState.RoundWon = false; diff --git a/Assets/Scripts/Game/Spawner.cs b/Assets/Scripts/Game/Spawner.cs index 5896ac4..1e37633 100644 --- a/Assets/Scripts/Game/Spawner.cs +++ b/Assets/Scripts/Game/Spawner.cs @@ -303,7 +303,7 @@ public void StopSpawning() { } - public static Stack[] ParseSpawnPattern(string spawnPatternFilePath, int level = 1) { + public static Dictionary> ParseSpawnPattern(string spawnPatternFilePath, int level = 1) { Type type = default; int limit = default, multiplier = -1; float rate = default, delay = default, @@ -319,16 +319,18 @@ public static Stack[] ParseSpawnPattern(string spawnPatternFilePath * the same node. */ while (!doneParsingForCurrentLevel) { - if (!(xmlPattern?.Read() ?? false)) { + if (!(xmlPattern?.Read() ?? false)) { //< Moving to next element happens here. xmlPattern = XmlReader.Create(spawnPatternFilePath); xmlPattern.MoveToContent(); - } - // skip to wanted level - if (level > 1) { + // skip to wanted level xmlPattern.ReadToDescendant("level"); - for (int i = 1; i < level; i++) { - xmlPattern.Skip(); + for (int i = 1; level > i; i++) { + if (!xmlPattern.ReadToNextSibling("level")) { + throw new UnityException( + "Not enough levels in spawn pattern." + ); + } } } @@ -421,100 +423,83 @@ public static Stack[] ParseSpawnPattern(string spawnPatternFilePath } } - xmlPattern.Close(); + xmlPattern.Dispose(); - Stack[] extractedPatterns = new Stack[groupsByType.Count]; - groupsByType.Values.CopyTo(extractedPatterns, 0); - return extractedPatterns; + return groupsByType; } public static void SpawnerFactory( - Stack[] spawnPatterns, int index, + Dictionary> spawnPatterns, ref Dictionary> spawners, ref Stack stashedSpawners) { - /* A list to keep track of which types of spawners - * are in the pattern and should not be stashed away. */ - List requiredSpawnerTypes = new List(); + /* If spawners.Keys is used directly in foreach, the enumeration + stops because its values gets changed. A copy of the values is + used to avoid that. */ + Type[] keys = new Type[spawners.Keys.Count]; + spawners.Keys.CopyTo(keys, 0); - bool recursed = false; - - for (int id = index; id < spawnPatterns.Length; id++) { - Stack pattern = spawnPatterns[id]; - Type type = pattern.Peek().type; - if (!requiredSpawnerTypes.Contains(type)) - requiredSpawnerTypes.Add(type); + /* Stash entire group of spawners when that type is not used + * this round. */ + foreach (Type type in keys) { + if (spawnPatterns.ContainsKey(type)) { + foreach (Spawner spawner in spawners[type]) { + stashedSpawners.Push(spawner); + } + spawners.Remove(type); + } + } - /* Skip indices that we don't care about. - * But even if we don't care about those skipped patterns, the - * type still needs to be tracked first so that the spawners - * that are actually needed don't end up stashed away. */ - if (id < index) continue; + Dictionary spawnersRequired = new Dictionary(); + /* Check how many spawners will be needed. + If spawners' list contains more spawners of a type, stash them away. + This step goes before parsing the actual patterns, to make sure that + all unused spawners are stashed and availiable to be reused when needed. */ + foreach (Type type in spawnPatterns.Keys) { if (!spawners.ContainsKey(type)) spawners.Add(type, new List(0)); - int spawnersRequired = pattern.Count; - int spawnersAvailable = spawners[type].Count; + int required = spawnPatterns[type].Count - spawners[type].Count; - // If there are not enough spawners available take from stash - if (spawnersRequired > spawnersAvailable) { - // How many spawners will be taken from stash? - int spawnersReused; - if (spawnersRequired <= spawnersAvailable + stashedSpawners.Count) - spawnersReused = spawnersRequired - spawnersAvailable; - else - spawnersReused = stashedSpawners.Count; + if (required > 0) + spawnersRequired.Add(type, required); - for (int i = 0; i < spawnersReused; i++) { - spawners[type].Add(stashedSpawners.Pop()); - spawnersRequired--; - } + else if (required < 0) /* Stash excess spawners */ { + for (int i = spawners[type].Count; i < spawners[type].Count + required; i--) + stashedSpawners.Push(spawners[type][i]); - /* If there are still not enough spawners, continue to the - * rest of the patterns hoping that more spawners will be - * stashed in the meantime. */ - int recursionIndex = index + id + 1; - if (spawnersRequired > 0 && recursionIndex < spawnPatterns.Length) { - SpawnerFactory(spawnPatterns, recursionIndex, - ref spawners, ref stashedSpawners); - - recursed = true; - - // Take spawners from stash - for ( - int i = stashedSpawners.Count; - i != 0 && spawnersRequired > 0; - i-- - ) { - spawners[type].Add(stashedSpawners.Pop()); - spawnersRequired--; - } - } + spawners[type].RemoveRange(spawners[type].Count + required, required); + } + } - // If there are still not enough spawners, create new - while (spawnersRequired > 0) { - spawners[type].Add(Instantiate( - Resources.Load(Prefabs.SPAWNER))); + // Configure spawners + foreach (Type type in spawnPatterns.Keys) { + int spawnersReused; // How many spawners will be taken from stash - spawnersRequired--; - } + // Take spawners from stash + if (spawnersRequired[type] <= stashedSpawners.Count) + spawnersReused = spawnersRequired[type]; + else + spawnersReused = stashedSpawners.Count; + + for (int i = 0; i < spawnersReused; i++) { + spawners[type].Add(stashedSpawners.Pop()); } - else if (spawnersRequired < spawnersAvailable) { - // Stash leftover spawners. - int firstLeftover = spawnersRequired + 1, - leftoversCount = spawnersAvailable - spawnersRequired; + spawnersRequired[type] -= spawnersReused; - for (int i = firstLeftover; i < leftoversCount; i++) - stashedSpawners.Push(spawners[type][i]); + // If there are not enough stashed spawners, create new. + while (spawnersRequired[type] > 0) { + spawners[type].Add(Instantiate( + Resources.Load(Prefabs.SPAWNER))); - spawners[type].RemoveRange(firstLeftover, leftoversCount); + spawnersRequired[type]--; } // Configure spawner using the retrieved data. foreach (Spawner spawner in spawners[type]) { - spawner.Configure(pattern.Pop()); + spawner.Configure(spawnPatterns[type].Pop()); } // Populating pools @@ -544,21 +529,6 @@ public static void SpawnerFactory( else if (type == typeof(PowerUpCapsule) && Spawnable.Pool.Instance.Count == 0) { Spawnable.Pool.Instance.Populate(); } - - /* If a recursion happened then the rest patterns have already - * been parsed, meaning that there is no need to continue the - * loop. */ - if (recursed) return; - } - - /* Stash entire group of spawners when that type is not used - * this round. */ - foreach (var type in requiredSpawnerTypes) { - if (!spawners.ContainsKey(type)) { - for (int i = 0; i < spawners[type].Count; i++) - stashedSpawners.Push(spawners[type][i]); - spawners.Remove(type); - } } } } diff --git a/Assets/Tests/PatternsFileTests.cs b/Assets/Tests/PatternsFileTests.cs index cc47dcc..530d78e 100644 --- a/Assets/Tests/PatternsFileTests.cs +++ b/Assets/Tests/PatternsFileTests.cs @@ -16,7 +16,7 @@ public void CopyFileToPermDataPath() { LocalFiles.CopyResourceToPersistentData(patternFileBasename, patternFile); Assert.That(File.Exists(targetFile)); - File.Delete(patternFile); + File.Delete(targetFile); } [Test] diff --git a/Assets/Tests/TestTools.cs b/Assets/Tests/TestTools.cs index 3106462..8a9adc2 100644 --- a/Assets/Tests/TestTools.cs +++ b/Assets/Tests/TestTools.cs @@ -199,6 +199,11 @@ public void FileSetUp() { writer.Write(1); // one string in file writer.Write(PATTERN_FILE.TrimEnd(".xml".ToCharArray())); } + + // Make sure the patterns directory exists. + DirectoryInfo patternsDir = new DirectoryInfo(Path.Combine( + Application.persistentDataPath, Configuration.PATTERNS_DIR)); + if (!patternsDir.Exists) patternsDir.Create(); } [TearDown] @@ -212,6 +217,12 @@ public void DeletePatternFile() { if (File.Exists(file)) File.Delete(file); + string highscoreFile = Path.Combine( + Application.persistentDataPath, Configuration.HIGHSCORES_DIR, + PATTERN_FILE + ); + if (File.Exists(highscoreFile)) File.Delete(highscoreFile); + Assert.That( !File.Exists(file), "The file should be deleted when the test ends."