enemies spawn with offsets in positions and animations

This commit is contained in:
Ian Woods 2026-06-23 21:16:12 -07:00
parent d1930a6b00
commit 53e7fc78a7
4 changed files with 35 additions and 12 deletions

View file

@ -16,4 +16,6 @@ MonoBehaviour:
Entries: Entries:
- EnemyType: {fileID: 11400000, guid: a05af37e2256f164d80a66cc2a582466, type: 2} - EnemyType: {fileID: 11400000, guid: a05af37e2256f164d80a66cc2a582466, type: 2}
Count: 50 Count: 50
SpawnInterval: 0.5 ChunkSize: 5
SpawnInterval: 2.5
SpawnSpread: 10

View file

@ -120,6 +120,13 @@ namespace TD.Gameplay
{ {
status = GetComponent<EnemyStatus>(); status = GetComponent<EnemyStatus>();
// 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<Animator>();
if (animator != null)
animator.Update(Random.Range(0f, 2f));
if (!IsServer) return; if (!IsServer) return;
if (!hasPendingInit) if (!hasPendingInit)

View file

@ -17,9 +17,18 @@ namespace TD.Gameplay
[Tooltip("How many enemies of this type to spawn.")] [Tooltip("How many enemies of this type to spawn.")]
public int Count; public int Count;
[Tooltip("Seconds between each individual spawn within this group. " + [Tooltip("How many enemies spawn per burst. 1 (default) = original one-at-a-time " +
"0 = all spawn simultaneously.")] "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; 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;
} }
/// <summary> /// <summary>

View file

@ -336,9 +336,12 @@ namespace TD.Gameplay
{ {
if (entry.EnemyType == null || entry.Count <= 0) continue; 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) if (entry.SpawnInterval > 0f)
yield return new WaitForSeconds(entry.SpawnInterval); yield return new WaitForSeconds(entry.SpawnInterval);
@ -355,7 +358,7 @@ namespace TD.Gameplay
// ----- Spawn helpers ---------------------------------------------- // ----- Spawn helpers ----------------------------------------------
private void SpawnEnemyInAllZones(EnemyDefinition def) private void SpawnEnemyInAllZones(EnemyDefinition def, float spawnSpread = 0f)
{ {
var loader = LevelLoader.Instance; var loader = LevelLoader.Instance;
if (loader?.LevelData?.PlayerZones == null) return; 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 // this enemy for leak attribution — can't be derived from the spawner
// tile's owner-grid entry because SpawnerVolume sits outside // tile's owner-grid entry because SpawnerVolume sits outside
// PlayerZoneVolume (so OwnerGrid[spawnerTile] = None). // 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) if (def.EnemyPrefab == null)
{ {
@ -388,10 +392,11 @@ namespace TD.Gameplay
return; return;
} }
var go = Instantiate( Vector3 spawnPos = GridCoordinates.GridToWorld(spawnerTile);
def.EnemyPrefab, if (spawnSpread > 0f)
GridCoordinates.GridToWorld(spawnerTile), spawnPos.x += Random.Range(-spawnSpread, spawnSpread);
Quaternion.identity);
var go = Instantiate(def.EnemyPrefab, spawnPos, Quaternion.identity);
var health = go.GetComponent<EnemyHealth>(); var health = go.GetComponent<EnemyHealth>();
var movement = go.GetComponent<EnemyMovement>(); var movement = go.GetComponent<EnemyMovement>();