Platformer¶
CharacterBody2D with gravity, jump, and platforms.
▶ Run in browserTags: 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())