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
}