Pong

Play Demo

Build a classic Pong game demonstrating input actions, collision detection, signals, and game state management.

What You Will Learn

  • Input actions – Bind keys to named actions with InputMap.add_action()

  • Input.get_strength() – Read analogue input strength for smooth movement

  • Signals – Decouple game events (the ball emits scored when it passes a paddle)

  • Collision – Manual AABB overlap for paddle-ball bouncing

  • Game state – Track and display scores

Controls

Key

Action

W / S

Left paddle up / down

Up / Down

Right paddle up / down

How It Works

Three node types compose the game:

  • Paddle reads two input actions (up/down) and clamps position to the screen

  • Ball moves at a velocity, bounces off top/bottom edges, and emits a scored signal when it exits left or right

  • PongGame (root) creates paddles and ball in ready(), connects the scored signal to update the score, and handles paddle-ball collision in process() by reflecting the ball’s velocity based on where it hits the paddle

Input actions are registered in __main__ via InputMap.add_action(), mapping physical keys (Key.W, Key.S, Key.UP, Key.DOWN) to named actions that the paddle nodes query each frame.

Source Code

  1#!/usr/bin/env python3
  2"""Pong -- Complete Two-Player Game
  3
  4Build a classic Pong game demonstrating input actions, collision detection,
  5signals, and game state management.
  6
  7## What You Will Learn
  8
  9- **Input actions** -- Bind keys to named actions with `InputMap.add_action()`
 10- **Input.get_strength()** -- Read analogue input strength for smooth movement
 11- **Signals** -- Decouple game events (the ball emits `scored` when it passes a paddle)
 12- **Collision** -- Manual AABB overlap for paddle-ball bouncing
 13- **Game state** -- Track and display scores
 14
 15## Controls
 16
 17| Key | Action |
 18|-----|--------|
 19| W / S | Left paddle up / down |
 20| Up / Down | Right paddle up / down |
 21
 22## How It Works
 23
 24Three node types compose the game:
 25
 26- **Paddle** reads two input actions (up/down) and clamps position to the screen
 27- **Ball** moves at a velocity, bounces off top/bottom edges, and emits a
 28  `scored` signal when it exits left or right
 29- **PongGame** (root) creates paddles and ball in `ready()`, connects the
 30  `scored` signal to update the score, and handles paddle-ball collision in
 31  `process()` by reflecting the ball's velocity based on where it hits the paddle
 32
 33Input actions are registered in `__main__` via `InputMap.add_action()`, mapping
 34physical keys (`Key.W`, `Key.S`, `Key.UP`, `Key.DOWN`) to named actions that
 35the paddle nodes query each frame.
 36"""
 37
 38import math
 39import random
 40
 41from simvx.core import Input, InputMap, Key, Node2D, Property, Signal, Vec2
 42from simvx.graphics import App
 43
 44WIDTH, HEIGHT = 800, 600
 45PADDLE_W, PADDLE_H = 12, 80
 46BALL_R = 8
 47
 48
 49class Paddle(Node2D):
 50    speed = Property(400.0, range=(100, 800))
 51    half_h = PADDLE_H // 2
 52
 53    def __init__(self, up_action: str, down_action: str, **kwargs):
 54        super().__init__(**kwargs)
 55        self.up_action = up_action
 56        self.down_action = down_action
 57
 58    def process(self, dt: float):
 59        dy = Input.get_strength(self.down_action) - Input.get_strength(self.up_action)
 60        self.position.y = max(self.half_h, min(HEIGHT - self.half_h, self.position.y + dy * self.speed * dt))
 61
 62    def draw(self, renderer):
 63        x, y = self.position.x - PADDLE_W // 2, self.position.y - self.half_h
 64        renderer.draw_rect((x, y), (PADDLE_W, PADDLE_H), colour=(1.0, 1.0, 1.0, 1.0), filled=True)
 65
 66
 67class Ball(Node2D):
 68    speed = Property(350.0, range=(200, 600))
 69
 70    def __init__(self, **kwargs):
 71        super().__init__(**kwargs)
 72        self.velocity = Vec2()
 73        self.scored = Signal()  # emits side: "left" or "right"
 74        self.reset()
 75
 76    def reset(self):
 77        self.position = Vec2(WIDTH / 2, HEIGHT / 2)
 78        angle = random.choice([-1, 1]) * random.uniform(-math.pi / 4, math.pi / 4)
 79        direction = random.choice([-1, 1])
 80        self.velocity = Vec2(math.cos(angle) * direction, math.sin(angle)) * self.speed
 81
 82    def process(self, dt: float):
 83        self.position += self.velocity * dt
 84
 85        # Top/bottom bounce
 86        if self.position.y < BALL_R:
 87            self.position.y = BALL_R
 88            self.velocity.y = abs(self.velocity.y)
 89        elif self.position.y > HEIGHT - BALL_R:
 90            self.position.y = HEIGHT - BALL_R
 91            self.velocity.y = -abs(self.velocity.y)
 92
 93        # Score detection
 94        if self.position.x < 0:
 95            self.scored.emit("right")
 96            self.reset()
 97        elif self.position.x > WIDTH:
 98            self.scored.emit("left")
 99            self.reset()
