Source file unity-csharp/arcade-classics/asteroids/ShipController.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: inertial vector movement (rotate + thrust + drag), firing along a heading, and asteroid collision
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-input]], [[steering-behaviours]]
using UnityEngine;
// The first ship in the route with momentum. Thrust adds to a velocity vector
// rather than setting position directly, so the ship drifts and turns are about
// managing inertia. Pair with ScreenWrap2D so it wraps at the edges.
public class ShipController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float rotationSpeed = 200f; // degrees per second
[SerializeField] private float thrustPower = 12f;
[SerializeField] private float maxSpeed = 9f;
[Tooltip("Fraction of speed shed per second when not thrusting (gentle space friction).")]
[SerializeField, Range(0f, 5f)] private float drag = 0.4f;
[Header("Firing")]
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private float bulletSpeed = 12f;
[SerializeField] private float fireCooldown = 0.25f;
[SerializeField] private LayerMask asteroidMask;
[Header("Collision")]
[SerializeField] private float collisionRadius = 0.4f;
private AsteroidsGame game;
private Vector2 velocity;
private float cooldownTimer;
private float invulnerableTimer;
public void Bind(AsteroidsGame owner)
{
game = owner;
}
// Reset to a clean state at the centre after a death.
public void Respawn(float invulnerableSeconds)
{
transform.position = Vector3.zero;
transform.rotation = Quaternion.identity;
velocity = Vector2.zero;
invulnerableTimer = invulnerableSeconds;
}
private void Update()
{
Rotate();
Thrust();
Integrate();
TickFire();
TickCollision();
}
private void Rotate()
{
float turn = Input.GetAxisRaw("Horizontal"); // A/D or left/right
transform.Rotate(0f, 0f, -turn * rotationSpeed * Time.deltaTime);
}
private void Thrust()
{
// Only forward thrust (transform.up is the ship's nose). No reverse — classic feel.
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow))
{
velocity += (Vector2)transform.up * (thrustPower * Time.deltaTime);
}
else
{
velocity *= Mathf.Clamp01(1f - drag * Time.deltaTime);
}
velocity = Vector2.ClampMagnitude(velocity, maxSpeed);
}
private void Integrate()
{
transform.position += (Vector3)(velocity * Time.deltaTime);
}
private void TickFire()
{
cooldownTimer -= Time.deltaTime;
if (cooldownTimer > 0f || !Input.GetKey(KeyCode.Space) || bulletPrefab == null)
{
return;
}
cooldownTimer = fireCooldown;
Bullet bullet = Instantiate(bulletPrefab, transform.position + transform.up * 0.5f, Quaternion.identity);
bullet.Launch(transform.up, bulletSpeed, asteroidMask);
}
private void TickCollision()
{
if (invulnerableTimer > 0f)
{
invulnerableTimer -= Time.deltaTime;
return;
}
if (Physics2D.OverlapCircle(transform.position, collisionRadius, asteroidMask) != null)
{
game?.OnShipHit();
}
}
}