Implement wave based spawning of enemies #11
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Implements basic waves of enemies that spawn. #11 Reviewed-on: #16 Reviewed-by: zephyr <zephyr@noreply.localhost> Co-authored-by: Michael <mep053@gmail.com> Co-committed-by: Michael <mep053@gmail.com>
This commit is contained in:
11
Assets/Scripts/Enemy.cs
Normal file
11
Assets/Scripts/Enemy.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace DefaultNamespace
|
||||
{
|
||||
// TODO (Michael): Empty behavior until we decide more on how enemies should be structured. Mainly being used for
|
||||
// other systems to have something to reference.
|
||||
public class Enemy : MonoBehaviour
|
||||
{
|
||||
|
||||
}
|
||||
}
|
3
Assets/Scripts/Enemy.cs.meta
generated
Normal file
3
Assets/Scripts/Enemy.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ce366659158473783c8d53d2f3fc5f5
|
||||
timeCreated: 1690856221
|
11
Assets/Scripts/EnemyRuntimeSetSO.cs
Normal file
11
Assets/Scripts/EnemyRuntimeSetSO.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace DefaultNamespace
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Enemy Runtime Set", fileName =
|
||||
"EnemyRuntimeSet")]
|
||||
public class EnemyRuntimeSetSO : RuntimeSetSO<Enemy>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
3
Assets/Scripts/EnemyRuntimeSetSO.cs.meta
generated
Normal file
3
Assets/Scripts/EnemyRuntimeSetSO.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f1e329a7f6343f88553db21ffb55693
|
||||
timeCreated: 1690858329
|
3
Assets/Scripts/Events.meta
generated
Normal file
3
Assets/Scripts/Events.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5741392326404cb387003f002d90acee
|
||||
timeCreated: 1690857552
|
17
Assets/Scripts/Events/SpawnWaveSOEventChannelSO.cs
Normal file
17
Assets/Scripts/Events/SpawnWaveSOEventChannelSO.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using Wave;
|
||||
|
||||
namespace Events
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Events/Spawn Wave SO Event Channel")]
|
||||
public class SpawnWaveSOEventChannelSO : ScriptableObject
|
||||
{
|
||||
public event UnityAction<SpawnWaveSO> OnEventRaised;
|
||||
|
||||
public void RaiseEvent(SpawnWaveSO spawnWave)
|
||||
{
|
||||
OnEventRaised?.Invoke(spawnWave);
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Scripts/Events/SpawnWaveSOEventChannelSO.cs.meta
generated
Normal file
3
Assets/Scripts/Events/SpawnWaveSOEventChannelSO.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 908e283bf3a54c859a0771fd8aed7bce
|
||||
timeCreated: 1690857571
|
3
Assets/Scripts/Level.meta
generated
Normal file
3
Assets/Scripts/Level.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb1638a1c58a4878adf9166dddc40738
|
||||
timeCreated: 1690927890
|
18
Assets/Scripts/Level/LevelDirector.cs
Normal file
18
Assets/Scripts/Level/LevelDirector.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Wave;
|
||||
|
||||
namespace Level
|
||||
{
|
||||
public class LevelDirector : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private WaveSpawner waveSpawner;
|
||||
[SerializeField] private List<SpawnWaveSO> spawnWaves;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
waveSpawner.Begin(spawnWaves);
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Scripts/Level/LevelDirector.cs.meta
generated
Normal file
3
Assets/Scripts/Level/LevelDirector.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a91533ae182d470c88ce779956713e02
|
||||
timeCreated: 1690927896
|
26
Assets/Scripts/RuntimeSetSO.cs
Normal file
26
Assets/Scripts/RuntimeSetSO.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public abstract class RuntimeSetSO<T> : ScriptableObject
|
||||
{
|
||||
[HideInInspector] [SerializeField] private List<T> items = new();
|
||||
|
||||
public IReadOnlyCollection<T> Items => items;
|
||||
public bool IsEmpty => items.Count == 0;
|
||||
|
||||
public void Add(T thing)
|
||||
{
|
||||
if (!items.Contains(thing))
|
||||
{
|
||||
items.Add(thing);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(T thing)
|
||||
{
|
||||
if (items.Contains(thing))
|
||||
{
|
||||
items.Remove(thing);
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Scripts/RuntimeSetSO.cs.meta
generated
Normal file
3
Assets/Scripts/RuntimeSetSO.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 942f0916a83b433891dd4c6a9bbb50fd
|
||||
timeCreated: 1690858304
|
3
Assets/Scripts/Wave.meta
generated
Normal file
3
Assets/Scripts/Wave.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6515ffdab88e4d23b8f09dace4a2157a
|
||||
timeCreated: 1690921607
|
9
Assets/Scripts/Wave/SpawnDistribution.cs
Normal file
9
Assets/Scripts/Wave/SpawnDistribution.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Wave
|
||||
{
|
||||
public enum SpawnDistribution
|
||||
{
|
||||
Uniform,
|
||||
Random,
|
||||
DiskRandom
|
||||
}
|
||||
}
|
3
Assets/Scripts/Wave/SpawnDistribution.cs.meta
generated
Normal file
3
Assets/Scripts/Wave/SpawnDistribution.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a4df8269a27463eaf87da9ae1283dcb
|
||||
timeCreated: 1690925288
|
32
Assets/Scripts/Wave/SpawnPackSO.cs
Normal file
32
Assets/Scripts/Wave/SpawnPackSO.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using DefaultNamespace;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace Wave
|
||||
{
|
||||
[CreateAssetMenu(fileName = "SpawnPack", menuName = "Spawn/Pack", order = 0)]
|
||||
public class SpawnPackSO : ScriptableObject
|
||||
{
|
||||
[SerializeField] private List<Enemy> possibleEnemies;
|
||||
[SerializeField] [Min(0)] private int minCount;
|
||||
[SerializeField] [Min(0)] private int maxCount;
|
||||
|
||||
public ReadOnlyCollection<Enemy> EnemiesToSpawn()
|
||||
{
|
||||
Assert.IsTrue(possibleEnemies.Count > 0);
|
||||
|
||||
var count = Random.Range(minCount, maxCount);
|
||||
var enemiesToSpawn = new List<Enemy>();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var enemyIndex = Random.Range(0, possibleEnemies.Count);
|
||||
enemiesToSpawn.Add(possibleEnemies[enemyIndex]);
|
||||
}
|
||||
|
||||
return enemiesToSpawn.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Scripts/Wave/SpawnPackSO.cs.meta
generated
Normal file
3
Assets/Scripts/Wave/SpawnPackSO.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52673fed940f4a67b5a08641a55feff6
|
||||
timeCreated: 1690938542
|
27
Assets/Scripts/Wave/SpawnWaveSO.cs
Normal file
27
Assets/Scripts/Wave/SpawnWaveSO.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace Wave
|
||||
{
|
||||
[CreateAssetMenu(fileName = "SpawnWave", menuName = "Spawn Wave", order = 0)]
|
||||
public class SpawnWaveSO : ScriptableObject
|
||||
{
|
||||
[SerializeField] private List<SpawnPackSO> packs;
|
||||
[SerializeField] private SpawnDistribution distribution;
|
||||
[SerializeField] [Min(10)] private float radius;
|
||||
[SerializeField] [Min(0)] private float packRadius;
|
||||
[SerializeField] [Min(0)] private float timeBetweenSpawns;
|
||||
[SerializeField] [Min(0)] private float timeBetweenPacks;
|
||||
[SerializeField] [Min(0)] private float timeToComplete;
|
||||
|
||||
|
||||
public IReadOnlyCollection<SpawnPackSO> Packs => packs;
|
||||
public SpawnDistribution Distribution => distribution;
|
||||
public float Radius => radius;
|
||||
public float PackRadius => packRadius;
|
||||
public float TimeBetweenSpawns => timeBetweenSpawns;
|
||||
public float TimeBetweenPacks => timeBetweenPacks;
|
||||
public float TimeToComplete => timeToComplete;
|
||||
}
|
||||
}
|
3
Assets/Scripts/Wave/SpawnWaveSO.cs.meta
generated
Normal file
3
Assets/Scripts/Wave/SpawnWaveSO.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17a69c35e3e64b6d9f655533478f9772
|
||||
timeCreated: 1690856578
|
120
Assets/Scripts/Wave/WaveSpawner.cs
Normal file
120
Assets/Scripts/Wave/WaveSpawner.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DefaultNamespace;
|
||||
using Events;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Assertions;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Wave
|
||||
{
|
||||
public class WaveSpawner : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private SpawnWaveSOEventChannelSO startNewSpawnWaveEventChannel;
|
||||
[SerializeField] private SpawnWaveSOEventChannelSO endSpawnWaveEventChannel;
|
||||
|
||||
[SerializeField] private EnemyRuntimeSetSO enemyRuntimeSet;
|
||||
[SerializeField] private Transform spawnCenter;
|
||||
[SerializeField] private float minimumSpawnRadius;
|
||||
[SerializeField] [Min(0)] private float timeBetweenClearedWaves;
|
||||
[SerializeField] [Min(0)] private float timeBeforeFirstWave;
|
||||
|
||||
private float _timeSinceLastSpawnWaveStarted;
|
||||
private Coroutine _handleSpawnWaves;
|
||||
|
||||
private bool HasWaveBeenCleared => enemyRuntimeSet.IsEmpty;
|
||||
|
||||
public void Begin(IEnumerable<SpawnWaveSO> spawnWaves)
|
||||
{
|
||||
Assert.IsNull(_handleSpawnWaves);
|
||||
_handleSpawnWaves = StartCoroutine(CO_HandleSpawnWaves(spawnWaves));
|
||||
}
|
||||
|
||||
public void End()
|
||||
{
|
||||
StopCoroutine(_handleSpawnWaves);
|
||||
}
|
||||
|
||||
private IEnumerator CO_HandleSpawnWaves(IEnumerable<SpawnWaveSO> spawnWaves)
|
||||
{
|
||||
yield return new WaitForSeconds(timeBeforeFirstWave);
|
||||
|
||||
foreach (var spawnWave in spawnWaves)
|
||||
{
|
||||
startNewSpawnWaveEventChannel.RaiseEvent(spawnWave);
|
||||
yield return SpawnWave(spawnWave);
|
||||
_timeSinceLastSpawnWaveStarted = 0.0f;
|
||||
|
||||
yield return new WaitUntil(() => _timeSinceLastSpawnWaveStarted >= spawnWave.TimeToComplete || HasWaveBeenCleared);
|
||||
|
||||
endSpawnWaveEventChannel.RaiseEvent(spawnWave);
|
||||
|
||||
if (HasWaveBeenCleared)
|
||||
{
|
||||
yield return new WaitForSeconds(timeBetweenClearedWaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SpawnWave(SpawnWaveSO spawnWave)
|
||||
{
|
||||
var enemyPacksToSpawn = spawnWave.Packs.Select(pack => pack.EnemiesToSpawn()).ToList().AsReadOnly();
|
||||
|
||||
for (var packN = 0; packN < enemyPacksToSpawn.Count; packN++)
|
||||
{
|
||||
var waveCompletionPercentage = (float)packN / (float)enemyPacksToSpawn.Count;
|
||||
|
||||
var enemyPackSpawnOffset = SampleSpawnOffset(spawnWave.Distribution, spawnWave.Radius, waveCompletionPercentage);
|
||||
enemyPackSpawnOffset += enemyPackSpawnOffset.normalized * (minimumSpawnRadius + spawnWave.PackRadius);
|
||||
|
||||
var enemyPackSpawnPosition = spawnCenter.position + enemyPackSpawnOffset;
|
||||
|
||||
var enemyPackToSpawn = enemyPacksToSpawn[packN];
|
||||
for (var enemyN = 0; enemyN < enemyPackToSpawn.Count; enemyN++)
|
||||
{
|
||||
var packCompletionPercentage = (float)enemyN / (float)enemyPackToSpawn.Count;
|
||||
var enemySpawnOffset = SampleSpawnOffset(SpawnDistribution.DiskRandom, spawnWave.PackRadius, packCompletionPercentage);
|
||||
var enemySpawnPosition = enemyPackSpawnPosition + enemySpawnOffset;
|
||||
|
||||
var spawnedEnemy = Instantiate(enemyPackToSpawn[enemyN], enemySpawnPosition, Quaternion.identity);
|
||||
enemyRuntimeSet.Add(spawnedEnemy);
|
||||
|
||||
if (spawnWave.TimeBetweenSpawns > 0.0f)
|
||||
{
|
||||
yield return new WaitForSeconds(spawnWave.TimeBetweenSpawns);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (spawnWave.TimeBetweenPacks > 0.0f)
|
||||
{
|
||||
yield return new WaitForSeconds(spawnWave.TimeBetweenPacks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 SampleSpawnOffset(SpawnDistribution distribution, float radius, float completionPercentage)
|
||||
{
|
||||
// TODO (Michael): Both the pack and enemy position samplers need to be a bit smarter. I think in most scenarios the pack circle just needs to not overlap with other pack circles.
|
||||
// The enemy spawns probably need something like a poission disk sampler so that monsters dont overlap. The enemy spawn radius at the pack's position should also probably scale with the size of the pack.
|
||||
var angle = completionPercentage * 2 * MathF.PI;
|
||||
var unitCirclePosition = Random.insideUnitCircle;
|
||||
var swizzledUnitCirclePosition = new Vector3(unitCirclePosition.x, 0, unitCirclePosition.y);
|
||||
|
||||
return distribution switch
|
||||
{
|
||||
SpawnDistribution.Uniform => new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * radius,
|
||||
SpawnDistribution.Random => swizzledUnitCirclePosition.normalized * radius,
|
||||
SpawnDistribution.DiskRandom => swizzledUnitCirclePosition * radius,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(distribution), distribution, null)
|
||||
};
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_timeSinceLastSpawnWaveStarted += Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
3
Assets/Scripts/Wave/WaveSpawner.cs.meta
generated
Normal file
3
Assets/Scripts/Wave/WaveSpawner.cs.meta
generated
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb718e52728142e4a8db081fd6a36a70
|
||||
timeCreated: 1690856916
|
Reference in New Issue
Block a user