Untitled Roguelite game
I am currently in the process of completing a demo for a roguelite game.
In the game you will be controlling a small spacecraft and your mission is to gather resources by mining asteroids and blowing up your enemies.
You will play as a cat that is defending their clouders space. Resources are very limited and other clouders are trying to take over. Fight back agaisnt the enemy waves.
Upgrade your ship and spacestation to help you survive a little longer each time. Progress through space to finally defeat the Chief.
The GitHub repository for the project can be found here and please change the branch to Main.
Below is some gifs videos and images of how the game is coming along.


Below are some of the systems that are currently in developement and the current code.
Created an achievement system that handles the tracking of stats and the checking of when the achievements have been achieved. This includes creating a pop-up notification that uses a queue to load up any sequential achievements that the player has gotten. The system waits for the local game achievement system to finish before running the code for popping the achievements on other platforms(steam, when its ready).
using DangryGames;
using UnityEngine;
using UnityEngine.SocialPlatforms.Impl;
using UnityEngine.UI;
#if PLATFORM_ANDROID
using GooglePlayGames;
#endif
[CreateAssetMenu(fileName = "Achievement", menuName = "Achievement", order =1)]
public class Achievement: ScriptableObject
{
public AchievementInfo achievementInfo;
public PermaSkills _linkedSkillToUnlock;
public TutorialNotification _tutorialNotification;
private enum AchievementCheckType
{
AC_LESS = 0,
AC_LESS_OR_EQUAL,
AC_EQUAL,
AC_GREATER_OR_EQUAL,
AC_GREATER,
AC_TYPE_MAX
};
public enum AchievementUnlockType
{
UNLOCK_STORY = 0,
UNLOCK_KILLS,
UNLOCK_SPECIFIC,
UNLOCK_KILLS_WITH_SHIELD,
UNLOCK_TYPE_MAX
};
public bool UnlockAchievement(Achievement achievementPassed, int total, uint hash = 0)
{
//Do some funky stuff here
if (Check(total, achievementPassed.achievementInfo.total, (int)achievementPassed.achievementInfo.check))
{
AddToQue(achievementPassed);
}
return false;
}
public bool Check(int total, int target, int check)
{
switch (check)
{
case (int)AchievementCheckType.AC_LESS: return (total < target);
case (int)AchievementCheckType.AC_LESS_OR_EQUAL: return (total <= target);
case (int)AchievementCheckType.AC_EQUAL: return (total == target);
case (int)AchievementCheckType.AC_GREATER_OR_EQUAL: return (total >= target);
case (int)AchievementCheckType.AC_GREATER: return (total > target);
}
return false;
}
public void ResetAchievement(Achievement ach)
{
ach.achievementInfo.unlocked = false;
}
public bool AddToQue(Achievement achievement)
{
if (IsAchievementUnlocked(achievement)) return false;
UnlockingAchievement(achievement);
return true;
}
private bool IsAchievementUnlocked(Achievement achievement)
{
if (achievement.achievementInfo.unlocked)
{
return true;
}
return false;
}
private bool UnlockingAchievement(Achievement achievement)
{
if (!achievement.achievementInfo.unlocked)
{
achievement.achievementInfo.unlocked = true;
if (GameManager.Instance != null)
{
GameManager.Instance._achievementNotificationController.AddToQueue(achievement);
if (_linkedSkillToUnlock != null)
{
_linkedSkillToUnlock.UnlockSkill();
_linkedSkillToUnlock.AddSkillToUnlockedList();
SaveData.Instance.SaveSkillsToJson();
//Tell player something has unlocked
}
}
//Do some analytics stuff here possibly
//Saving Achievements once unlocked
SaveData.Instance.SaveAchievementsToJson();
return true;
}
return false;
}
}
[System.Serializable]
public struct AchievementInfo
{
public enum ACHIEVEMENTTYPE
{
STORY,
KILLS,
SPECIFIC,
KILLS_SHIELD
}
public ACHIEVEMENTTYPE type;
public enum CHECKTYPE
{
l, //less
le, //less equal
e, //equal
ge, //greater equal
g //greater
}
public CHECKTYPE check;
public string name;
public string id;
public int score;
public int total;
public string specificType;
public Texture2D texture;
public string descriptionLocked;
public string descriptionUnlocked;
public bool unlocked;
public uint specific;
public string localizationFieldName;
public string steamID;
public bool showIfNotUnlocked;
}
using DangryGames;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AchievementNotificationController : MonoBehaviour
{
public Queue notifications;
public AchievementNotification achievementNotification;
float m_hold_timer;
List notificationList;
[Header("Analytics")]
public DangryGames.IntVariable achievementCount;
private void Awake()
{
notifications = new Queue();
notificationList = new List();
}
public void AddToQueue(Achievement achivement)
{
notificationList.Add(achivement);
notifications.Enqueue(achivement);
achievementCount.value++;
}
public void HoldNotifications( float time)
{
m_hold_timer = time;
}
private void Update()
{
if (m_hold_timer > 0)
{
m_hold_timer -= Time.deltaTime;
if (notifications.Count > 0)
achievementNotification.Reset();
return;
}
if (notifications.Count <= 0 )
{
if(notificationList.Count > 0)
{
//Unlock achievements on platforms
UnlockPlatformAchievement(notificationList);
}
return;
}
Achievement notification = notifications.Peek();
if (notification && achievementNotification.UpdateAchievement(notification))
{
if(notification._tutorialNotification != null)
{
TutorialPopup.Instance.Init(notification._tutorialNotification, true);
}
notifications.Dequeue();
achievementNotification.gameObject.SetActive(false);
}
}
public void UnlockPlatformAchievement(List achievements)
{
//TODO: add in steam achievements here
achievements.Clear();
}
}
This save system uses JSON to save and load the data from Unity's scriptable objects. Each type of data that is being saved is saving into their own files.
JSON doesn't like scriptable objects, so I had to be very explicit with the types are that being loaded back into Unity and the scriptable objects.
I am looking to improve the encryption of the Save system by using the Rijndael algorithm for Advanced encryption
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System.Xml.Linq;
namespace DangryGames
{
public class SaveData : MonoSingleton
{
[SerializeField] private List _permaSkillsList;
[SerializeField] private List _achievementList;
[SerializeField] private StatsSO _statSO;
[SerializeField] private List _notificationList;
[SerializeField] private PlayerSettings _playerSettings;
[SerializeField] private List _weaponsList;
[SerializeField] private List _astronautList;
[SerializeField] private List _resourcesList;
[SerializeField] private List _astroSkillList;
public PlayerSettings _PlayerSettings
{
get { return _playerSettings; }
}
#region save skills
public void SaveSkillsToJson()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each PermaSkills in the list and populate the saving data
foreach (var skill in _permaSkillsList)
{
SavingPlayerSkills savingData = new SavingPlayerSkills
{
_skillName = skill._skillName,
_uniqueID = skill.UniqueID,
_currentLevel = skill._currentSkillLevel,
_startingValue = skill._startingValue,
_skillType = (int)skill._skillType,
_isUnlocked = skill._isUnlocked,
_currentValue = skill._currentvalue
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string skillData = JsonUtility.ToJson(new SavingWrapper { _skills = savingDataList });
string filePath = Application.persistentDataPath + "/SkillData.json";
// Save the serialized data to a file
File.WriteAllText(filePath, skillData);
Debug.Log($"Data saved to {filePath}");
}
public void LoadSkillsToJson()
{
string filePath = Application.persistentDataPath + "/SkillData.json";
if (File.Exists(filePath))
{
string skillData = File.ReadAllText(filePath);
SavingWrapper loadedDataWrapper = JsonUtility.FromJson(skillData);
foreach (SavingPlayerSkills data in loadedDataWrapper._skills)
{
// Find the matching ScriptableObject by unique ID
PermaSkills skill = _permaSkillsList.Find(s => s._skillName == data._skillName);
if (skill != null)
{
skill._currentSkillLevel = data._currentLevel;
skill._startingValue = data._startingValue;
skill._skillType = (PermaSkills.SkillType)data._skillType;
skill._isUnlocked = data._isUnlocked;
skill._currentvalue = data._currentValue;
}
else
{
Debug.LogWarning($"Skill with UniqueID {data._uniqueID} not found in the game. It may have been removed.");
}
}
}
else
{
Debug.LogWarning("No save file found!");
}
}
#endregion
#region save achievements
public void SaveAchievementsToJson()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each achievement in the list and populate the saving data
foreach (var ach in _achievementList)
{
SavingAchievements savingData = new SavingAchievements
{
_achievementName = ach.achievementInfo.name,
_isUnlocked = ach.achievementInfo.unlocked,
_uniqueID = ach.achievementInfo.id,
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string achData = JsonUtility.ToJson(new AchievementsWrapper { _achievements = savingDataList });
string filePath = Application.persistentDataPath + "/achievements.json";
// Save the serialized data to a file
File.WriteAllText(filePath, achData);
}
public void LoadAchievementsToJson()
{
string filePath = Application.persistentDataPath + "/achievements.json";
// Check if the file exists before trying to load it
if (File.Exists(filePath))
{
string achData = File.ReadAllText(filePath);
// Deserialize the JSON into an AchievementsWrapper
AchievementsWrapper loadedDataWrapper = JsonUtility.FromJson(achData);
List loadedDataList = loadedDataWrapper._achievements;
// Apply the loaded data back to the corresponding achievements in the list
for (int i = 0; i < _achievementList.Count && i < loadedDataList.Count; i++)
{
Achievement achievement = _achievementList.Find(s => s.achievementInfo.name == _achievementList[i].achievementInfo.name);
if (achievement != null)
{
SavingAchievements data = loadedDataList[i];
achievement.achievementInfo.unlocked = data._isUnlocked;
}
}
}
else
{
Debug.LogWarning("No save file found!");
}
}
#endregion
#region Save Stats
public void SaveStats()
{
// Create a new StatsWrapper object to hold the data
StatsWrapper savingStats = new StatsWrapper
{
_enemiesKilled = _statSO._enemiesKilled,
_clustersMined = _statSO._clustersMined,
_oreCollected = _statSO._oreCollected,
_weaponsPickup = _statSO._weaponsPickup,
_bulletsFired = _statSO._bulletsFired,
_timesDodged = _statSO._timesDodged,
_empUsed = _statSO._empUsed,
_bombsUsed = _statSO._bombsUsed,
_homingMissilesUsed = _statSO._homingMissilesUsed,
_multishotUsed = _statSO._multishotUsed,
_damageTaken = _statSO._damageTaken,
_shipsLost = _statSO._shipsLost,
_astronautsFound = _statSO._astronautsFound,
_healthSavedByShield = _statSO._healthSavedByShield,
_bossesDestroyed = _statSO._bossesDestroyed,
_oreSpent = _statSO._oreSpent,
_spaceStationDamageTaken = _statSO._spaceStationDamageTaken,
_spaceStationHealthSavedByShield = _statSO._spaceStationHealthSavedByShield,
_shipsRebuilt = _statSO._shipsRebuilt,
_spaceStationsLost = _statSO._spaceStationsLost,
_totalTimePlayed = _statSO._totalTimePlayed,
_achievementsUnlocked = _statSO._achievementsUnlocked,
_droneKills = _statSO._droneKills,
_spaceStationKills = _statSO._spaceStationKills,
_spaceStationHealedPlayer = _statSO._spaceStationHealedPlayer,
_homingMissileKills = _statSO._homingMissileKills,
_bombKills = _statSO._bombKills,
_empDisables = _statSO._empDisables,
_ingotsCreated = _statSO._ingotsCreated,
_oreOne = _statSO._oreOne,
_oreTwo = _statSO._oreTwo,
_oreThree = _statSO._oreThree,
_oreFour = _statSO._oreFour,
_distanceTravelled = _statSO._distanceTravelled,
_gameOvers = _statSO._gameOvers
};
// Serialize the StatsWrapper object to JSON
string statsData = JsonUtility.ToJson(savingStats);
// Specify the file path to save the stats
string filePath = Application.persistentDataPath + "/stats.json";
// Save the JSON string to the file
File.WriteAllText(filePath, statsData);
Debug.Log($"Stats data saved to {filePath}");
}
public void LoadStats()
{
string filePath = Application.persistentDataPath + "/stats.json";
if (File.Exists(filePath))
{
string statsData = File.ReadAllText(filePath);
StatsWrapper loadedStats = JsonUtility.FromJson(statsData);
// Apply the loaded data back to the StatsSO instance
_statSO._enemiesKilled = loadedStats._enemiesKilled;
_statSO._clustersMined = loadedStats._clustersMined;
_statSO._oreCollected = loadedStats._oreCollected;
_statSO._weaponsPickup = loadedStats._weaponsPickup;
_statSO._bulletsFired = loadedStats._bulletsFired;
_statSO._timesDodged = loadedStats._timesDodged;
_statSO._empUsed = loadedStats._empUsed;
_statSO._bombsUsed = loadedStats._bombsUsed;
_statSO._homingMissilesUsed = loadedStats._homingMissilesUsed;
_statSO._multishotUsed = loadedStats._multishotUsed;
_statSO._damageTaken = loadedStats._damageTaken;
_statSO._shipsLost = loadedStats._shipsLost;
_statSO._astronautsFound = loadedStats._astronautsFound;
_statSO._healthSavedByShield = loadedStats._healthSavedByShield;
_statSO._bossesDestroyed = loadedStats._bossesDestroyed;
_statSO._oreSpent = loadedStats._oreSpent;
_statSO._spaceStationDamageTaken = loadedStats._spaceStationDamageTaken;
_statSO._spaceStationHealthSavedByShield = loadedStats._spaceStationHealthSavedByShield;
_statSO._shipsRebuilt = loadedStats._shipsRebuilt;
_statSO._spaceStationsLost = loadedStats._spaceStationsLost;
_statSO._totalTimePlayed = loadedStats._totalTimePlayed;
_statSO._achievementsUnlocked = loadedStats._achievementsUnlocked;
_statSO._droneKills = loadedStats._droneKills;
_statSO._spaceStationKills = loadedStats._spaceStationKills;
_statSO._spaceStationHealedPlayer = loadedStats._spaceStationHealedPlayer;
_statSO._homingMissileKills = loadedStats._homingMissileKills;
_statSO._bombKills = loadedStats._bombKills;
_statSO._empDisables = loadedStats._empDisables;
_statSO._ingotsCreated = loadedStats._ingotsCreated;
_statSO._oreOne = loadedStats._oreOne;
_statSO._oreTwo = loadedStats._oreTwo;
_statSO._oreThree = loadedStats._oreThree;
_statSO._oreFour = loadedStats._oreFour;
_statSO._distanceTravelled = loadedStats._distanceTravelled;
_statSO._gameOvers = loadedStats._gameOvers;
Debug.Log("Stats data loaded successfully");
}
else
{
Debug.LogWarning("No stats data found!");
}
}
#endregion
#region Save Tutorials
public void SaveTutorialsToJson()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each achievement in the list and populate the saving data
foreach (var tut in _notificationList)
{
SavingTutorials savingData = new SavingTutorials
{
//achievementName = ach.achievementInfo.name,
_tutName = tut._tutorialTitle,
_hasSeenTutorial = tut._hasSeenTut
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string achData = JsonUtility.ToJson(new TutorialWrapper {_savingTutorials = savingDataList });
string filePath = Application.persistentDataPath + "/tutorials.json";
// Save the serialized data to a file
File.WriteAllText(filePath, achData);
Debug.Log($"Data saved to {filePath}");
}
public void LoadTutorialsToJson()
{
string filePath = Application.persistentDataPath + "/tutorials.json";
// Check if the file exists before trying to load it
if (File.Exists(filePath))
{
string achData = File.ReadAllText(filePath);
// Deserialize the JSON into a TutorialWrapper
TutorialWrapper loadedDataWrapper = JsonUtility.FromJson(achData);
List loadedDataList = loadedDataWrapper._savingTutorials;
foreach(SavingTutorials tutData in loadedDataList)
{
TutorialNotification tutorial = _notificationList.Find(s => s._tutorialTitle == tutData._tutName);
if (tutorial != null)
{
tutorial._hasSeenTut = tutData._hasSeenTutorial;
}
}
}
else
{
Debug.LogWarning("No save file found!");
}
}
#endregion
#region Save Player Settings
public void SavePlayerSettings()
{
Playersettings savingSettings = new Playersettings
{
_fullscreen = _playerSettings._fullscreen,
_vSync = _playerSettings._vSync,
_bloom = _playerSettings._bloom,
_antialiasing = _playerSettings._antialiasing,
_showTutorials = _playerSettings._showTutorials,
_masterVolume = _playerSettings._masterVolume,
_musicValue = _playerSettings._musicValue,
_sfxValue = _playerSettings._sfxValue,
};
// Serialize the StatsWrapper object to JSON
string settingsData = JsonUtility.ToJson(savingSettings);
// Specify the file path to save the stats
string filePath = Application.persistentDataPath + "/playersettings.json";
// Save the JSON string to the file
File.WriteAllText(filePath, settingsData);
Debug.Log($"settings data saved to {filePath}");
}
public void LoadPlayerSettings()
{
string filePath = Application.persistentDataPath + "/playersettings.json";
if (File.Exists(filePath))
{
string settingsData = File.ReadAllText(filePath);
Playersettings loadedStats = JsonUtility.FromJson(settingsData);
// Apply the loaded data back to the StatsSO instance
_playerSettings._fullscreen = loadedStats._fullscreen;
_playerSettings._vSync = loadedStats._vSync;
_playerSettings._bloom = loadedStats._bloom;
_playerSettings._antialiasing = loadedStats._antialiasing;
_playerSettings._showTutorials = loadedStats._showTutorials;
_playerSettings._masterVolume = loadedStats._masterVolume;
_playerSettings._musicValue = loadedStats._musicValue;
_playerSettings._sfxValue = loadedStats._sfxValue;
Debug.Log("settings data loaded successfully");
}
else
{
Debug.LogWarning("No stats data found!");
}
}
#endregion
#region Saving SS Weapons
public void SaveSSWeaponsToJson()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each achievement in the list and populate the saving data
foreach (var wep in _weaponsList)
{
SavingSSWeapons savingData = new SavingSSWeapons
{
//achievementName = ach.achievementInfo.name,
_isUnlocked = wep._isUnlocked
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string achData = JsonUtility.ToJson(new SSWeaponsWrapper { _weapons = savingDataList });
string filePath = Application.persistentDataPath + "/ssweapons.json";
// Save the serialized data to a file
File.WriteAllText(filePath, achData);
Debug.Log($"Data saved to {filePath}");
}
public void LoadSSWeaponsFromJson()
{
string filePath = Application.persistentDataPath + "/ssweapons.json";
// Check if the file exists before trying to load it
if (File.Exists(filePath))
{
string ssweaponsData = File.ReadAllText(filePath);
// Deserialize the JSON into a TutorialWrapper
SSWeaponsWrapper loadedDataWrapper = JsonUtility.FromJson(ssweaponsData);
List loadedDataList = loadedDataWrapper._weapons;
// Apply the loaded data back to the corresponding tutorials in the list
for (int i = 0; i < _weaponsList.Count && i < loadedDataList.Count; i++)
{
SavingSSWeapons data = loadedDataList[i];
_weaponsList[i]._isUnlocked = data._isUnlocked;
}
}
else
{
Debug.LogWarning("No save file found!");
}
}
#endregion
#region Saving Astros
public void SaveAstronautsToJson()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each achievement in the list and populate the saving data
foreach (var ast in _astronautList)
{
SavingAstronauts savingData = new SavingAstronauts
{
_astronautName = ast._astronautName,
_alreadyCollected = ast._alreadyCollected,
_uniqueID = ast.ID,
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string astroData = JsonUtility.ToJson(new AstronautWrapper { _astronauts = savingDataList });
string filePath = Application.persistentDataPath + "/astronauts.json";
// Save the serialized data to a file
File.WriteAllText(filePath, astroData);
Debug.Log($"Data saved to {filePath}");
}
public void LoadAstronautsFromJson()
{
string filePath = Application.persistentDataPath + "/astronauts.json";
if (File.Exists(filePath))
{
string astData = File.ReadAllText(filePath);
//Debug.Log($"Loading JSON from: {filePath}\nContent: {astData}"); // Log the raw JSON
AstronautWrapper loadedDataWrapper = JsonUtility.FromJson(astData);
if (loadedDataWrapper == null || loadedDataWrapper._astronauts == null)
{
Debug.LogError("Failed to deserialize astronauts.json");
return;
}
List loadedDataList = loadedDataWrapper._astronauts;
// Match astronauts by uniqueID
foreach (SavingAstronauts data in loadedDataList)
{
Astronaut astronaut = _astronautList.Find(s => s._astronautName == data._astronautName);
if (astronaut != null)
{
astronaut._alreadyCollected = data._alreadyCollected;
}
else
{
Debug.LogWarning($"No astronaut found with ID {data._uniqueID}");
}
}
Debug.Log("Data loaded and applied to astronaut list");
}
else
{
Debug.LogWarning("No astro save file found!");
foreach (Astronaut a in _astronautList)
{
a._collectedOnRun = false;
a._alreadyCollected = false;
}
}
}
#endregion
#region Saving Resource
public void SaveResourcesToJson()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each achievement in the list and populate the saving data
foreach (var res in _resourcesList)
{
SavingResources savingData = new SavingResources
{
_resourceValue = res._resourceValue
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string resData = JsonUtility.ToJson(new ResourcesWrapper { _resources = savingDataList });
string filePath = Application.persistentDataPath + "/resources.json";
// Save the serialized data to a file
File.WriteAllText(filePath, resData);
Debug.Log($"Data saved to {filePath}");
}
public void LoadResourcesFromJson()
{
string filePath = Application.persistentDataPath + "/resources.json";
// Check if the file exists before trying to load it
if (File.Exists(filePath))
{
string resData = File.ReadAllText(filePath);
// Deserialize the JSON into a Resources Wrapper
ResourcesWrapper loadedDataWrapper = JsonUtility.FromJson(resData);
List loadedDataList = loadedDataWrapper._resources;
Debug.Log(resData);
// Apply the loaded data back to the corresponding tutorials in the list
for (int i = 0; i < _resourcesList.Count && i < loadedDataList.Count; i++)
{
SavingResources data = loadedDataList[i];
_resourcesList[i]._resourceValue = data._resourceValue;
}
Debug.Log("Data loaded and applied to resrouces list");
}
else
{
Debug.LogWarning("No save file found!");
}
}
#endregion
#region Saving Boss Astro Skills
public void SaveBossAstroSkillUnlocks()
{
// Create a list to store the serialized data
List savingDataList = new List();
// Loop through each achievement in the list and populate the saving data
foreach (var res in _astroSkillList)
{
SavingBossAstroSkills savingData = new SavingBossAstroSkills
{
_name = res._astroName,
_isUnlocked = res._skillUnlocked
};
savingDataList.Add(savingData);
}
// Serialize the list to JSON
string resData = JsonUtility.ToJson(new BossAstroSkillWrapper { _bossAstroSkills = savingDataList });
string filePath = Application.persistentDataPath + "/bossastroskills.json";
// Save the serialized data to a file
File.WriteAllText(filePath, resData);
Debug.Log($"Data saved to {filePath}");
}
public void LoadBossAstroSkillsUnlocked()
{
string filePath = Application.persistentDataPath + "/bossastroskills.json";
if (File.Exists(filePath))
{
string skillData = File.ReadAllText(filePath);
BossAstroSkillWrapper loadedDataWrapper = JsonUtility.FromJson(skillData);
foreach (SavingBossAstroSkills data in loadedDataWrapper._bossAstroSkills)
{
// Find the matching ScriptableObject by unique ID
AstroSkill skill = _astroSkillList.Find(s => s._astroName == data._name);
Debug.Log(skill);
if (skill != null)
{
skill._skillUnlocked = data._isUnlocked;
}
else
{
Debug.LogWarning($"Skill with UniqueID {data._name} not found in the game. It may have been removed.");
}
}
}
else
{
Debug.LogWarning("No save file found!");
}
}
#endregion
}
// The class used to store and load the specific data
[System.Serializable]
public class SavingPlayerSkills
{
public string _skillName;
public string _uniqueID;
public int _currentLevel;
public float _startingValue;
public int _skillType;
public bool _isUnlocked;
public float _currentValue;
}
[System.Serializable]
public class SavingAchievements
{
public string _achievementName;
public bool _isUnlocked;
public string _uniqueID;
}
[System.Serializable]
public class SavingAstronauts
{
public string _astronautName;
public bool _alreadyCollected;
public int _uniqueID;
}
[System.Serializable]
public class SavingTutorials
{
public string _tutName;
public bool _hasSeenTutorial;
}
[System.Serializable]
public class SavingResources
{
public float _resourceValue;
}
[System.Serializable]
public class SavingSSWeapons
{
public bool _isUnlocked;
}
[System.Serializable]
public class SavingBossAstroSkills
{
public string _name;
public bool _isUnlocked;
}
// Wrapper class to serialize a list of SavingStuff
[System.Serializable]
public class SavingWrapper
{
public List _skills;
}
[System.Serializable]
public class AchievementsWrapper
{
public List _achievements;
}
[System.Serializable]
public class AstronautWrapper
{
public List _astronauts;
}
[System.Serializable]
public class SSWeaponsWrapper
{
public List _weapons;
}
[System.Serializable]
public class ResourcesWrapper
{
public List _resources;
}
[System.Serializable]
public class BossAstroSkillWrapper
{
public List _bossAstroSkills;
}
[System.Serializable]
public class StatsWrapper
{
public int _enemiesKilled;
public int _clustersMined;
public int _oreCollected;
public int _weaponsPickup;
public int _bulletsFired;
public int _timesDodged;
public int _empUsed;
public int _bombsUsed;
public int _homingMissilesUsed;
public int _multishotUsed;
public int _damageTaken;
public int _shipsLost;
public int _astronautsFound;
public int _healthSavedByShield;
public int _bossesDestroyed;
public int _oreSpent;
public int _spaceStationDamageTaken;
public int _spaceStationHealthSavedByShield;
public int _shipsRebuilt;
public int _spaceStationsLost;
public int _totalTimePlayed;
public int _achievementsUnlocked;
public int _droneKills;
public int _spaceStationKills;
public int _spaceStationHealedPlayer;
public int _homingMissileKills;
public int _bombKills;
public int _empDisables;
public int _ingotsCreated;
public int _oreOne;
public int _oreTwo;
public int _oreThree;
public int _oreFour;
public int _distanceTravelled;
public int _gameOvers;
}
[System.Serializable]
public class TutorialWrapper
{
public List _savingTutorials;
}
[System.Serializable]
public class Playersettings
{
public bool _fullscreen;
public bool _vSync;
public bool _bloom;
public bool _antialiasing;
public bool _showTutorials;
public float _masterVolume;
public float _musicValue;
public float _sfxValue;
public enum Difficulty
{
Easy, Normal, Hard
}
}
}
This handles the timing, max number, and type of enemies and bosses that spawn in every level.
In my current project "Untitled" I reworked the wave system in Age of Zombies to make it more designer-friendly so that it can be updated quicker and easier in the Unity editor.
The editor in the original was a bit of a mess and it had a mountain of dropdowns to add weapons to spawn, zombies, how many, the timings etc.
But with the rework of my project, I removed this fiddly editor mess and now you just create two things.
- Create a wave of enemies - This will be the min/max enemies, and the types of enemy being spawned. This is a scriptable object.
- Wave set - This is a list of the waves. This way you can drop in as many waves to the set and all you drag into the wave system is the wave sets.


Making those changes allows for faster enemy wave iterations and testing of new enemies.

The system then works its way through the waves, then the wave set.
using UnityEngine;
using System.Collections;
using TMPro;
using System.Collections.Generic;
using UnityEngine.UI;
using System;
using Random = UnityEngine.Random;
namespace DangryGames
{
[System.Serializable]
public class EnemyToSpawn
{
public EnemyType _enemyTypesToSpawn;
}
public class WaveSystenRevamp : MonoSingleton
{
[SerializeField] private AudioClip _fightingMusic;
[SerializeField] private AudioClip _backgroundMusic;
public WaveSet[] _waveSets;
private WaveSet _currentWaveSet;
private int _currentWaveSetNumber;
private int _currentWaveSetWavesCount;
private int _currentWaveNumber;
private int _waveSetsCount;
public GameObject _spaceStation;
public PermaSkills _enemyTimerSkill;
private float _warningTime;
private float _runTime;
[Tooltip("Text object to warn the player that the enemy is approaching")]
public GameObject _enemyWarningObject;
public TextMeshProUGUI _timerOnScreen;
public string _spawningAtPlayerString;
public string _spawningAtSpacestationString;
Vector3 _playerPosition;
[SerializeField] private AudioSource _audioSource;
public Image _enemyTimerFillBar;
[SerializeField] private bool _canFillEnemyTimer = true;
public bool _CanFillEnemyTimer
{
get { return _canFillEnemyTimer; }
set { _canFillEnemyTimer = value; }
}
float _timerMax;
public static Action CombatMode;
public static Action AllEnemiesSpawned;
[SerializeField] private BoolVariable _autoSwitchFireMode;
private float _timeBetweenEnemiesSpawning = 0.2f;
private float _resetTimeBetweenEnemiesSpawning = 0.2f;
private float _enemiesLeftToSpawn;
public float _EnemiesLeftToSpawn
{
get { return _enemiesLeftToSpawn; }
}
private int _rand;
private int _randEnemy;
[Header("Wave sfx & visuals")]
[SerializeField] Animator _hudAnimatorWaveDefeated;
[SerializeField] private AudioClip _enemyWaveStartedSound;
[SerializeField] private AudioClip _enemyWaveDefeatedSound;
[SerializeField] float _soundVolume = 0.5f;
[Header("Tutorials")]
[SerializeField] private TutorialNotification _secondaryTut;
[SerializeField] private TutorialNotification _enemyTimerTut;
private bool _gameOver = false;
private void OnEnable()
{
Tracker.AllEnemiesDead += AllEnemiesAreDead;
GameManager.UpdateStatsAfterAstro += UpdatePermaSkills;
GameManager.GameOver += GameOver;
}
private void OnDisable()
{
Tracker.AllEnemiesDead -= AllEnemiesAreDead;
GameManager.UpdateStatsAfterAstro -= UpdatePermaSkills;
GameManager.GameOver -= GameOver;
}
private void GameOver(bool value)
{
_gameOver = value;
}
private void UpdatePermaSkills(bool value)
{
_timerMax = (_enemyTimerSkill._currentvalue + _enemyTimerSkill._permSkillValue) + ((_enemyTimerSkill._currentvalue + _enemyTimerSkill._permSkillValue) * _enemyTimerSkill._modifyValue);
}
public void BossSpawned()
{
DamageAllEnemies();
_canFillEnemyTimer = false; //Used to pause the wavesystem
}
public void DamageAllEnemies()
{
foreach (EnemyBase em in Tracker.Instance._AliveEnemies)
{
//Destroy all enemies once the boss has spawned
float randomValue = UnityEngine.Random.value;
StartCoroutine(DamageShit(em, randomValue)); //Maybe this should be an invoke and not coroutine??
}
}
private IEnumerator DamageShit(EnemyBase enemy, float time)
{
yield return new WaitForSeconds(time);
if (enemy != null) enemy.Damage(5, IDamagable.KilledBy.None);
}
private void AllEnemiesAreDead()
{
if (!GameManager.Instance._BossIsSpawned)
{
ResetTimerAndBar();
if (!GameManager.Instance._bossDefeatedText.gameObject.activeInHierarchy && !_gameOver)
{
_hudAnimatorWaveDefeated.SetTrigger("waveDefeated");
AudioManager.Instance.PlaySFXOneShot(_enemyWaveDefeatedSound, _soundVolume, 1);
AudioManager.Instance.FadeInMusic(_backgroundMusic, 0.5f, 1, 1);
}
}
}
public void BossIsDead()
{
Invoke("ResetTimerAndBar", 2);
}
private void ResetTimerAndBar()
{
_runTime = 0;
_enemyTimerFillBar.fillAmount = 0;
_canFillEnemyTimer = true;
}
private void Start()
{
_waveSetsCount = _waveSets.Length;
_currentWaveSet = _waveSets[0];
_currentWaveSetNumber = 0;
_currentWaveNumber = 0;
_currentWaveSetWavesCount = _waveSets[_currentWaveSetNumber]._enemyWaves.Count;
_timerMax = _enemyTimerSkill._currentvalue;
_warningTime = _timerMax / 2;
ResetTimerAndBar();
_wantToDestroyOnLoad = true;
}
private void Update()
{
if (!GameManager.Instance._pauseManager._IsPaused && !_gameOver)
{
if (_canFillEnemyTimer)
{
_runTime += Time.deltaTime;
if (_runTime >= _warningTime)
{
TutorialPopup.Instance.Init(_enemyTimerTut, true);
}
if (_runTime < _timerMax && Tracker.Instance._AliveEnemies.Count == 0)
{
float fillAmount = _runTime / _timerMax;
_enemyTimerFillBar.fillAmount = Mathf.Clamp01(fillAmount);
//TODO: Need to give warning when 20/30 seconds before next wave
}
else if (_runTime > _timerMax)
{
//_audioSource.PlayOneShot(enemyWaveStartedSound, soundVolume);
AudioManager.Instance.PlaySFXOneShot(_enemyWaveStartedSound, _soundVolume, 1);
_rand = Random.Range(_currentWaveSet._enemyWaves[_currentWaveNumber]._minNumberEnemies, _currentWaveSet._enemyWaves[_currentWaveNumber]._maxNumberEnemies);
_enemiesLeftToSpawn = _rand;
Tracker.Instance._DoNotTouchEnemyCount = (int)_enemiesLeftToSpawn;
_canFillEnemyTimer = false;
Player player = GameManager.Instance.ReturnPlayerTransform().GetComponent();
}
}
if (!_canFillEnemyTimer && !GameManager.Instance._BossIsSpawned)
{
_timeBetweenEnemiesSpawning -= Time.deltaTime;
if (_timeBetweenEnemiesSpawning <= 0 && _enemiesLeftToSpawn > 0)
{
RunWave();
}
}
}
}
private void RunWave()
{
TutorialPopup.Instance.Init(_secondaryTut, true);
_randEnemy = Random.Range(0, _currentWaveSet._enemyWaves[_currentWaveNumber]._enemyTypes.Count);
if (_enemiesLeftToSpawn > 0)
{
AllEnemiesSpawned?.Invoke(false);
EnemyType et = _currentWaveSet._enemyWaves[_currentWaveNumber]._enemyTypes[_randEnemy];
SpawnEnemy(et);
_enemiesLeftToSpawn--;
_timeBetweenEnemiesSpawning = _resetTimeBetweenEnemiesSpawning;
}
if (_enemiesLeftToSpawn <= 0)
{
AllEnemiesSpawned?.Invoke(true);
//Increase the wave number if it's not at the end of the wave count
if (_currentWaveNumber != _waveSets[_currentWaveSetNumber]._enemyWaves.Count)
{
_currentWaveNumber++;
}
if (_currentWaveNumber >= _waveSets[_currentWaveSetNumber]._enemyWaves.Count)
{
if (_currentWaveSetNumber < _waveSetsCount - 1)
{
if (_currentWaveSet._nextSet == 1)
{
_currentWaveSetNumber++;
_currentWaveSet = _waveSets[_currentWaveSetNumber];
_currentWaveSetWavesCount = _waveSets[_currentWaveSetNumber]._enemyWaves.Count;
_currentWaveNumber = 0;
}
if (_currentWaveSet._nextSet == 0)
{
_currentWaveSetNumber = 0;
_currentWaveSet = _waveSets[0];
_currentWaveNumber = 0;
_currentWaveSetWavesCount = _waveSets[_currentWaveSetNumber]._enemyWaves.Count;
}
}
}
}
//run music for enemies
AudioManager.Instance.FadeInMusic(_fightingMusic, 1, 1, 1);
}
private void SpawnEnemy(EnemyType enemyType)
{
GameObject go = Instantiate(enemyType._enemyPrefab, Vector3.zero, Quaternion.identity);
go.SetActive(true);
EnemyBase enemyBase = go.GetComponent();
Tracker.Instance.AddToList(go, Tracker.TrackerType.Enemies);
int index = Tracker.Instance._AliveEnemies.IndexOf(enemyBase);
enemyBase._ListIndex = index;
enemyBase._ID = index;
enemyBase.InitialseEnemy(enemyType);
if (_currentWaveSet._enemyWaves[_currentWaveNumber]._spawnAtPlayer) //Spawn at player
{
if (GameManager.Instance.IsAlive)
{
_playerPosition = GameManager.Instance.ReturnPlayerTransform().position;
}
// Generate random offsets for X and Y ensuring they are at least 10 away but no more than 15
float randomX = Random.Range(0, 2) == 0 ? Random.Range(-10f, -8f) : Random.Range(8f, 10f);
float randomY = Random.Range(0, 2) == 0 ? Random.Range(-10f, -8f) : Random.Range(8f, 10f);
// Create the random position
Vector3 randPos = new Vector3(_playerPosition.x + randomX, _playerPosition.y + randomY, _playerPosition.z);
go.transform.position = randPos;
}
else //Spawn at space station
{
Vector3 ssPosition = GameManager.Instance.ReturnSpaceStationPos().position;
// Generate random offsets for X and Y within the range of -10 to 10
float randomX = Random.Range(-10f, 10f);
float randomY = Random.Range(-10f, 10f);
Vector3 randPos = new Vector3(ssPosition.x + randomX, ssPosition.y + randomY, ssPosition.z);
go.transform.position = randPos;
}
}
}
}
This tool helps me to create scripts & folders I need in a quick manner. I created this tool because it was taking forever to create scriptable objects and to adjust them every time just to use them.
The tool allows me to create the following:
- Mono classes with namespaces
- Regular C# classes with namespaces &/or constructors
- Derived classes with search or drag and drop
- Interfaces
- Scriptables objects - Filename and Menu name
using UnityEngine;
using UnityEngine.UIElements;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.UIElements;
using System.IO;
namespace DangryGames.Generator
{
public class GeneratorEditor : EditorWindow
{
VisualElement _container;
TextField _txtScriptName;
TextField _pathName;
TextField _folderName;
TextField _soFilename;
TextField _soMenuname;
Toggle _toggleNewFolder;
TextField _nameSpace;
Toggle _toggleNameSpace;
ObjectField _baseClassName;
Toggle _baseClassConstructor;
bool _needConstructor;
Button _classBtn;
Button _monoBtn;
Button _derivedBtn;
Button _interfaceBtn;
Button _soBtn;
Label _warningLbl;
Color showColour = Color.grey;
Color unselectColour = Color.black;
VisualElement _soContainer;
VisualElement _derivedContainer;
Button _testBtn;
string _currentSelectedBtn = "";
private Button[] _buttonArray = new Button[5];
public const string _assetPath = "Assets/DangryGames/Generator/Editor/EditorWindow/";
[MenuItem("DangryGames/Script Generator")]
public static void ShowWindow()
{
//Opens an editor window with a name of the class
GeneratorEditor window = GetWindow();
//Changes the name of the editor window to the string
window.titleContent = new GUIContent("Script Generator");
//sets the min size of the editor window
window.minSize = new Vector2(600, 600);
}
private void CreateGUI()
{
_container = rootVisualElement;
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(_assetPath + "GeneratorEditor.uxml");
_container.Add(visualTree.Instantiate());
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(_assetPath + "GeneratorEditor.uss");
_container.styleSheets.Add(styleSheet);
//Gets the first element of type TextField called txtScriptName
_txtScriptName = _container.Q<TextField>("txtScriptName");
//Gets the first element of type TextField called pathName
_pathName = _container.Q<TextField>("pathName");
_folderName = _container.Q<TextField>("folderName");
if (_folderName == null)
{
Debug.Log("Failed to get Folder");
}
_toggleNameSpace = _container.Q<Toggle>("toggleNameSpace");
_toggleNameSpace.RegisterValueChangedCallback(ShowNamespaceField);
_nameSpace = _container.Q<TextField>("nameSpace");
_testBtn = _container.Q<Button>("testBtn");
_testBtn.clicked += CreateTestFolder;
_monoBtn = _container.Q<Button>("monoBtn");
_monoBtn.clicked += _baseBtn_clicked;
_classBtn = _container.Q<Button>("classBtn");
_classBtn.clicked += OnScriptButtonClick;
_derivedBtn = _container.Q<Button>("derivedBtn");
_derivedBtn.clicked += _derivedBtn_clicked;
_interfaceBtn = _container.Q<Button>("interfaceBtn");
_interfaceBtn.clicked += _interfaceBtn_clicked;
_soBtn = _container.Q<Button>("soBtn");
_soBtn.clicked += _soBtn_clicked;
_buttonArray[0] = _monoBtn;
_buttonArray[1] = _classBtn;
_buttonArray[2] = _derivedBtn;
_buttonArray[3] = _interfaceBtn;
_buttonArray[4] = _soBtn;
_soContainer = _container.Q("SOContainer");
_soFilename = _container.Q("soFilename");
_soMenuname = _container.Q("soMenuname");
_derivedContainer = _container.Q("DerivedContainer");
_baseClassName = _container.Q("childName");
_baseClassName.objectType = typeof(MonoScript);
_baseClassConstructor = _container.Q("ctor");
_baseClassConstructor.RegisterValueChangedCallback(NeedConstructor);
_warningLbl = _container.Q
This system allows me to control the number of enemies that can attack at any one time. I read that Doom(2016) implemented the token system and I created a simplified version.
The system is currently hard coded to allow 5 enemies to fire at once. But it will be updated later on to take into account the Difficulty setting the player chooses
using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using System;
namespace DangryGames
{
public class TokenManager : MonoSingleton
{
public enum AttackToken
{
Normal,
Special
}
public Dictionary _tokenCounts;
private bool _isAttackInProgress = false;
private void Start()
{
_wantToDestroyOnLoad = true;
_tokenCounts = new Dictionary();
foreach (AttackToken token in Enum.GetValues(typeof(AttackToken)))
{
_tokenCounts[token] = 5;
}
}
public void StartAttack(AttackToken token)
{
_isAttackInProgress = true;
RemoveToken(token);
}
public void RemoveToken(AttackToken token)
{
_tokenCounts[token]--;
}
public void EndAttack(AttackToken token)
{
_isAttackInProgress = false;
ReplenishToken(token);
}
public void ReplenishToken(AttackToken token)
{
_tokenCounts[token]++;
}
public void ResetTokenCountWhenAllEnemiesAreDead()
{
_tokenCounts[AttackToken.Normal] = 5;
}
}
}
Example Enemy script using the Token system. Parts of the script have been removed for ease of reading
using UnityEngine;
using System;
using Unity.Mathematics;
using Random = UnityEngine.Random;
namespace DangryGames
{
public class Enemy : MonoBehaviour, IDamagable
{
private float _accuracy;
private Transform _player;
private Transform _spaceStation;
[SerializeField] private float _speed;
[SerializeField] private float _rotateSpeed;
[SerializeField] private bool _moveTowardsPlayer;
[SerializeField] private bool _targetPlayer;
[SerializeField] private GameObject _bulletObject;
[SerializeField] private Transform _firePos;
[SerializeField] private ParticleSystem _empParticles;
private float _empDisableTime = 5f;
private float _empEnabledRotateSpeed = 50f;
private float _fireRate;
private float _health;
public float _Health
{
get { return _health; }
set { _health = value; }
}
private float _cooldownTimer;
private float _cooldownReset;
private float _endAttackTimer = 2;
private bool _endAttack = false;
private SpriteRenderer _sprite;
protected int _listIndex;
public int _ListIndex
{
get { return _listIndex; }
set { _listIndex = value; }
}
protected int _id;
public int _ID
{
get { return _id; }
set { _id = value; }
}
[SerializeField] private EnemyType _enemyType;
private int _normalTokens = 1;
private int _specialTokens = 1;
private TokenManager.AttackToken _attackToken;
private bool _hasAttackToken = false;
private void Update()
{
if (!_isEmpEnabled)
{
if (!GameManager.Instance._pauseManager._IsPaused)
{
_cooldownTimer -= Time.deltaTime;
if (_cooldownTimer <= 0)
{
if (_targetPlayer && distanceFromPlayer < 20)
{
FireWeapon(_attackToken);
}
else if(!_targetPlayer && distanceFromSS < 20)
{
FireWeapon(_attackToken);
}
}
}
}
if (_endAttack)
{
_endAttackTimer -= Time.deltaTime;
}
if(_endAttackTimer <= 0)
{
_endAttack = false;
EndAttack();
_endAttackTimer = 2;
}
}
private bool IsTokenAvailable(TokenManager.AttackToken token)
{
switch (token)
{
case TokenManager.AttackToken.Normal: return _normalTokens > 0 && TokenManager.Instance._tokenCounts[token] > 0;
case TokenManager.AttackToken.Special: return _specialTokens > 0 && TokenManager.Instance._tokenCounts[token] > 0;
default: return false;
}
}
private void FireWeapon(TokenManager.AttackToken token)
{
if (!IsTokenAvailable(token)) return;
switch (token)
{
case TokenManager.AttackToken.Normal:
_normalTokens--;
_hasAttackToken = true;
break;
case TokenManager.AttackToken.Special:
_specialTokens--;
_hasAttackToken = true;
break;
}
//something something something shoot player or space station
if (_targetPlayer)
{
if (!GameManager.Instance.IsAlive)
{
return;
}
//Tell the token manager to remove 1 token
TokenManager.Instance.StartAttack(token);
Vector3 targetPos = GameManager.Instance.ReturnPlayerTransform().position;
// Calculate the direction from the spawn position to the target position
Vector2 direction = (targetPos - _firePos.position).normalized;
direction += EnemyAimError(_accuracy);
// Calculate the angle in degrees for the rotation
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90;
// Create a rotation using the angle around the Z-axis
Quaternion rotation = Quaternion.Euler(0f, 0f, angle);
// Spawn the bullet with the calculated rotation
GameObject bullet = Instantiate(_bulletObject, _firePos.position, Quaternion.identity);
EnemyLaser el = bullet.GetComponent();
el.InitLaser(false);
el.PlaySound(true);
bullet.transform.rotation = rotation;
_cooldownTimer = _cooldownReset;
//Debug.Log("Firing at player??");
}
else if(!_targetPlayer)
{
//Tell the token manager to remove 1 token
TokenManager.Instance.StartAttack(token);
Vector3 targetPos = GameManager.Instance.ReturnSpaceStationPos().position;
// Calculate the direction from the spawn position to the target position
Vector2 direction = (targetPos - _firePos.position).normalized;
direction += EnemyAimError(_accuracy);
// Calculate the angle in degrees for the rotation
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90;
// Create a rotation using the angle around the Z-axis
Quaternion rotation = Quaternion.Euler(0f, 0f, angle);
// Spawn the bullet with the calculated rotation
GameObject bullet = Instantiate(_bulletObject, _firePos.position, Quaternion.identity);
bullet.transform.rotation = rotation;
_cooldownTimer = _cooldownReset;
}
//Tell token manager to put back 1 token
_endAttack = true;
}
private void EndAttack()
{
TokenManager.Instance.EndAttack(_attackToken);
if (_attackToken == TokenManager.AttackToken.Normal) _normalTokens = 1;
else if (_attackToken == TokenManager.AttackToken.Special) _specialTokens = 1;
_hasAttackToken = false;
}
public void Damage(float amount, IDamagable.KilledBy killedBy = IDamagable.KilledBy.None)
{
_health -= amount;
if (_health <= 0)
{
if (Tracker.Instance._PickupsActive.Count < 3)
{
//Spawn random weapon if Chance skill is higher than rand
float rand = Random.Range(0f, 1f);
float chance = 0.1f; //Set to 1 for testing// TODO: create and link chance skill
if (rand <= chance)
{
GameObject randomPickup = ObjectPooling.ObjectPool.Instance.GetPooledObject("RandomPickup");
randomPickup.transform.position = transform.position;
randomPickup.SetActive(true);
}
}
//End chance of spawning secondary
//Remove from global enemy count list in the future
GameObject boom = ObjectPooling.ObjectPool.Instance.GetPooledObject("EnemyEx");
boom.SetActive(true);
boom.transform.position = transform.position;
boom.transform.rotation = transform.rotation;
Tracker.Instance.RemoveFromList(gameObject, Tracker.TrackerType.Enemies);
Stats.Instance.UpdateStat(1, StatsSO.Stats.EnemiesKilled);
switch (killedBy)
{
case IDamagable.KilledBy.Homing: Stats.Instance.UpdateStat(1, StatsSO.Stats.HomingMissileKills);
break;
case IDamagable.KilledBy.Bomb:Stats.Instance.UpdateStat(1, StatsSO.Stats.BombKills);
break;
}
if (_hasAttackToken) EndAttack();
gameObject.SetActive(false);
Destroy(gameObject);
}
}
}
}
Other systems include:
- Tracker: This helps to track enemies, asteroid clusters, offscreen markers and more
- Stats System: Allows me to keep track of the player stats for each run and overall stats. Also pings the achievement system if a stat is connected to an achievement.
- Upgrade System: Allows the player to upgrade their ship and skills
- Weapon System: Controls the secondary weapons the player can use, by managing individual weapon cooldowns and weapon swapping