// GDnD wiki example // Demonstrates: spawning waves, recursive object splitting, and owning score/lives/state // Related pages: [[overview-arcade-classics-as-learning-projects]], [[csharp-collections]], [[unity-gamemanager-pattern]] using System.Collections.Generic; using UnityEngine; // Controller and asteroid field in one. It spawns each wave away from the ship, // handles the split when an asteroid is shot, and owns score, lives, and the // game state. The split is the step-5 highlight: one large rock becomes two // mediums, each medium becomes two smalls, and smalls just vanish. public class AsteroidsGame : MonoBehaviour { public enum State { Playing, GameOver } [SerializeField] private Asteroid asteroidPrefab; [SerializeField] private ShipController ship; [Header("Waves")] [SerializeField] private int firstWaveCount = 4; [SerializeField] private float largeSpeed = 1.5f; [SerializeField] private float mediumSpeed = 2.2f; [SerializeField] private float smallSpeed = 3f; [Tooltip("Keep new large asteroids at least this far from the ship at spawn.")] [SerializeField] private float safeSpawnRadius = 3f; [Header("Lives")] [SerializeField] private int startingLives = 3; [SerializeField] private float respawnInvulnerability = 2f; public State CurrentState { get; private set; } public int Score { get; private set; } public int Lives { get; private set; } public int Wave { get; private set; } private readonly List asteroids = new List(); private void Start() { ship.Bind(this); StartGame(); } private void StartGame() { ClearAsteroids(); Score = 0; Lives = startingLives; Wave = 1; CurrentState = State.Playing; ship.Respawn(respawnInvulnerability); SpawnWave(firstWaveCount); } private void SpawnWave(int count) { for (int i = 0; i < count; i++) { Vector3 position = RandomEdgePositionAwayFromShip(); SpawnAsteroid(position, Asteroid.Size.Large); } Debug.Log($"Wave {Wave} — {count} asteroids, Lives {Lives}, Score {Score}"); } private void SpawnAsteroid(Vector3 position, Asteroid.Size size) { Asteroid asteroid = Instantiate(asteroidPrefab, position, Quaternion.identity); Vector2 drift = Random.insideUnitCircle.normalized * SpeedFor(size); asteroid.Configure(this, size, drift, ScaleFor(size)); asteroids.Add(asteroid); } // Called by an asteroid when a bullet hits it. public void OnAsteroidHit(Asteroid asteroid) { asteroids.Remove(asteroid); Score += ScoreFor(asteroid.CurrentSize); // The split: spawn two of the next size down at the same spot. if (asteroid.CurrentSize != Asteroid.Size.Small) { Asteroid.Size next = asteroid.CurrentSize == Asteroid.Size.Large ? Asteroid.Size.Medium : Asteroid.Size.Small; SpawnAsteroid(asteroid.transform.position, next); SpawnAsteroid(asteroid.transform.position, next); } Destroy(asteroid.gameObject); if (asteroids.Count == 0) { Wave++; SpawnWave(firstWaveCount + Wave - 1); // each wave a little busier } } public void OnShipHit() { if (CurrentState != State.Playing) { return; } Lives--; Debug.Log($"Ship destroyed. Lives remaining: {Lives}"); if (Lives <= 0) { CurrentState = State.GameOver; Debug.Log($"Game over. Final score: {Score}"); StartGame(); } else { ship.Respawn(respawnInvulnerability); } } private Vector3 RandomEdgePositionAwayFromShip() { Camera cam = Camera.main; float halfHeight = cam.orthographicSize; float halfWidth = halfHeight * cam.aspect; Vector3 position; int guard = 0; do { position = new Vector3( Random.Range(-halfWidth, halfWidth), Random.Range(-halfHeight, halfHeight), 0f); guard++; } while (Vector3.Distance(position, ship.transform.position) < safeSpawnRadius && guard < 100); return position; } private void ClearAsteroids() { foreach (Asteroid asteroid in asteroids) { if (asteroid != null) { Destroy(asteroid.gameObject); } } asteroids.Clear(); } private float SpeedFor(Asteroid.Size size) => size switch { Asteroid.Size.Large => largeSpeed, Asteroid.Size.Medium => mediumSpeed, _ => smallSpeed, }; private float ScaleFor(Asteroid.Size size) => size switch { Asteroid.Size.Large => 1f, Asteroid.Size.Medium => 0.6f, _ => 0.35f, }; private int ScoreFor(Asteroid.Size size) => size switch { Asteroid.Size.Large => 20, Asteroid.Size.Medium => 50, _ => 100, }; }