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
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())