Skip to content

Instantly share code, notes, and snippets.

@blewert
Created January 28, 2026 12:39
Show Gist options
  • Select an option

  • Save blewert/bf84322f52681be1aea75775a81245d4 to your computer and use it in GitHub Desktop.

Select an option

Save blewert/bf84322f52681be1aea75775a81245d4 to your computer and use it in GitHub Desktop.
ECS-based wave spawner, data conversion from managed to unmanaged without BlobAssets.
using Unity.Entities;
using UnityEngine;
using WaveSpawner.Entities;
///This namespace contains MonoBehaviour authoring scripts for wave
///spawning data.
///
namespace WaveSpawner
{
#region Units
/// <summary>
/// Represents a unit to be spawned. This is a serializeable
/// class which is used in WaveSpawnerDataAuthoring.
/// </summary>
[System.Serializable]
public class UnitAuthoring
{
public int id;
public string name;
public float damage;
public float speed;
public float health;
}
#endregion
#region Clusters
/// <summary>
/// Represents a cluster to be spawned.
/// </summary>
[System.Serializable]
public class ClusterAuthoring
{
public int id;
public string name;
public ClusterRuleAuthoring[] clusterRules;
}
/// <summary>
/// Represents a cluster rule for a cluster, specifically
/// the unit to spawn, and the amount of to spawn of that
/// unit.
/// </summary>
[System.Serializable]
public class ClusterRuleAuthoring
{
public int unitId;
public int amount;
}
#endregion
#region Waves
/// <summary>
/// Represents a wave of clusters/units to be spawned. Essentially
/// a wrapper around many wave rules.
/// </summary>
[System.Serializable]
public class WaveAuthoring
{
public int id;
public string name;
public WaveRuleAuthoring[] waveRules;
}
/// <summary>
/// A single wave rule -- the cluster or type to spawn,
/// the conditions in which the wave rule should trigger spawn,
/// and the type of thing to spawn.
/// </summary>
[System.Serializable]
public class WaveRuleAuthoring
{
public int clusterOrTypeId;
public int triggerPopulationCap;
public float triggerTimeSinceLastSpawn;
public WaveRuleType type;
}
#endregion
/// <summary>
/// This is a wrapper component which holds a reference to
/// a WaveSpawnerDataAuthoring. This is baked into the ECS world.
///
/// The WaveSpawnerSingletonSystem looks for this type of component,
/// and if found, allocates native memory, copying across the values
/// in this component.
///
/// This is done because it is difficult to allocate native memory
/// at bake time. Instead, we must defer the allocation until the
/// system starts running -- whereupon we can allocate memory.
/// </summary>
public struct SpawnerDataWrapper : IComponentData
{
public UnityObjectRef<WaveSpawnerDataAuthoring> spawnerAuthoring;
public bool debug;
}
/// <summary>
/// Authoring script for wave spawner data. At the moment,
/// the values are editable through the inspector. But you can
/// imagine how straight-forward it would be to set these
/// values after parsing a TomBen wave spawning file.
///
/// TODO: Make it so these arrays are populated after
/// parsing the format, rather than at edit time.
/// </summary>
public class WaveSpawnerDataAuthoring : MonoBehaviour
{
[Header("Wave spawning data")]
public UnitAuthoring[] m_units;
public ClusterAuthoring[] m_clusters;
public WaveAuthoring[] m_waves;
[Header("Options")]
[Tooltip("Whether to print out verbose debug messages")]
public bool m_debug;
/// <summary>
/// Bakes a WaveSpawnerDataAuthoring to a
/// SpawnerDataWrapper, which is read later by
/// a WaveSpawnerSingletonSystem for deferred baking.
/// </summary>
private class WaveSpawnerDataBaker : Baker<WaveSpawnerDataAuthoring>
{
public override void Bake(WaveSpawnerDataAuthoring authoring)
{
AddComponent(GetEntity(TransformUsageFlags.None), new SpawnerDataWrapper
{
spawnerAuthoring = authoring,
debug = authoring.m_debug
});
}
}
}
}
using System;
using Unity.Collections;
using Unity.Entities;
///This namespace contains unmanaged data structures for wave
///spawning data. These are set by the WaveSpawnerSingletonSystem
///and are converted from the managed types found in the
///WaveSpawner namespace (e.g. WaveSpawner.UnitAuthoring).
///
///Note: These are mostly non-component structs, as they are not
/// ECS data. They are simply containers that contain wave
/// spawning data.
namespace WaveSpawner.Entities
{
#region Units
/// <summary>
/// ECS wave spawning data regarding an in-game
/// Unit. Its managed equivalent is WaveSpawner.UnitAuthoring.
/// </summary>
public struct Unit
{
public int id;
public FixedString32Bytes name;
public float damage;
public float speed;
public float health;
}
#endregion
#region Clusters
/// <summary>
/// ECS wave spawning data regarding an in-game Cluster. Its
/// managed equivalent is WaveSpawner.ClusterAuthoring.
/// </summary>
public struct Cluster
{
public NativeArray<ClusterRule> clusterRules;
public FixedString32Bytes name;
/// <summary>
/// Builds a cluster, given a name (for the cluster) and an array
/// of cluster rules.
/// </summary>
/// <param name="name">The name of the cluster</param>
/// <param name="rules">An array of cluster rules for this cluster.</param>
/// <returns>A cluster.</returns>
public static Cluster Build(string name, ClusterRule[] rules)
{
//Construct a new cluster, set the name
Cluster cluster = new Cluster();
cluster.name = name;
//This assumes the array of cluster rules will be unchanging in length
cluster.clusterRules = new NativeArray<ClusterRule>(rules.Length, Allocator.Persistent);
//Copy over rule data
for (int i = 0; i < rules.Length; i++)
cluster.clusterRules[i] = rules[i];
return cluster;
}
/// <summary>
/// Builds a cluster, given a name and array of cluster rules. Note that
/// this uses ClusterRuleAuthoring rather than cluster rule.
/// </summary>
/// <param name="name">The name of the cluster</param>
/// <param name="rules">An array of cluster rules for this cluster.</param>
/// <returns>A cluster.</returns>
public static Cluster BuildFromAuthoring(string name, ClusterRuleAuthoring[] rules)
{
//Construct, set name
Cluster cluster = new Cluster();
cluster.name = name;
//Allocate a native container for the cluster rules. This assumes a fixed size.
cluster.clusterRules = new NativeArray<ClusterRule>(rules.Length, Allocator.Persistent);
//Copy over cluster rules, convert from managed -> unmanaged
for (int i = 0; i < rules.Length; i++)
cluster.clusterRules[i] = new ClusterRule(rules[i].unitId, rules[i].amount);
return cluster;
}
/// <summary>
/// Called when this cluster is disposed of, deallocates
/// the native container associated with cluster rules.
/// </summary>
public void Dispose()
{
clusterRules.Dispose();
}
}
/// <summary>
/// ECS wave spawning data regarding a cluster rule. Its managed
/// equivalent is a WaveSpawner.ClusterRuleAuthoring.
/// </summary>
public struct ClusterRule
{
public int unitId;
public int amount;
public ClusterRule(int unitId, int amount)
{
this.unitId = unitId;
this.amount = amount;
}
}
#endregion
#region Waves
/// <summary>
/// ECS wave spawning data regarding an in-game wave. Its
/// managed equivalent is WaveSpawner.WaveAuthoring.
/// </summary>
public struct Wave : IDisposable
{
public NativeArray<WaveRule> waveRules;
public FixedString32Bytes name;
/// <summary>
/// Builds a wave given a name and array of wave rules.
/// </summary>
/// <param name="name">The name of the wave</param>
/// <param name="rules">The set of rules</param>
/// <returns>A constructed wave object</returns>
public static Wave Build(string name, WaveRule[] rules)
{
//Make a new wave with a name
Wave wave = new Wave();
wave.name = name;
//Allocate a native container for wave rules, assumes fixed size
wave.waveRules = new NativeArray<WaveRule>(rules.Length, Allocator.Persistent);
//Copy over the rules
for (int i = 0; i < rules.Length; i++)
wave.waveRules[i] = rules[i];
return wave;
}
/// <summary>
/// Builds a wave given a name and array of wave rules, where the
/// rules are authoring (managed) equivalents.
/// </summary>
/// <param name="name">The name of the wave</param>
/// <param name="rules">The set of rules</param>
/// <returns>A constructed wave object</returns>
public static Wave BuildFromAuthoring(string name, WaveRuleAuthoring[] rules)
{
//Make a wave with a name
Wave wave = new Wave();
wave.name = name;
//Allocate a native container for the wave rules, assumes fixed size
wave.waveRules = new NativeArray<WaveRule>(rules.Length, Allocator.Persistent);
//Copy over wave rules -- construct and copy over to
//unmanaged type
for (int i = 0; i < rules.Length; i++)
{
wave.waveRules[i] = new WaveRule
{
clusterOrTypeId = rules[i].clusterOrTypeId,
triggerPopulationCap = rules[i].triggerPopulationCap,
triggerTimeSinceLastSpawn = rules[i].triggerTimeSinceLastSpawn,
type = rules[i].type
};
}
return wave;
}
/// <summary>
/// Disposes of the wave rules, which are a native container.
/// </summary>
public void Dispose()
{
waveRules.Dispose();
}
}
/// <summary>
/// Represents the type of thing in the wave rule. This
/// is needed because of the clusterOrTypeId member which
/// could either refer to a cluster or unit.
/// </summary>
public enum WaveRuleType
{
Cluster, Unit
}
/// <summary>
/// Represents a wave rule: the type of thing to spawn, the
/// id of the unity/cluster & the spawn conditions.
/// </summary>
public struct WaveRule
{
public int clusterOrTypeId;
public int triggerPopulationCap;
public float triggerTimeSinceLastSpawn;
public WaveRuleType type;
public WaveRule(WaveRuleType type, int clusterOrTypeId, int triggerPopulationCap, float triggerTimeSinceLastSpawn)
{
this.type = type;
this.clusterOrTypeId = clusterOrTypeId;
this.triggerPopulationCap = triggerPopulationCap;
this.triggerTimeSinceLastSpawn = triggerTimeSinceLastSpawn;
}
}
#endregion
/// <summary>
/// This is a singleton component which is created by
/// the WaveSpawnerSingletonSystem. This represents the
/// singleton in the ECS world you can access to get all
/// of the relevant wave spawner data.
///
/// The data in this singleton is set in OnStartRunning of
/// the WaveSpawnerSingletonSystem. That system looks for
/// the existence of a SpawnerDataWrapper (which is baked).
/// When it is baked, and OnStartRunning is called, that system
/// will construct this component and set the relevant data.
///
/// -----------------------
///
/// Note: These are hash maps. This is because the wave
/// spawning data uses non-contiguous indices. For
/// example, you might parse a file with wave id
/// 1, and wave id 5, and no others.
///
/// Therefore, these hash maps are look ups -- they
/// convert:
/// - unit ids (int key) to a Unit
/// - cluster ids (int key) to a Cluster
/// - wave ids (int key) to a Wave.
///
/// See WaveSpawnerSystem for some example usage on how
/// to use these data structures.
/// </summary>
public struct SpawnerDataSingleton : IComponentData
{
public NativeHashMap<int, Unit> units;
public NativeHashMap<int, Cluster> clusters;
public NativeHashMap<int, Wave> waves;
}
}
using UnityEngine;
using Unity.Entities;
using Unity.Collections;
using WaveSpawner;
using WaveSpawner.Entities;
/// <summary>
/// This is a system which has a few main responsibilities:
///
/// - To create a SpawnerDataSingleton entity & component singleton in the world.
/// - To copy all the data from the single baked SpawnerDataWrapped to that singleton.
/// - To manage the allocation, disposal & deallocation of persistent native containers for this data.
///
/// This is the system which makes the singleton access to all the spawner data possible. It is the
/// main system which kickstarts all the other managed -> unmanaged conversion processes.
/// </summary>
public partial struct WaveSpawnerCreateSingletonSystem : ISystem, ISystemStartStop
{
/// <summary>
/// Whether or not to show debug messages. This is set from
/// the debug property on WaveSpawnerDataAuthoring.
/// </summary>
private bool debug;
public void OnCreate(ref SystemState state)
{
//Wait for a baked SpawnerDataWrapper to be present before
//running update.
state.RequireForUpdate<SpawnerDataWrapper>();
//>= singleton? Throw an error, something has gone very wrong
if(SystemAPI.HasSingleton<SpawnerDataSingleton>())
throw new UnityException("Detected more than one instance of SpawnerDataSingleton.");
//Create the singleton entity and set its component data. Note the
//second line here calls AllocWaveSpawner(), which allocates all
//the native memory it needs through its lifetime
Entity entity = state.EntityManager.CreateEntity(typeof(SpawnerDataSingleton));
state.EntityManager.SetComponentData(entity, AllocWaveSpawner());
}
public void OnStartRunning(ref SystemState state)
{
//This is called when there is a SpawnerDataWrapper in the world. Therefore
//we can guarantee there is at least one, and there should also be a SpawnerDataSingleton
//as we created this in OnCreate.
var authoringWrapper = SystemAPI.GetSingleton<SpawnerDataWrapper>();
var spawnerSingleton = SystemAPI.GetSingletonRW<SpawnerDataSingleton>();
//Allocate and convert all managed to unmanaged data
SetupSpawnerFromDeferredAuthoringData(authoringWrapper.spawnerAuthoring, ref spawnerSingleton.ValueRW);
//Set debug mode -- everything below this is just debug information
//that is printed out
debug = authoringWrapper.debug;
if (!debug)
return;
foreach (var unit in spawnerSingleton.ValueRO.units)
{
Debug.Log($"UNIT: key {unit.Key} and name {unit.Value.name}");
}
foreach (var cluster in spawnerSingleton.ValueRO.clusters)
{
Debug.Log($"CLUSTER: key {cluster.Key} and name {cluster.Value.name}");
foreach (var rule in cluster.Value.clusterRules)
Debug.Log($"- RULE: unit id {rule.unitId} ({spawnerSingleton.ValueRO.units[rule.unitId].name}) and amount {rule.amount}");
}
foreach (var wave in spawnerSingleton.ValueRO.waves)
{
Debug.Log($"WAVE: key {wave.Key} and name {wave.Value.name}");
foreach (var rule in wave.Value.waveRules)
Debug.Log($"- RULE: type {rule.type}, unit/cluster id: {rule.clusterOrTypeId}, pop {rule.triggerPopulationCap}, delay {rule.triggerTimeSinceLastSpawn}");
}
}
/// <summary>
/// Deallocates the wave spawner by calling dispose on
/// all native containers associated with it. Should be
/// called in OnDestroy.
/// </summary>
/// <param name="spawner"></param>
public void DeallocWaveSpawner(ref SpawnerDataSingleton spawner)
{
if(debug) Debug.Log("Deallocating wave spawner native memory");
//We have to run through each of these individual
//elements and call dispose because this will trigger the cluster's
//own dispose method -- which will dispose of the rules
foreach (var kv in spawner.clusters)
kv.Value.Dispose();
//Same for the waves
foreach (var wave in spawner.waves)
wave.Value.Dispose();
spawner.units.Dispose();
spawner.clusters.Dispose();
spawner.waves.Dispose();
}
private void LogDuplicateIdWarning(string type, int id, FixedString32Bytes oldName, string newName)
{
Debug.LogWarning($"Found duplicate {type} with id {id} ({oldName}). Can't set to new {type} '{newName}' as it would overwrite it; skipping entry.");
}
/// <summary>
/// This is the primary function which copies WaveSpawnerDataAuthoring data
/// in
/// </summary>
/// <param name="wrapper"></param>
/// <param name="spawner"></param>
private void SetupSpawnerFromDeferredAuthoringData(in UnityObjectRef<WaveSpawnerDataAuthoring> wrapper, ref SpawnerDataSingleton spawner)
{
//Convert all units
foreach (var unit in wrapper.Value.m_units)
{
if (spawner.units.ContainsKey(unit.id))
{
LogDuplicateIdWarning("unit", unit.id, spawner.units[unit.id].name, unit.name);
continue;
}
spawner.units[unit.id] = new Unit
{
damage = unit.damage,
id = unit.id,
health = unit.health,
name = unit.name,
speed = unit.speed,
};
}
//Convert all clusters
foreach (var cluster in wrapper.Value.m_clusters)
{
if(spawner.clusters.ContainsKey(cluster.id))
{
LogDuplicateIdWarning("cluster", cluster.id, spawner.clusters[cluster.id].name, cluster.name);
continue;
}
spawner.clusters[cluster.id] = Cluster.BuildFromAuthoring(cluster.name, cluster.clusterRules);
}
//Convert all waves
foreach(var wave in wrapper.Value.m_waves)
{
if (spawner.waves.ContainsKey(wave.id))
{
LogDuplicateIdWarning("wave", wave.id, spawner.waves[wave.id].name, wave.name);
continue;
}
spawner.waves[wave.id] = Wave.BuildFromAuthoring(wave.name, wave.waveRules);
}
if (debug) Debug.Log("Completed deferred baking; found SpawnerDataWrapper.");
}
/// <summary>
/// Test method to set up the spawner with some sample data, for
/// debug purposes.
/// </summary>
/// <param name="spawner">The spawner to set up</param>
private void SetupTestSpawner(ref SpawnerDataSingleton spawner)
{
//Set up units
spawner.units[0] = new Unit { damage = 25, health = 100, speed = 10, id = 0, name = "skeleton" };
spawner.units[1] = new Unit { damage = 10, health = 100, speed = 5, id = 1, name = "spider" };
spawner.units[2] = new Unit { damage = 10, health = 100, speed = 2, id = 2, name = "zombie" };
//Set up clusters
spawner.clusters[0] = Cluster.Build("skeleton-spider-gang", new ClusterRule[] { new(0, 3), new(1, 5) });
//Set up waves
spawner.waves[0] = Wave.Build("first-wave", new WaveRule[] { new (WaveRuleType.Cluster, 0, -1, 2.0f) });
}
/// <summary>
/// Allocates all native memory and returns a SpawnerDataSingleton which
/// is set in OnCreate.
/// </summary>
/// <returns></returns>
private SpawnerDataSingleton AllocWaveSpawner()
{
SpawnerDataSingleton spawner = new SpawnerDataSingleton();
//Note: this assumes there will be a max of 32 types of unit, clusters
// and waves.
spawner.units = new NativeHashMap<int, Unit>(32, Allocator.Persistent);
spawner.clusters = new NativeHashMap<int, Cluster>(32, Allocator.Persistent);
spawner.waves = new NativeHashMap<int, Wave>(32, Allocator.Persistent);
if(debug) Debug.Log("Allocated native memory for wave spawner");
//Somehow there is no SpawnerDataSingleton? If so, add some test data
//to stop things falling over.
if(!SystemAPI.HasSingleton<SpawnerDataSingleton>())
SetupTestSpawner(ref spawner);
return spawner;
}
public void OnDestroy(ref SystemState state)
{
//Called when the singleton needs to be deallocated.
//..
//Get the wave spawner & its entity
var waveSpawner = SystemAPI.GetSingleton<SpawnerDataSingleton>();
var waveSpawnerEntity = SystemAPI.GetSingletonEntity<SpawnerDataSingleton>();
//Call dealloc
DeallocWaveSpawner(ref waveSpawner);
}
public void OnStopRunning(ref SystemState state) { }
}
using UnityEngine;
using Unity.Entities;
using Unity.Collections;
using UnityEngine.InputSystem;
using WaveSpawner;
using WaveSpawner.Entities;
/// <summary>
/// The wave spawner system, which uses wave spawner data
/// grabbed from SpawnerDataSingleton.
/// </summary>
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<SpawnerDataSingleton>();
//Grab query for counting enemies later
enemyQuery = GetEntityCountQuery<EnemyComponent>(ref state);
//Setup default state
timer = 0.0f;
waveIndex = 0;
waveRuleIndex = 0;
lastWaveSpawnTime = 0.0f;
hasFinished = false;
}
/// <summary>
/// Checks whether the next wave rule should be spawned. This returns
/// true if the spawning conditions are met.
/// </summary>
/// <param name="waveData"></param>
/// <param name="numEnemies"></param>
/// <param name="timeSinceSpawn"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Spawns entities according to a wave rule.
/// </summary>
/// <param name="wave"></param>
/// <param name="waveSpawnerData"></param>
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);
}
/// <summary>
/// Spawns a specified cluster.
/// </summary>
/// <param name="cluster"></param>
/// <param name="units"></param>
private void SpawnCluster(Cluster cluster, in NativeHashMap<int, Unit> 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}");
}
}
/// <summary>
/// Spawns a specified unit.
/// </summary>
/// <param name="unit"></param>
private void SpawnUnit(Unit unit)
{
//TODO: Spawn the unit with ECS
Debug.Log($"Wave {waveIndex}, spawn {unit.name} ({unit.id})");
}
/// <summary>
/// Clears all enemies in the world. Used for debugging
/// the population threshold condition.
/// </summary>
/// <param name="state"></param>
private void ClearAllEnemies(ref SystemState state)
{
foreach (var (enemy, entity) in SystemAPI.Query<RefRW<EnemyComponent>>().WithEntityAccess())
ecb.DestroyEntity(entity);
}
/// <summary>
/// Updates the wave spawner. Contains the main logic of this system.
/// </summary>
/// <param name="state"></param>
/// <param name="waveSpawnerData"></param>
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<SpawnerDataSingleton>();
//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<T>(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<int, Wave> waves)
=> waveIndex >= waves.Length;
private EntityCommandBuffer GetECB(ref SystemState state)
=> new EntityCommandBuffer(Allocator.Temp);
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment