using UnityEngine; using Unity.Entities; using Unity.Collections; using UnityEngine.InputSystem; using WaveSpawner; using WaveSpawner.Entities; /// /// The wave spawner system, which uses wave spawner data /// grabbed from SpawnerDataSingleton. /// public partial struct WaveSpawnerSystem : ISystem { private float timer; private int waveIndex; private int waveRuleIndex; private float lastWaveSpawnTime; private bool hasFinished; private EntityQuery enemyQuery; private EntityCommandBuffer ecb; public void OnCreate(ref SystemState state) { //Run only when we have data state.RequireForUpdate(); //Grab query for counting enemies later enemyQuery = GetEntityCountQuery(ref state); //Setup default state timer = 0.0f; waveIndex = 0; waveRuleIndex = 0; lastWaveSpawnTime = 0.0f; hasFinished = false; } /// /// Checks whether the next wave rule should be spawned. This returns /// true if the spawning conditions are met. /// /// /// /// /// private bool ShouldSpawnNextWaveRule(Wave waveData, int numEnemies, float timeSinceSpawn) { //Are we outside the number of rules? If so, exit early if (CompletedWaveRules(waveData)) return false; var currentWaveRule = waveData.waveRules[waveRuleIndex]; bool popCapExceeded = (numEnemies <= currentWaveRule.triggerPopulationCap); bool timerExceeded = (timer > currentWaveRule.triggerTimeSinceLastSpawn); bool popCapSpecified = currentWaveRule.triggerPopulationCap >= 0; bool timerSpecified = currentWaveRule.triggerTimeSinceLastSpawn >= 0; //Neither specified? if(!popCapSpecified && !timerSpecified) { Debug.Log("No spawning criteria, spawning immediately"); return true; } //Both specified? if(popCapSpecified && timerSpecified && (popCapExceeded || timerExceeded)) { Debug.Log("Spawning, either pop cap or timer reached"); Debug.Log($"- #enemies ({numEnemies}) < pop cap ({currentWaveRule.triggerPopulationCap})"); Debug.Log($"- timer {timer} > {currentWaveRule.triggerTimeSinceLastSpawn}"); return true; } //Pop cap specified? if(popCapSpecified && popCapExceeded) { Debug.Log($"Spawning, #enemies ({numEnemies}) < pop cap ({currentWaveRule.triggerPopulationCap})"); return true; } //Timer specified? if(timerSpecified && timerExceeded) { Debug.Log($"Spawning, timer {timer} > {currentWaveRule.triggerTimeSinceLastSpawn}"); return true; } //Otherwise we shouldnt spawn anything return false; } /// /// Spawns entities according to a wave rule. /// /// /// private void SpawnWaveRule(in Wave wave, in SpawnerDataSingleton waveSpawnerData) { //Get clusters & units for passing in var clusters = waveSpawnerData.clusters; var units = waveSpawnerData.units; var rule = wave.waveRules[waveRuleIndex]; if (rule.type == WaveRuleType.Unit) SpawnUnit(units[rule.clusterOrTypeId]); else if (rule.type == WaveRuleType.Cluster) SpawnCluster(clusters[rule.clusterOrTypeId], units); } /// /// Spawns a specified cluster. /// /// /// private void SpawnCluster(Cluster cluster, in NativeHashMap units) { foreach(var clusterRule in cluster.clusterRules) { //TODO: Spawn the unit according to the rule, with ECS var unit = units[clusterRule.unitId]; var amount = clusterRule.amount; Debug.Log($"Wave {waveIndex}, spawn {unit.name} ({unit.id}), {amount} times for cluster {cluster.name}"); } } /// /// Spawns a specified unit. /// /// private void SpawnUnit(Unit unit) { //TODO: Spawn the unit with ECS Debug.Log($"Wave {waveIndex}, spawn {unit.name} ({unit.id})"); } /// /// Clears all enemies in the world. Used for debugging /// the population threshold condition. /// /// private void ClearAllEnemies(ref SystemState state) { foreach (var (enemy, entity) in SystemAPI.Query>().WithEntityAccess()) ecb.DestroyEntity(entity); } /// /// Updates the wave spawner. Contains the main logic of this system. /// /// /// private void UpdateWaveSpawner(ref SystemState state, in SpawnerDataSingleton waveSpawnerData) { //Relevant data for use in this function var waves = waveSpawnerData.waves.GetKeyValueArrays(Allocator.Temp); int numEnemies = GetEntityCount(ref state); float timeSinceSpawn = GetTimeSinceLastSpawn(ref state); var currentWaveData = waves.Values[waves.Keys[waveIndex]]; //For debug purposes if (Keyboard.current.spaceKey.wasPressedThisFrame) ClearAllEnemies(ref state); //Check if wave should be spawned.. if (ShouldSpawnNextWaveRule(currentWaveData, numEnemies, timeSinceSpawn)) { //If it should, then spawn the wave SpawnWaveRule(currentWaveData, waveSpawnerData); //Increase rule index, reset timer, etc. waveRuleIndex++; timer = 0.0f; timeSinceSpawn = 0.0f; Debug.Log($"Wave {waveIndex}, rule start: {waveRuleIndex}"); //Have we completed all the rules in this wave? If so: if (CompletedWaveRules(currentWaveData)) { waveRuleIndex = 0; waveIndex++; Debug.Log($"Wave {waveIndex - 1} end; start: Wave {waveIndex}"); } //Have we completed all the waves specified? If so: if (CompletedWaves(waves)) { waveIndex = 0; waveRuleIndex = 0; hasFinished = true; Debug.Log($"All waves finished"); return; } } } public void OnUpdate(ref SystemState state) { //Finished all waves? Skip logic if(hasFinished) return; //Grab ECB & wave spawner data this.ecb = GetECB(ref state); var waveSpawner = SystemAPI.GetSingleton(); //Update with this wave spawner UpdateWaveSpawner(ref state, waveSpawner); //Increase timer timer += SystemAPI.Time.DeltaTime; //And dispose of ECB this.ecb.Playback(state.EntityManager); this.ecb.Dispose(); } #region Helper functions private EntityQuery GetEntityCountQuery(ref SystemState state) where T : unmanaged, IComponentData => state.GetEntityQuery(typeof(T)); private int GetEntityCount(ref SystemState state) => enemyQuery.CalculateEntityCount(); private float GetTimeSinceLastSpawn(ref SystemState state) => (float)SystemAPI.Time.ElapsedTime - lastWaveSpawnTime; private bool CompletedWaveRules(Wave waveData) => waveRuleIndex >= waveData.waveRules.Length; private bool CompletedWaves(in NativeKeyValueArrays waves) => waveIndex >= waves.Length; private EntityCommandBuffer GetECB(ref SystemState state) => new EntityCommandBuffer(Allocator.Temp); #endregion }