// 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 Scored; private Vector2 velocity; private float halfSize; private Camera cachedCamera; private void Awake() { cachedCamera = Camera.main; halfSize = GetComponent().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); } } }