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