Source file unity-csharp/arcade-classics/pong/Ball2D.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: continuous movement, manual reflection off walls and paddles, and edge scoring — the step-1 Pong skills
// Related pages: [[overview-arcade-classics-as-learning-projects]], [[unity-collider2d-and-triggers]], [[monobehaviour-lifecycle]], [[game-feel]]
using System;
using UnityEngine;
// Transform-driven ball. No Rigidbody is needed: the whole point of Pong as a
// first project is to make collision and reflection explicit rather than hand
// them to the physics engine. Attach to a small square sprite.
public class Ball2D : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float startSpeed = 8f;
[SerializeField] private float speedGainPerHit = 0.35f;
[SerializeField] private float maxSpeed = 18f;
[Header("Paddles")]
[Tooltip("Colliders the ball reflects off. Usually the two paddle BoxCollider2Ds.")]
[SerializeField] private Collider2D[] paddles;
[Header("Feel")]
[Tooltip("How strongly the contact point on the paddle angles the bounce (0 = pure mirror).")]
[SerializeField, Range(0f, 1f)] private float paddleEnglish = 0.75f;
// Raised with +1 (ball exited right → left player scores) or -1 (left player's goal).
public event Action<int> Scored;
private Vector2 velocity;
private float halfSize;
private Camera cachedCamera;
private void Awake()
{
cachedCamera = Camera.main;
halfSize = GetComponent<SpriteRenderer>().bounds.extents.y;
}
// Called by the match controller to (re)serve toward a given horizontal direction.
public void Serve(int horizontalDirection)
{
float angle = UnityEngine.Random.Range(-0.5f, 0.5f); // gentle vertical spread
velocity = new Vector2(Mathf.Sign(horizontalDirection), angle).normalized * startSpeed;
transform.position = Vector3.zero;
}
private void Update()
{
transform.position += (Vector3)(velocity * Time.deltaTime);
BounceOffWalls();
BounceOffPaddles();
CheckForScore();
}
// Reflect the y component when the ball passes the top/bottom of the screen.
private void BounceOffWalls()
{
float topEdge = cachedCamera.orthographicSize - halfSize;
if (transform.position.y > topEdge && velocity.y > 0f)
{
velocity.y = -velocity.y;
}
else if (transform.position.y < -topEdge && velocity.y < 0f)
{
velocity.y = -velocity.y;
}
}
private void BounceOffPaddles()
{
if (paddles == null)
{
return;
}
foreach (Collider2D paddle in paddles)
{
if (paddle == null || !paddle.bounds.Contains(transform.position))
{
continue;
}
// Only reflect if we are heading into the paddle, so the ball cannot
// get "stuck" reversing every frame while overlapping.
bool movingIntoPaddle = Mathf.Sign(velocity.x) != Mathf.Sign(transform.position.x - paddle.bounds.center.x);
if (!movingIntoPaddle)
{
continue;
}
// Where on the paddle did we hit? -1 = bottom edge, +1 = top edge.
float offset = (transform.position.y - paddle.bounds.center.y) / paddle.bounds.extents.y;
float speed = Mathf.Min(velocity.magnitude + speedGainPerHit, maxSpeed);
Vector2 direction = new Vector2(-Mathf.Sign(velocity.x), offset * paddleEnglish).normalized;
velocity = direction * speed;
return;
}
}
private void CheckForScore()
{
float sideEdge = cachedCamera.orthographicSize * cachedCamera.aspect + halfSize;
if (transform.position.x > sideEdge)
{
Scored?.Invoke(+1);
}
else if (transform.position.x < -sideEdge)
{
Scored?.Invoke(-1);
}
}
}