Initial pass on wave spawning.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2023-08-02 17:27:45 -04:00
parent 3043c54bc4
commit 505b292718
40 changed files with 952 additions and 0 deletions

11
Assets/Scripts/Enemy.cs Normal file
View 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
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0ce366659158473783c8d53d2f3fc5f5
timeCreated: 1690856221

View File

@@ -0,0 +1,11 @@
using UnityEngine;
namespace DefaultNamespace
{
[CreateAssetMenu(menuName = "Enemy Runtime Set", fileName =
"EnemyRuntimeSet")]
public class EnemyRuntimeSetSO : RuntimeSetSO<Enemy>
{
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5f1e329a7f6343f88553db21ffb55693
timeCreated: 1690858329

3
Assets/Scripts/Events.meta generated Normal file
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5741392326404cb387003f002d90acee
timeCreated: 1690857552

View 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);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 908e283bf3a54c859a0771fd8aed7bce
timeCreated: 1690857571

3
Assets/Scripts/Level.meta generated Normal file
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eb1638a1c58a4878adf9166dddc40738
timeCreated: 1690927890

View 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);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a91533ae182d470c88ce779956713e02
timeCreated: 1690927896

View 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
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 942f0916a83b433891dd4c6a9bbb50fd
timeCreated: 1690858304

3
Assets/Scripts/Wave.meta generated Normal file
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6515ffdab88e4d23b8f09dace4a2157a
timeCreated: 1690921607

View File

@@ -0,0 +1,9 @@
namespace Wave
{
public enum SpawnDistribution
{
Uniform,
Random,
DiskRandom
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2a4df8269a27463eaf87da9ae1283dcb
timeCreated: 1690925288

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
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 IReadOnlyCollection<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;
}
}
}

3
Assets/Scripts/Wave/SpawnPackSO.cs.meta generated Normal file
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 52673fed940f4a67b5a08641a55feff6
timeCreated: 1690938542

View 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
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17a69c35e3e64b6d9f655533478f9772
timeCreated: 1690856578

View File

@@ -0,0 +1,129 @@
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);
yield return new WaitForSeconds(timeBetweenClearedWaves);
}
}
private IEnumerator SpawnWave(SpawnWaveSO spawnWave)
{
var enemyPacksToSpawn = spawnWave.Packs.Select(pack => pack.EnemiesToSpawn()).ToList().AsReadOnly();
var totalPacksToSpawn = enemyPacksToSpawn.Count;
var numberPacksSpawned = 0;
foreach (var enemyPackToSpawn in enemyPacksToSpawn)
{
numberPacksSpawned++;
var waveCompletionPercentage = (float) numberPacksSpawned / (float) totalPacksToSpawn;
// 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 enemyPackSpawnCenter = CalculatePackSpawnPosition(waveCompletionPercentage, spawnWave);
foreach (var enemy in enemyPackToSpawn)
{
var spawnLocation = CalculateEnemySpawnPosition(enemyPackSpawnCenter, spawnWave.PackRadius);
var spawnedEnemy = Instantiate(enemy, spawnLocation, 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 Vector3 CalculatePackSpawnPosition(float waveCompletionPercentage, SpawnWaveSO spawnWave)
{
var angle = waveCompletionPercentage * 2 * MathF.PI;
var unitCirclePosition = Random.insideUnitCircle;
var swizzledUnitCirclePosition = new Vector3(unitCirclePosition.x, 0, unitCirclePosition.y);
var spawnOffset = spawnWave.Distribution switch
{
SpawnDistribution.Uniform => new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle)) * spawnWave.Radius,
SpawnDistribution.Random => swizzledUnitCirclePosition.normalized * spawnWave.Radius,
SpawnDistribution.DiskRandom => swizzledUnitCirclePosition * spawnWave.Radius,
_ => throw new ArgumentOutOfRangeException(nameof(spawnWave.Distribution), spawnWave.Distribution, null)
};
spawnOffset += spawnOffset.normalized * (minimumSpawnRadius + spawnWave.PackRadius);
return spawnCenter.position + spawnOffset;
}
private Vector3 CalculateEnemySpawnPosition(Vector3 center, float radius)
{
var unitCirclePosition = Random.insideUnitCircle;
var swizzledUnitCirclePosition = new Vector3(unitCirclePosition.x, 0, unitCirclePosition.y);
var spawnOffset = swizzledUnitCirclePosition * radius;
return center + spawnOffset;
}
private void Update()
{
_timeSinceLastSpawnWaveStarted += Time.deltaTime;
}
}
}

3
Assets/Scripts/Wave/WaveSpawner.cs.meta generated Normal file
View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bb718e52728142e4a8db081fd6a36a70
timeCreated: 1690856916