Platformer

CharacterBody2D with gravity, jump, and platforms.

▶ Run in browser

Tags: game character-body gravity input-actions

Demonstrates: CharacterBody2D, RectangleShape2D, move_and_slide, is_on_floor, gravity, input actions, static-body platform collision.

This is a SCREEN-SPACE (Y-down) platformer: gravity pulls toward +Y and the character’s up_direction is flipped to -Y so “up” on screen is up for the slope/floor classifier. Static platforms are PhysicsBody2D(mode=STATIC) the character collides with and slides along.

Controls: A/D or Left/Right = move, Space = jump. Mouse/touch: hold the left or right half of the screen to move, press the top half to jump.

Source

  1#!/usr/bin/env python3
  2"""Platformer: CharacterBody2D with gravity, jump, and platforms.
  3
  4# /// simvx
  5# tags = ["game", "character-body", "gravity", "input-actions"]
  6# web = { root = "PlatformerDemo" }
  7# ///
  8
  9Demonstrates: CharacterBody2D, RectangleShape2D, move_and_slide, is_on_floor,
 10              gravity, input actions, static-body platform collision.
 11
 12This is a SCREEN-SPACE (Y-down) platformer: gravity pulls toward +Y and the
 13character's ``up_direction`` is flipped to -Y so "up" on screen is up for the
 14slope/floor classifier. Static platforms are ``PhysicsBody2D(mode=STATIC)`` the
 15character collides with and slides along.
 16
 17Controls: A/D or Left/Right = move, Space = jump. Mouse/touch: hold the left
 18or right half of the screen to move, press the top half to jump.
 19"""
 20
 21from simvx.core import (
 22    BodyMode,
 23    CharacterBody2D,
 24    Input,
 25    InputMap,
 26    Key,
 27    MouseButton,
 28    Node2D,
 29    PhysicsBody2D,
 30    PhysicsRoot2D,
 31    Property,
 32    RectangleShape2D,
 33    Vec2,
 34)
 35from simvx.graphics import App
 36
 37WIDTH, HEIGHT = 800, 600
 38GRAVITY = 800.0
 39# Screen-space (Y-down) world: gravity is +Y, "up" for the character is -Y.
 40SCREEN_UP = Vec2(0.0, -1.0)
 41
 42
 43class Platform(PhysicsBody2D):
 44    """Static platform that the character collides with and slides along."""
 45
 46    w = Property(120.0, range=(20, 400))
 47    h = Property(16.0, range=(8, 60))
 48
 49    def __init__(self, w: float = 120, h: float = 16, **kwargs):
 50        super().__init__(mode=BodyMode.STATIC, shape=RectangleShape2D(half_extents=Vec2(w / 2, h / 2)), **kwargs)
 51        self.w = w
 52        self.h = h
 53
 54    def on_draw(self, renderer):
 55        x = self.position.x - self.w / 2
 56        y = self.position.y - self.h / 2
 57        renderer.draw_rect((x, y), (self.w, self.h), colour=(0.39, 0.71, 0.31, 1.0), filled=True)
 58
 59
 60class Player(CharacterBody2D):
 61    speed = Property(250.0, range=(100, 500))
 62    jump_force = Property(450.0, range=(200, 800))
 63
 64    def __init__(self, **kwargs):
 65        super().__init__(shape=RectangleShape2D(half_extents=Vec2(10.0, 14.0)), **kwargs)
 66        self.up_direction = SCREEN_UP  # screen-space: "up" is -Y
 67        self.can_jump = True
 68
 69    def on_update(self, dt: float):
 70        # Horizontal movement (keyboard)
 71        move = Input.get_strength("right") - Input.get_strength("left")
 72        jump = Input.is_action_just_pressed("jump")
 73
 74        # Pointer/touch (touch arrives as MouseButton.LEFT on web): hold the
 75        # left/right half of the screen to move, press the top half to jump.
 76        if Input.is_mouse_button_pressed(MouseButton.LEFT):
 77            win_w, win_h = self.app.width, self.app.height
 78            pointer = Input.mouse_position
 79            move += -1.0 if pointer.x < win_w / 2 else 1.0
 80            if Input.is_mouse_button_just_pressed(MouseButton.LEFT) and pointer.y < win_h / 2:
 81                jump = True
 82        vx = max(-1.0, min(1.0, move)) * self.speed
 83
 84        # Gravity (toward +Y on screen). Reset downward fall when grounded.
 85        vy = self.velocity.y
 86        if self.is_on_floor() and vy > 0.0:
 87            vy = 0.0
 88        vy += GRAVITY * dt
 89
 90        # Jump (a -Y, upward-on-screen kick)
 91        if jump and self.is_on_floor():
 92            vy = -self.jump_force
 93
 94        self.velocity = Vec2(vx, vy)
 95        self.move_and_slide(dt)
 96
 97        # Reset if fallen off screen
 98        if self.position.y > HEIGHT + 50:
 99            self.position = Vec2(WIDTH / 2, 100)
100            self.velocity = Vec2()
101
102    def on_draw(self, renderer):
103        px, py = self.position.x, self.position.y
104        renderer.draw_rect((px - 10, py - 14), (20, 28), colour=(0.24, 0.55, 1.0, 1.0), filled=True)
105        # Eyes
106        renderer.draw_rect((px - 5, py - 10), (4, 4), colour=(1.0, 1.0, 1.0, 1.0), filled=True)
107        renderer.draw_rect((px + 1, py - 10), (4, 4), colour=(1.0, 1.0, 1.0, 1.0), filled=True)
108
109
110class PlatformerDemo(Node2D):
111    def on_ready(self):
112        InputMap.add_action("left", [Key.A, Key.LEFT])
113        InputMap.add_action("right", [Key.D, Key.RIGHT])
114        InputMap.add_action("jump", [Key.SPACE])
115
116        # Screen-space (Y-down) world: gravity pulls toward +Y so falling reads as
117        # falling on screen. Every physics node below resolves to this root's world.
118        world = self.add_child(PhysicsRoot2D(name="World", gravity=Vec2(0.0, GRAVITY)))
119
120        world.add_child(Player(name="Player", position=Vec2(WIDTH / 2, 100)))
121
122        # Ground
123        world.add_child(Platform(name="Ground", w=WIDTH, h=20, position=Vec2(WIDTH / 2, HEIGHT - 10)))
124
125        # Platforms
126        platforms = [
127            (200, 450, 150),
128            (400, 350, 120),
129            (600, 450, 150),
130            (300, 250, 100),
131            (500, 250, 100),
132            (150, 150, 130),
133            (650, 150, 130),
134        ]
135        for i, (x, y, w) in enumerate(platforms):
136            world.add_child(Platform(name=f"Plat{i}", w=w, position=Vec2(x, y)))
137
138    def on_draw(self, renderer):
139        renderer.draw_text("Platformer Demo", (10, 10), scale=2, colour=(1.0, 1.0, 1.0))
140        renderer.draw_text(
141            "A/D: move  SPACE: jump  |  touch: hold a side to move, tap top to jump",
142            (10, 45),
143            scale=1,
144            colour=(0.71, 0.71, 0.71),
145        )
146
147
148if __name__ == "__main__":
149    App(title="Platformer Demo", width=WIDTH, height=HEIGHT).run(PlatformerDemo())