Pong¶
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
scoredwhen 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
scoredsignal when it exits left or rightPongGame (root) creates paddles and ball in
ready(), connects thescoredsignal to update the score, and handles paddle-ball collision inprocess()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())