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