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