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

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

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

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

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:

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

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:

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:

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:

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:

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:

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 Examples Gallery for the full list, or Building a Simple Game with the SimVX Editor to build games visually in the editor.