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