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

// GDnD wiki example
// Demonstrates: collision response against walls, paddle, and a brick layer, with AABB side detection — the step-2 Breakout skill
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-collider2d-and-triggers]], [[game-feel]]
 
using System;
using UnityEngine;
 
// Transform-driven ball, in the same explicit style as the Pong example, extended
// for Breakout: it reflects off the top/side walls, the paddle, and bricks, and
// reports a miss when it falls past the bottom. Bricks are found with a physics
// overlap query against a layer rather than by iterating the whole grid.
public class Ball2D : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float startSpeed = 9f;
    [SerializeField] private float speedGainPerHit = 0.15f;
    [SerializeField] private float maxSpeed = 18f;
 
    [Header("Collision")]
    [SerializeField] private Collider2D paddle;
    [Tooltip("Layer(s) the bricks live on. Set the brick prefab to this layer.")]
    [SerializeField] private LayerMask brickMask;
 
    [Header("Feel")]
    [SerializeField, Range(0f, 1f)] private float paddleEnglish = 0.8f;
 
    // Raised when the ball exits the bottom of the screen.
    public event Action Missed;
 
    private Vector2 velocity;
    private float halfSize;
    private Camera cachedCamera;
    private bool isLive;
 
    private void Awake()
    {
        cachedCamera = Camera.main;
        halfSize = GetComponent<SpriteRenderer>().bounds.extents.x;
    }
 
    // Launch upward with a small random horizontal lean. Called by the game controller.
    public void Serve()
    {
        transform.position = new Vector3(0f, -cachedCamera.orthographicSize * 0.5f, 0f);
        velocity = new Vector2(UnityEngine.Random.Range(-0.4f, 0.4f), 1f).normalized * startSpeed;
        isLive = true;
    }
 
    private void Update()
    {
        if (!isLive)
        {
            return;
        }
 
        transform.position += (Vector3)(velocity * Time.deltaTime);
        BounceOffWalls();
        BounceOffPaddle();
        BounceOffBricks();
        CheckForMiss();
    }
 
    private void BounceOffWalls()
    {
        float halfWidth = cachedCamera.orthographicSize * cachedCamera.aspect - halfSize;
        float topEdge = cachedCamera.orthographicSize - halfSize;
 
        if ((transform.position.x > halfWidth && velocity.x > 0f) ||
            (transform.position.x < -halfWidth && velocity.x < 0f))
        {
            velocity.x = -velocity.x;
        }
 
        if (transform.position.y > topEdge && velocity.y > 0f)
        {
            velocity.y = -velocity.y;
        }
    }
 
    private void BounceOffPaddle()
    {
        if (paddle == null || !paddle.bounds.Contains(transform.position) || velocity.y >= 0f)
        {
            return;
        }
 
        // Steer the bounce by where the ball struck the paddle, then send it upward.
        float offset = (transform.position.x - paddle.bounds.center.x) / paddle.bounds.extents.x;
        float speed = Mathf.Min(velocity.magnitude + speedGainPerHit, maxSpeed);
        velocity = new Vector2(offset * paddleEnglish, 1f).normalized * speed;
    }
 
    private void BounceOffBricks()
    {
        Collider2D hit = Physics2D.OverlapBox(transform.position, Vector2.one * (halfSize * 2f), 0f, brickMask);
        if (hit == null)
        {
            return;
        }
 
        Brick brick = hit.GetComponent<Brick>();
        if (brick == null)
        {
            return;
        }
 
        ReflectOffBox(hit.bounds);
        brick.Hit();
        velocity = velocity.normalized * Mathf.Min(velocity.magnitude + speedGainPerHit, maxSpeed);
    }
 
    // Classic AABB resolution: reflect whichever axis we have penetrated least,
    // i.e. the side we most recently crossed.
    private void ReflectOffBox(Bounds bounds)
    {
        float overlapX = (halfSize + bounds.extents.x) - Mathf.Abs(transform.position.x - bounds.center.x);
        float overlapY = (halfSize + bounds.extents.y) - Mathf.Abs(transform.position.y - bounds.center.y);
 
        if (overlapX < overlapY)
        {
            velocity.x = -velocity.x;
        }
        else
        {
            velocity.y = -velocity.y;
        }
    }
 
    private void CheckForMiss()
    {
        if (transform.position.y < -(cachedCamera.orthographicSize + halfSize))
        {
            isLive = false;
            Missed?.Invoke();
        }
    }
}