# Pong Complete two-player game in ~150 lines. ```{raw} html ▶ Run in browser ``` **Tags:** `tutorial` `game` `input-actions` `signals` `collision` # Pong This is the capstone of the basics track: a complete two-player game in about 150 lines that puts together everything so far. Input actions move the paddles, a signal reports scoring, and manual collision makes the ball bounce. Player 1 uses **W/S**, player 2 uses the **arrow keys**. ## 1. The paddle reads named actions Each `Paddle` is told the names of its two actions and moves on the signed axis between them, clamped to the window. The same class drives both players: the bindings differ, the code does not. ```python class Paddle(Node2D): speed = Property(400.0, range=(100, 800)) def __init__(self, up_action, down_action, **kwargs): super().__init__(**kwargs) self.up_action, self.down_action = up_action, down_action def on_update(self, dt): dy = Input.get_strength(self.down_action) - Input.get_strength(self.up_action) self.position.y = max(self.half_h, min(HEIGHT - self.half_h, self.position.y + dy * self.speed * dt)) ``` ## 2. The ball moves, bounces, and announces scoring The `Ball` integrates its velocity, reflects off the top and bottom walls, and **emits a `scored` signal** when it leaves the left or right edge, then resets to the centre with a fresh random angle. It does not touch the score itself. ```python class Ball(Node2D): def __init__(self, **kwargs): super().__init__(**kwargs) self.scored = Signal() self.reset() def on_update(self, dt): self.position += self.velocity * dt # reflect off top/bottom ... if self.position.x < 0: self.scored.emit("right"); self.reset() elif self.position.x > WIDTH: self.scored.emit("left"); self.reset() ``` ## 3. The root wires it together `PongGame` declares its `input_actions` at class scope (the web-safe registration path you saw in *Input and Movement*), builds the two paddles and the ball in `on_ready()`, and connects the ball's `scored` signal to a handler that bumps the score. The ball and the score never reference each other. ```python class PongGame(Node2D): input_actions = { "p1_up": [Key.W], "p1_down": [Key.S], "p2_up": [Key.UP], "p2_down": [Key.DOWN], } def on_ready(self): self.left_paddle = self.add_child(Paddle("p1_up", "p1_down", position=Vec2(30, HEIGHT/2))) self.right_paddle = self.add_child(Paddle("p2_up", "p2_down", position=Vec2(WIDTH-30, HEIGHT/2))) self.ball = self.add_child(Ball()) self.scores = [0, 0] self.ball.scored.connect(self._on_scored) ``` ## 4. Collision lives in the parent The root checks paddle-ball overlap each frame with a simple AABB test. On a hit it reflects the ball, and varies the bounce **angle by where the ball struck the paddle** so players can aim: ```python def on_update(self, dt): for paddle in (self.left_paddle, self.right_paddle): if abs(self.ball.position.x - paddle.position.x) < PADDLE_W/2 + BALL_R and \ abs(self.ball.position.y - paddle.position.y) < PADDLE_H/2 + BALL_R: offset = (self.ball.position.y - paddle.position.y) / (PADDLE_H/2) angle = offset * math.pi/3 # ... set ball.velocity from angle + a small speed-up ``` > For a physics-driven game, use `CharacterBody2D` + `CollisionShape2D` and > `move_and_slide(dt)` instead of hand-rolled AABB. Manual collision keeps this > tutorial's moving parts visible. ## 5. Draw the board `on_draw` paints the centre line, both scores, and the control hints. Because the scores are plain state updated by the signal handler, drawing them is just reading `self.scores`. ## Run it ```bash uv run python examples/tutorials/pong/main.py ``` ## What's next - **Monolith to Composed** -- refactor a single big node into clean, reusable nodes. - Browse the **feature references** for cameras, tilemaps, particles, audio, and more. ## Source ```{literalinclude} ../../examples/tutorials/pong/main.py :language: python :linenos: ```