100
101    def draw(self, renderer):
102        renderer.draw_circle(self.position, BALL_R, colour=(1.0, 1.0, 1.0, 1.0))
103
104
105class PongGame(Node2D):
106    def ready(self):
107        InputMap.add_action("p1_up", [Key.W])
108        InputMap.add_action("p1_down", [Key.S])
109        InputMap.add_action("p2_up", [Key.UP])
110        InputMap.add_action("p2_down", [Key.DOWN])
111
112        self.left_paddle = self.add_child(Paddle("p1_up", "p1_down", name="Left", position=Vec2(30, HEIGHT / 2)))
113        self.right_paddle = self.add_child(
114            Paddle("p2_up", "p2_down", name="Right", position=Vec2(WIDTH - 30, HEIGHT / 2))
115        )
116        self.ball = self.add_child(Ball(name="Ball"))
117        self.scores = [0, 0]
118
119        self.ball.scored.connect(self._on_scored)
120
121    def _on_scored(self, side: str):
122        self.scores[0 if side == "left" else 1] += 1
123
124    def process(self, dt: float):
125        # Paddle-ball collision
126        for paddle in (self.left_paddle, self.right_paddle):
127            dx = abs(self.ball.position.x - paddle.position.x)
128            dy = abs(self.ball.position.y - paddle.position.y)
129            if dx < PADDLE_W / 2 + BALL_R and dy < PADDLE_H / 2 + BALL_R:
130                # Reflect and slightly speed up
131                direction = 1.0 if paddle is self.left_paddle else -1.0
132                offset = (self.ball.position.y - paddle.position.y) / (PADDLE_H / 2)
133                angle = offset * math.pi / 3
134                speed = self.ball.velocity.length() * 1.05
135                self.ball.velocity = Vec2(math.cos(angle) * direction, math.sin(angle)) * speed
136                # Push ball out of paddle
137                self.ball.position = Vec2(
138                    paddle.position.x + direction * (PADDLE_W / 2 + BALL_R + 1),
139                    self.ball.position.y,
140                )
141
142    def draw(self, renderer):
143        # Center line
144        for y in range(0, HEIGHT, 20):
145            renderer.draw_rect((WIDTH // 2 - 1, y), (2, 10), colour=(0.31, 0.31, 0.31), filled=True)
146
147        # Scores
148        renderer.draw_text(str(self.scores[0]), (WIDTH // 2 - 60, 20), scale=4, colour=(1.0, 1.0, 1.0))
149        renderer.draw_text(str(self.scores[1]), (WIDTH // 2 + 40, 20), scale=4, colour=(1.0, 1.0, 1.0))
150
151        # Controls hint
152        renderer.draw_text("W/S", (10, HEIGHT - 20), scale=1, colour=(0.39, 0.39, 0.39))
153        renderer.draw_text("Up/Down", (WIDTH - 80, HEIGHT - 20), scale=1, colour=(0.39, 0.39, 0.39))
154
155
156if __name__ == "__main__":
157    App(title="Pong", width=WIDTH, height=HEIGHT).run(PongGame())