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 platformsgame_asteroids2d.py– Classic arcade game with wrap-around physicsgame_spaceinvaders2d.py– Rows of enemies, bullets, and wave progression2d_sprite.py– PNG textures as 2D quads2d_tilemap.py– GPU-rendered tilemap with camera panning2d_light.py– Point lights with shadow-casting occluders2d_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.