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();
        }
    }
}