Created
January 28, 2026 12:39
-
-
Save blewert/bf84322f52681be1aea75775a81245d4 to your computer and use it in GitHub Desktop.
ECS-based wave spawner, data conversion from managed to unmanaged without BlobAssets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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