Source file unity-csharp/arcade-classics/asteroids/AsteroidsGame.cs from the GDnD code examples. Download raw file

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