From 53e7fc78a71e5bd76c187267757d3914df77c983 Mon Sep 17 00:00:00 2001 From: Ian Woods Date: Tue, 23 Jun 2026 21:16:12 -0700 Subject: [PATCH] enemies spawn with offsets in positions and animations --- .../Definitions/Waves/Wave1Definition.asset | 4 +++- .../Scripts/Gameplay/EnemyMovement.cs | 7 ++++++ .../Scripts/Gameplay/WaveDefinition.cs | 13 +++++++++-- .../_Project/Scripts/Gameplay/WaveManager.cs | 23 +++++++++++-------- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Assets/_Project/Definitions/Waves/Wave1Definition.asset b/Assets/_Project/Definitions/Waves/Wave1Definition.asset index fc6ffe8..6379238 100644 --- a/Assets/_Project/Definitions/Waves/Wave1Definition.asset +++ b/Assets/_Project/Definitions/Waves/Wave1Definition.asset @@ -16,4 +16,6 @@ MonoBehaviour: Entries: - EnemyType: {fileID: 11400000, guid: a05af37e2256f164d80a66cc2a582466, type: 2} Count: 50 - SpawnInterval: 0.5 + ChunkSize: 5 + SpawnInterval: 2.5 + SpawnSpread: 10 diff --git a/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs b/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs index 56b4abf..2ee92d4 100644 --- a/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs +++ b/Assets/_Project/Scripts/Gameplay/EnemyMovement.cs @@ -120,6 +120,13 @@ namespace TD.Gameplay { status = GetComponent(); + // Randomize the walk-cycle phase on every peer independently. Animations + // are purely visual and not replicated, so each client picks its own offset — + // the result looks correct everywhere without any network traffic. + var animator = GetComponentInChildren(); + if (animator != null) + animator.Update(Random.Range(0f, 2f)); + if (!IsServer) return; if (!hasPendingInit) diff --git a/Assets/_Project/Scripts/Gameplay/WaveDefinition.cs b/Assets/_Project/Scripts/Gameplay/WaveDefinition.cs index 16a9e4c..5b62aef 100644 --- a/Assets/_Project/Scripts/Gameplay/WaveDefinition.cs +++ b/Assets/_Project/Scripts/Gameplay/WaveDefinition.cs @@ -17,9 +17,18 @@ namespace TD.Gameplay [Tooltip("How many enemies of this type to spawn.")] public int Count; - [Tooltip("Seconds between each individual spawn within this group. " + - "0 = all spawn simultaneously.")] + [Tooltip("How many enemies spawn per burst. 1 (default) = original one-at-a-time " + + "behaviour. Values > 1 spawn that many simultaneously before waiting SpawnInterval.")] + public int ChunkSize; + + [Tooltip("Seconds between each burst. With ChunkSize > 1 this is the pause between " + + "groups rather than between individual enemies.")] public float SpawnInterval; + + [Tooltip("Max X-axis world-unit offset applied randomly to each enemy's spawn position. " + + "0 = all spawn at the exact tile centre. Spreads enemies left/right without " + + "affecting their forward/backward position or A* pathfinding.")] + public float SpawnSpread; } /// diff --git a/Assets/_Project/Scripts/Gameplay/WaveManager.cs b/Assets/_Project/Scripts/Gameplay/WaveManager.cs index 405b148..d39e908 100644 --- a/Assets/_Project/Scripts/Gameplay/WaveManager.cs +++ b/Assets/_Project/Scripts/Gameplay/WaveManager.cs @@ -336,9 +336,12 @@ namespace TD.Gameplay { if (entry.EnemyType == null || entry.Count <= 0) continue; - for (int i = 0; i < entry.Count; i++) + int chunkSize = Mathf.Max(1, entry.ChunkSize); + for (int i = 0; i < entry.Count; i += chunkSize) { - SpawnEnemyInAllZones(entry.EnemyType); + int burst = Mathf.Min(chunkSize, entry.Count - i); + for (int j = 0; j < burst; j++) + SpawnEnemyInAllZones(entry.EnemyType, entry.SpawnSpread); if (entry.SpawnInterval > 0f) yield return new WaitForSeconds(entry.SpawnInterval); @@ -355,7 +358,7 @@ namespace TD.Gameplay // ----- Spawn helpers ---------------------------------------------- - private void SpawnEnemyInAllZones(EnemyDefinition def) + private void SpawnEnemyInAllZones(EnemyDefinition def, float spawnSpread = 0f) { var loader = LevelLoader.Instance; if (loader?.LevelData?.PlayerZones == null) return; @@ -376,11 +379,12 @@ namespace TD.Gameplay // this enemy for leak attribution — can't be derived from the spawner // tile's owner-grid entry because SpawnerVolume sits outside // PlayerZoneVolume (so OwnerGrid[spawnerTile] = None). - SpawnEnemy(def, zone.Spawners[0].TilePosition, zone.Owner); + SpawnEnemy(def, zone.Spawners[0].TilePosition, zone.Owner, spawnSpread); } } - private void SpawnEnemy(EnemyDefinition def, Vector2Int spawnerTile, PlayerSlot ownerSlot) + private void SpawnEnemy(EnemyDefinition def, Vector2Int spawnerTile, PlayerSlot ownerSlot, + float spawnSpread = 0f) { if (def.EnemyPrefab == null) { @@ -388,10 +392,11 @@ namespace TD.Gameplay return; } - var go = Instantiate( - def.EnemyPrefab, - GridCoordinates.GridToWorld(spawnerTile), - Quaternion.identity); + Vector3 spawnPos = GridCoordinates.GridToWorld(spawnerTile); + if (spawnSpread > 0f) + spawnPos.x += Random.Range(-spawnSpread, spawnSpread); + + var go = Instantiate(def.EnemyPrefab, spawnPos, Quaternion.identity); var health = go.GetComponent(); var movement = go.GetComponent();