Skip to main content

Overview

Space Pong uses Godot’s physics-based collision system to handle interactions between the ball, paddle, and walls. The collision system is built on CharacterBody2D nodes and the move_and_collide() method, providing precise collision detection and realistic bounce physics.

Collision Architecture

Both the ball and player paddle are implemented as CharacterBody2D nodes:
extends CharacterBody2D
CharacterBody2D is designed for kinematic bodies that need precise movement control and collision detection. Unlike RigidBody2D, it doesn’t use Godot’s physics engine for automatic forces, giving developers full control over movement behavior.

The move_and_collide Method

Both game objects use move_and_collide() for collision-aware movement:
var collision = move_and_collide(velocity * delta)
if collision != null:
	velocity = velocity.bounce(collision.get_normal()) * incremental_speed

How move_and_collide Works

1

Attempt Movement

The method tries to move the body by the specified vector (velocity × delta)
2

Detect Collision

If the movement would result in a collision with another physics body, the method detects it
3

Stop at Contact

The body moves only until it touches the colliding object, preventing overlap
4

Return Collision Data

If a collision occurred, returns a KinematicCollision2D object with collision details. Returns null if no collision

Frame-Rate Independent

Multiplying by delta ensures consistent movement regardless of frame rate

Precise Detection

Detects the exact moment of contact without object overlap

No Tunneling

Prevents fast-moving objects from passing through thin walls

Manual Response

Returns collision data but lets you decide how to respond

Ball Collision Response

The ball implements sophisticated collision handling in scenes/ball.gd:
if started:
	var collision = move_and_collide(velocity * delta)
	if collision != null:
		velocity = velocity.bounce(collision.get_normal()) * incremental_speed
		print(velocity)

Collision Detection Check

var collision = move_and_collide(velocity * delta)
if collision != null:
The collision variable holds:
  • null if no collision occurred (ball moved freely)
  • KinematicCollision2D object if a collision occurred
Checking collision != null determines whether to apply bounce physics.

Getting the Collision Normal

collision.get_normal()
The collision normal is a Vector2 that points perpendicular to the collision surface:

Top Wall

Normal: Vector2(0, 1) pointing downward

Bottom Wall

Normal: Vector2(0, -1) pointing upward

Left Wall

Normal: Vector2(1, 0) pointing right

Right Wall

Normal: Vector2(-1, 0) pointing left
The normal always points away from the surface into the space where the collision occurred. For a ball hitting a wall, the normal points from the wall toward the ball.

Vector Bounce Physics

The core of the collision response is the bounce() method:
velocity = velocity.bounce(collision.get_normal()) * incremental_speed

How Vector2.bounce() Works

The bounce() method reflects a vector across a surface defined by its normal:
velocity.bounce(normal)
Mathematically, this implements reflection:
reflected = velocity - 2 * (velocity · normal) * normal
Where · represents the dot product.

Bounce Examples

// Ball moving down-right hits bottom wall
velocity = Vector2(300, 400)
normal = Vector2(0, -1)  // Wall normal points up

// After bounce:
velocity = Vector2(300, -400)  // X unchanged, Y flipped
// Ball now moving up-right

Speed Multiplication

After computing the bounce direction, the velocity is multiplied by the speed increase factor:
velocity = velocity.bounce(collision.get_normal()) * incremental_speed
With incremental_speed = 1.05, this increases the ball’s speed by 5% per collision:
// Example progression
Initial velocity magnitude: 559 px/s  // sqrt(500² + 250²)
After 1st collision:        587 px/s  // 559 × 1.05
After 2nd collision:        616 px/s  // 559 × 1.05²
After 5th collision:        713 px/s  // 559 × 1.05
After 10th collision:       911 px/s  // 559 × 1.05¹⁰
Because multiplication applies to the entire velocity vector, both X and Y components increase proportionally. The angle of movement stays the same, but the ball moves faster in that direction.

Complete Collision Flow

Collision Detection Timing

The collision system only runs when the game has started:
if started:
	var collision = move_and_collide(velocity * delta)
This means:

Before Game Start

No collision detection occurs, the ball is stationary with velocity = Vector2.ZERO

After Game Start

Collision detection runs every physics frame (typically 60 times per second)

Player Collision Behavior

The player paddle has simpler collision handling:
move_and_collide(velocity * delta)
Key differences from ball collision:
  • No return value used: The paddle doesn’t check for collisions
  • No bounce: The paddle simply stops when hitting an obstacle
  • No speed changes: Paddle speed remains constant

Why Paddle Collisions Are Simpler

The paddle only needs to:
  1. Move in response to input
  2. Stop at screen boundaries or walls
  3. Act as a solid surface for the ball to bounce off
Since the paddle never bounces or accelerates from collisions, it doesn’t need to process collision responses.

Collision Layers and Masks

While not visible in the code, collision layers and masks are configured in the Godot editor on each CharacterBody2D node. These determine which objects can collide with each other.
Typical configuration:
  • Ball: Collides with walls, paddle, and goals
  • Paddle: Collides with walls and ball
  • Walls: Collide with ball and paddle
  • Goals: Collide only with ball (to detect scoring)

Performance Characteristics

Efficient Detection

move_and_collide() uses Godot’s optimized collision detection, handling complex shapes efficiently

Per-Frame Cost

Each collision check runs 60 times per second, but Godot’s physics engine is highly optimized for this

Single Collision

move_and_collide() only detects the first collision along the movement path

No Continuous Detection

Very fast objects might theoretically tunnel through thin objects, though this is rare with proper frame rates

Debugging Collisions

The ball script includes debug output:
print(velocity)
This prints the velocity vector after each collision, useful for:
  • Verifying bounce angles are correct
  • Monitoring speed increases
  • Debugging unexpected behavior
  • Understanding collision patterns
Example output:
(456.3, -523.8)
(479.1, -549.9)
(-502.8, -577.4)
(527.9, 606.2)
Remember to remove or comment out debug print statements in production builds to avoid performance overhead and console clutter.

Physics Properties

Beyond the code, collision behavior is also affected by properties set in the Godot editor:
  • Collision Shape: Defines the physical boundary (typically CircleShape2D for ball, RectangleShape2D for paddle)
  • Collision Layer: Which layer this object exists on (1-32)
  • Collision Mask: Which layers this object can collide with (1-32)
  • Physics Material: Optional material defining friction and bounce (not used in this implementation)

Advanced Collision Scenarios

Multiple Collisions in One Frame

move_and_collide() only handles the first collision. If the ball should collide with multiple objects in a single frame:
// Current implementation (handles first collision only)
var collision = move_and_collide(velocity * delta)

// Alternative for multiple collisions
var remaining_movement = velocity * delta
for i in range(max_collisions):
	var collision = move_and_collide(remaining_movement)
	if collision == null:
		break
	velocity = velocity.bounce(collision.get_normal())
	remaining_movement = velocity * delta * (1.0 - collision.get_travel().length() / remaining_movement.length())

Corner Collisions

When the ball hits a corner exactly, the normal might be ambiguous. Godot handles this by providing the normal of the first surface detected.

Build docs developers (and LLMs) love