Source file unity-csharp/arcade-classics/space-invaders/InvaderFormation.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: a marching enemy formation, edge-drop logic, timer-driven firing, and speed-up as numbers fall
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[csharp-collections]], [[object-pool-pattern]]
using System.Collections.Generic;
using UnityEngine;
// The formation is the heart of Space Invaders. All invaders are children of this
// object, so the whole grid moves by moving one transform. Two timers drive it:
// one steps the march, one fires from a random survivor. Each kill speeds the
// march up, which is what makes the last few invaders feel frantic.
public class InvaderFormation : MonoBehaviour
{
[Header("Grid")]
[SerializeField] private Invader invaderPrefab;
[SerializeField] private int columns = 8;
[SerializeField] private int rows = 4;
[SerializeField] private Vector2 spacing = new Vector2(1.1f, 0.9f);
[SerializeField] private float topMargin = 1.5f;
[Header("March (timer-driven)")]
[SerializeField] private float marchInterval = 0.55f;
[SerializeField] private float minMarchInterval = 0.08f;
[SerializeField] private float marchStepX = 0.5f;
[SerializeField] private float dropStepY = 0.5f;
[Tooltip("Multiplier applied to the march interval each time an invader dies (<1 = faster).")]
[SerializeField] private float speedUpFactor = 0.97f;
[SerializeField] private float edgePadding = 0.6f;
[Header("Firing")]
[SerializeField] private ProjectilePool bulletPool;
[SerializeField] private float fireInterval = 1.1f;
[SerializeField] private float bulletSpeed = 7f;
[Tooltip("Layer the invader bullets damage (the player).")]
[SerializeField] private LayerMask playerMask;
private readonly List<Invader> invaders = new List<Invader>();
private SpaceInvadersGame game;
private Camera cachedCamera;
private int direction = 1;
private float marchTimer;
private float fireTimer;
private float currentMarchInterval;
private float playerLineY;
private bool active;
private void Awake()
{
cachedCamera = Camera.main;
}
public void Bind(SpaceInvadersGame owner, float playerY)
{
game = owner;
playerLineY = playerY;
}
public void BuildWave()
{
foreach (Invader invader in invaders)
{
if (invader != null)
{
Destroy(invader.gameObject);
}
}
invaders.Clear();
transform.position = Vector3.zero;
direction = 1;
marchTimer = 0f;
fireTimer = 0f;
currentMarchInterval = marchInterval;
float startX = -((columns - 1) * spacing.x) / 2f;
float startY = cachedCamera.orthographicSize - topMargin;
for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
Vector3 position = new Vector3(startX + column * spacing.x, startY - row * spacing.y, 0f);
Invader invader = Instantiate(invaderPrefab, position, Quaternion.identity, transform);
invader.Bind(this);
invaders.Add(invader);
}
}
active = true;
}
private void Update()
{
if (!active)
{
return;
}
marchTimer += Time.deltaTime;
if (marchTimer >= currentMarchInterval)
{
marchTimer -= currentMarchInterval;
March();
}
fireTimer += Time.deltaTime;
if (fireTimer >= fireInterval)
{
fireTimer -= fireInterval;
FireFromRandomInvader();
}
}
private void March()
{
GetExtents(out float minX, out float maxX, out float minY);
float screenHalfWidth = cachedCamera.orthographicSize * cachedCamera.aspect - edgePadding;
bool willHitEdge =
(direction > 0 && maxX + marchStepX > screenHalfWidth) ||
(direction < 0 && minX - marchStepX < -screenHalfWidth);
if (willHitEdge)
{
// Turn around and drop a row. Dropping onto the player's line is a loss.
direction = -direction;
transform.position += Vector3.down * dropStepY;
if (minY - dropStepY <= playerLineY)
{
active = false;
game.OnInvadersReachedPlayer();
}
}
else
{
transform.position += Vector3.right * (direction * marchStepX);
}
}
private void FireFromRandomInvader()
{
if (invaders.Count == 0 || bulletPool == null)
{
return;
}
Invader shooter = invaders[Random.Range(0, invaders.Count)];
bulletPool.Get().Launch(shooter.transform.position + Vector3.down * 0.5f, -1f, bulletSpeed, playerMask);
}
// Called by an invader when a player bullet hits it.
public void OnInvaderKilled(Invader invader)
{
invaders.Remove(invader);
game.AddScore(invader.ScoreValue);
Destroy(invader.gameObject);
// The fewer invaders remain, the faster the survivors march.
currentMarchInterval = Mathf.Max(minMarchInterval, currentMarchInterval * speedUpFactor);
if (invaders.Count == 0)
{
active = false;
game.OnWaveCleared();
}
}
private void GetExtents(out float minX, out float maxX, out float minY)
{
minX = float.MaxValue;
maxX = float.MinValue;
minY = float.MaxValue;
foreach (Invader invader in invaders)
{
Vector3 position = invader.transform.position;
minX = Mathf.Min(minX, position.x);
maxX = Mathf.Max(maxX, position.x);
minY = Mathf.Min(minY, position.y);
}
}
}