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