# Your First 2D Game Build a playable Pong game from scratch in 7 steps. Each section adds to the same project. ## 1. A Window and a Paddle ```python from simvx.core import Node2D, Vec2 from simvx.graphics import App WIDTH, HEIGHT = 800, 600 class Paddle(Node2D): def draw(self, renderer): renderer.draw_rect((self.position.x - 6, self.position.y - 40), (12, 80), colour=(255, 255, 255)) App(title="Pong", width=WIDTH, height=HEIGHT).run(Paddle(position=Vec2(30, HEIGHT / 2))) ``` `Node2D` is the base for all 2D game objects. Override `draw()` to render -- the `renderer` provides `draw_rect()`, `draw_circle()`, and `draw_text()`. `App` creates a Vulkan window and runs the game loop. ## 2. Move the Paddle ```python from simvx.core import Node2D, Vec2, InputMap, Key, Input, Property from simvx.graphics import App WIDTH, HEIGHT = 800, 600 HALF_H = 40 class Paddle(Node2D): speed = Property(400.0, range=(100, 800)) def process(self, dt: float): dy = Input.get_strength("down") - Input.get_strength("up") new_y = max(HALF_H, min(HEIGHT - HALF_H, self.position.y + dy * self.speed * dt)) self.position = Vec2(self.position.x, new_y) def draw(self, renderer): renderer.draw_rect((self.position.x - 6, self.position.y - HALF_H), (12, 80), colour=(255, 255, 255)) InputMap.add_action("up", [Key.W, Key.UP]) InputMap.add_action("down", [Key.S, Key.DOWN]) App(title="Pong", width=WIDTH, height=HEIGHT).run(Paddle(position=Vec2(30, HEIGHT / 2))) ``` `Property` declares editor-visible, serializable values with optional range validation. `InputMap.add_action()` binds named actions to `Key` enums. `Input.get_strength()` returns 0.0-1.0 for digital keys. `process(dt)` runs every frame -- `dt` is the delta time in seconds. ## 3. Add a Ball ```python class Ball(Node2D): speed = Property(350.0, range=(200, 600)) def __init__(self, **kwargs): super().__init__(**kwargs) self.velocity = Vec2(self.speed, self.speed * 0.5) def process(self, dt: float): self.position += self.velocity * dt def draw(self, renderer): renderer.draw_circle(self.position, 8, colour=(255, 255, 255)) ``` Add it as a child in a root node's `ready()`: ```python class PongGame(Node2D): def ready(self): self.paddle = self.add_child(Paddle(name="Paddle", position=Vec2(30, HEIGHT / 2))) self.ball = self.add_child(Ball(name="Ball", position=Vec2(WIDTH / 2, HEIGHT / 2))) ``` `add_child()` attaches nodes to the scene tree. Children inherit their parent's coordinate space and are processed automatically. ## 4. Bounce and Collide Add wall bouncing and paddle collision to the ball: ```python class Ball(Node2D): speed = Property(350.0, range=(200, 600)) def __init__(self, **kwargs): super().__init__(**kwargs) self.velocity = Vec2(self.speed, self.speed * 0.5) def process(self, dt: float): self.position += self.velocity * dt # Bounce off top/bottom walls if self.position.y < 8 or self.position.y > HEIGHT - 8: self.velocity = Vec2(self.velocity.x, -self.velocity.y) def draw(self, renderer): renderer.draw_circle(self.position, 8, colour=(255, 255, 255)) ``` Check paddle collision in the parent's `process()`: ```python def process(self, dt: float): bx, by = self.ball.position.x, self.ball.position.y px, py = self.paddle.position.x, self.paddle.position.y if abs(bx - px) < 14 and abs(by - py) < 48: self.ball.velocity = Vec2(abs(self.ball.velocity.x), self.ball.velocity.y) ``` For production games, use `CharacterBody2D` with `CollisionShape2D` and `move_and_slide(dt)` -- see `game_platformer.py`. ## 5. Score and Signals `Signal` provides decoupled event communication: ```python from simvx.core import Signal class Ball(Node2D): def __init__(self, **kwargs): super().__init__(**kwargs) self.scored = Signal() # emits when ball passes a paddle # ... def process(self, dt: float): self.position += self.velocity * dt if self.position.x < 0: self.scored.emit("right") self.reset() elif self.position.x > WIDTH: self.scored.emit("left") self.reset() ``` Connect signals in the parent: ```python class PongGame(Node2D): def ready(self): # ... add paddle, ball ... self.scores = [0, 0] self.ball.scored.connect(self._on_scored) def _on_scored(self, side: str): self.scores[0 if side == "left" else 1] += 1 def draw(self, renderer): renderer.draw_text(str(self.scores[0]), (WIDTH // 2 - 60, 20), scale=4, colour=(255, 255, 255)) renderer.draw_text(str(self.scores[1]), (WIDTH // 2 + 40, 20), scale=4, colour=(255, 255, 255)) ``` ## 6. Polish Add a tween on score and a timer for serve delay: ```python from simvx.core import tween, Timer from simvx.core.animation.tween import ease_out_elastic class PongGame(Node2D): def ready(self): # ... setup ... self.scale_factor = 1.0 self.ball.scored.connect(self._on_scored) def _on_scored(self, side: str): self.scores[0 if side == "left" else 1] += 1 # Punch the score text self.start_coroutine(tween(self, "scale_factor", 1.5, duration=0.1)) self.start_coroutine(tween(self, "scale_factor", 1.0, duration=0.3, easing=ease_out_elastic)) ``` `tween()` animates any property over time with optional easing. `Timer` fires a signal after a delay: ```python timer = Timer(duration=1.0, one_shot=True, autostart=True) timer.timeout.connect(self.serve_ball) self.add_child(timer) ``` ## 7. Next Steps The complete game is at `packages/graphics/examples/game_pong.py` -- run it with: ```bash uv run python packages/graphics/examples/game_pong.py ``` More 2D examples to explore: - `game_platformer.py` -- CharacterBody2D with gravity, jump, and platforms - `game_asteroids2d.py` -- Classic arcade game with wrap-around physics - `game_spaceinvaders2d.py` -- Rows of enemies, bullets, and wave progression - `2d_sprite.py` -- PNG textures as 2D quads - `2d_tilemap.py` -- GPU-rendered tilemap with camera panning - `2d_light.py` -- Point lights with shadow-casting occluders - `2d_navigation.py` -- A* pathfinding on a grid See {doc}`examples` for the full list, or {doc}`editor_tutorial` to build games visually in the editor.