From e1d7ec5109cc8e12bdc8ecc5241963f2c1f30642 Mon Sep 17 00:00:00 2001 From: AnimaRain Date: Mon, 7 Dec 2020 03:45:01 +0200 Subject: [PATCH 1/6] Fix looping back to the top of spawn pattern Signed-off-by: AnimaRain --- Assets/Scripts/Game/GameManager.cs | 22 +++++++++++++++++++++- Assets/Scripts/Game/Spawner.cs | 16 +++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index d34b3f8..2acf8d3 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -6,6 +6,7 @@ using UnityEngine.SceneManagement; using UnityEngine.UI; using System.Collections; +using System.Xml; namespace ShootAR { @@ -221,9 +222,28 @@ private void AdvanceLevel() { Debug.Log($"Advancing to level {gameState.Level}"); #endif + // 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")); + } + + int roundedLevel; // round index translated to level in the spawn file + if (gameState.Level > numberOfLevels + && gameState.Level / numberOfLevels > 0) + { + roundedLevel = gameState.Level / numberOfLevels; + } + else + roundedLevel = gameState.Level; + // Configuring spawners Stack[] patterns - = Spawner.ParseSpawnPattern(Configuration.Instance.SpawnPatternFile); + = Spawner.ParseSpawnPattern(Configuration.Instance.SpawnPatternFile, roundedLevel); Spawner.SpawnerFactory(patterns, 0, ref spawnerGroups, ref stashedSpawners); diff --git a/Assets/Scripts/Game/Spawner.cs b/Assets/Scripts/Game/Spawner.cs index 5896ac4..fdea79b 100644 --- a/Assets/Scripts/Game/Spawner.cs +++ b/Assets/Scripts/Game/Spawner.cs @@ -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,7 +423,7 @@ public static Stack[] ParseSpawnPattern(string spawnPatternFilePath } } - xmlPattern.Close(); + xmlPattern.Dispose(); Stack[] extractedPatterns = new Stack[groupsByType.Count]; groupsByType.Values.CopyTo(extractedPatterns, 0); From f75ebd00e059f0f70c0c5fdea7ef878a695fb43b Mon Sep 17 00:00:00 2001 From: AnimaRain Date: Sat, 2 Jan 2021 04:38:05 +0200 Subject: [PATCH 2/6] Fix mishandling spawners between rounds Spawners where not always being stashed correctly. This caused errors with the recursion, which led to not enough spawners being added to the list, and not having enough spawners to assign the patterns. Function SpawnerFactory had to be heavily reworked. Now it works correclty, but is also simpler, and makes more sense. Function ParseSpawnPattern is changed to return a dictionary instead of an array: 1) It is more convenient for being "piped" into SpawnerFactory. 2) It doesn't make sense converting the data from a dictionary to an array before returning, and then converting the array back to a dictionary. Signed-off-by: AnimaRain --- Assets/Scripts/Game/GameManager.cs | 4 +- Assets/Scripts/Game/Spawner.cs | 134 +++++++++++------------------ 2 files changed, 53 insertions(+), 85 deletions(-) diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 2acf8d3..1793a4d 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -242,10 +242,10 @@ private void AdvanceLevel() { roundedLevel = gameState.Level; // Configuring spawners - Stack[] patterns + 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) { diff --git a/Assets/Scripts/Game/Spawner.cs b/Assets/Scripts/Game/Spawner.cs index fdea79b..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, @@ -425,98 +425,81 @@ public static Stack[] ParseSpawnPattern(string spawnPatternFilePath 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 @@ -546,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); - } } } } From 2e67d6a4728cabae25a3122cdbdf5729eb15e427 Mon Sep 17 00:00:00 2001 From: AnimaRain Date: Sun, 25 Apr 2021 20:47:57 +0300 Subject: [PATCH 3/6] Refactor points and bullets reward between rounds This refactoring is reverting some changes in commit 1cc85a6ffb70, with some additional changes. There is a possibility that, previously, convertions to ulong were messing up the arithmetic results, but that has not been confirmed. Either way, after these changes, there shouldn't be this problem anyway. Signed-off-by: AnimaRain --- Assets/Scripts/Game/GameManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 1793a4d..0cee88f 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -262,18 +262,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; From 87d01be31f60c62cb1881bc02435d31ce79a81aa Mon Sep 17 00:00:00 2001 From: AnimaRain Date: Tue, 27 Apr 2021 20:03:48 +0300 Subject: [PATCH 4/6] Simplify getting the number of levels Instead of, every time the number of levels is requested, going through the XML file and counting the nodes, it is calculated once and stored in a public property when the pattern file is loaded. Signed-off-by: AnimaRain --- Assets/Scripts/Configuration.cs | 19 +++++++++++++++++++ Assets/Scripts/Game/GameManager.cs | 23 ++++------------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Assets/Scripts/Configuration.cs b/Assets/Scripts/Configuration.cs index 0dc70f0..6fee5cd 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 { /// @@ -29,6 +30,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 +60,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 { diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 0cee88f..73d6995 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -5,7 +5,6 @@ using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; -using System.Collections; using System.Xml; namespace ShootAR @@ -222,24 +221,10 @@ private void AdvanceLevel() { Debug.Log($"Advancing to level {gameState.Level}"); #endif - // 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")); - } - - int roundedLevel; // round index translated to level in the spawn file - if (gameState.Level > numberOfLevels - && gameState.Level / numberOfLevels > 0) - { - roundedLevel = gameState.Level / numberOfLevels; - } - else - roundedLevel = gameState.Level; + /* 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 Dictionary> patterns From 7e13f3ee58a552a12728ab7d241b22433de00e55 Mon Sep 17 00:00:00 2001 From: AnimaRain Date: Wed, 26 May 2021 14:59:55 +0300 Subject: [PATCH 5/6] Fix unending loop in Configuration constructor Signed-off-by: AnimaRain --- Assets/Scripts/Configuration.cs | 37 ++++++++++++++++++--------------- Assets/Tests/TestTools.cs | 5 +++++ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Assets/Scripts/Configuration.cs b/Assets/Scripts/Configuration.cs index 6fee5cd..972ba6f 100644 --- a/Assets/Scripts/Configuration.cs +++ b/Assets/Scripts/Configuration.cs @@ -12,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; } @@ -124,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 @@ -146,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. @@ -186,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, @@ -213,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/Tests/TestTools.cs b/Assets/Tests/TestTools.cs index 3106462..8dcff19 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] From db27894b502ce081f7b70f24e69de40437b8112b Mon Sep 17 00:00:00 2001 From: AnimaRain Date: Fri, 28 May 2021 17:19:19 +0300 Subject: [PATCH 6/6] Fix file tests not cleaning after themselves properly Signed-off-by: AnimaRain --- Assets/Tests/PatternsFileTests.cs | 2 +- Assets/Tests/TestTools.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 8dcff19..8a9adc2 100644 --- a/Assets/Tests/TestTools.cs +++ b/Assets/Tests/TestTools.cs @@ -217,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."