// 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 invaders = new List(); 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); } } }