Bouncing Balls¶
Properties, velocity, and screen-edge collision.
▶ Run in browserTags: tutorial beginner properties
Bouncing Balls¶
You can move one node now. This tutorial spawns many nodes that each carry their
own state, and introduces Property descriptors: the engine’s way to declare a
value that is type-checked, range-validated, editor-visible, and serialised, all at once.
By the end you will have a window of colourful balls bouncing off the edges, each running its own physics.
Step 1: Declare properties¶
A Property looks like a class attribute with a default and an optional range. The
engine validates assignments against the range and shows the value in the editor:
class Ball(Node2D):
radius = Property(12.0, range=(4, 40))
speed = Property(200.0, range=(50, 500))
You read and write a property like a normal attribute (self.radius); the descriptor
does the validation behind the scenes.
Step 2: Give each ball its own state¶
__init__ runs once per ball. Each picks a random direction and colour, so every
ball is independent even though they share a class:
def __init__(self, **kwargs):
super().__init__(**kwargs)
angle = random.uniform(0, math.tau)
self.velocity = Vec2(math.cos(angle), math.sin(angle)) * self.speed
Step 3: Move and bounce each frame¶
on_update(dt) advances the ball and reflects its velocity when it reaches an edge,
clamping the position so it never escapes the window:
def on_update(self, dt):
self.position += self.velocity * dt
if self.position.x < self.radius or self.position.x > WIDTH - self.radius:
self.velocity.x *= -1
self.position.x = max(self.radius, min(WIDTH - self.radius, self.position.x))
# ... same for y
Step 4: Spawn a crowd¶
The root builds the scene tree in on_ready(), adding eight balls at random
positions. Each child then updates and draws itself: you write the behaviour once,
and every instance runs it.
class BouncingBalls(Node2D):
def on_ready(self):
for i in range(8):
self.add_child(Ball(name=f"Ball{i}",
position=Vec2(random.uniform(50, WIDTH - 50),
random.uniform(50, HEIGHT - 50))))
Run it¶
uv run python examples/tutorials/bouncing_balls/main.py
What’s next¶
Pong – combine input, signals, and collision into a complete game.
Source¶
1#!/usr/bin/env python3
2"""Bouncing Balls: Properties, velocity, and screen-edge collision.
3
4Spawn colourful balls that bounce off screen edges. Demonstrates the
5`Property` descriptor for editor-visible values and basic frame-by-frame
6movement with `on_update()`.
7
8# /// simvx
9# tags = ["tutorial", "beginner", "properties"]
10# web = { root = "BouncingBalls", width = 800, height = 600, responsive = true }
11# ///
12
13## What You Will Learn
14
15- **Property** -- Declare editor-visible properties with validation ranges
16- **on_update(dt)** -- Per-frame update callback with delta time
17- **position** -- Move nodes by updating `self.position` each frame
18- **add_child()** -- Build a scene tree dynamically in `on_ready()`
19- **draw_circle()** -- Render filled circles
20
21## How It Works
22
23`Ball` declares `radius` and `speed` as `Property` descriptors with value
24ranges. In `__init__`, a random velocity vector is created. Each frame,
25`on_update(dt)` advances the ball's position and reflects velocity when the
26ball hits a screen edge.
27
28`BouncingBalls` is the root node that spawns 8 `Ball` children in `on_ready()`,
29placing them at random positions. The parent also draws a title and ball
30count via `draw_text()`.
31"""
32
33import math
34import random
35
36from simvx.core import Node2D, Property, Vec2
37from simvx.graphics import App
38
39WIDTH, HEIGHT = 800, 600
40
41
42class Ball(Node2D):
43 radius = Property(12.0, range=(4, 40))
44 speed = Property(200.0, range=(50, 500))
45
46 def __init__(self, **kwargs):
47 super().__init__(**kwargs)
48 angle = random.uniform(0, math.tau)
49 self.velocity = Vec2(math.cos(angle), math.sin(angle)) * self.speed
50 self.colour = (
51 random.uniform(0.4, 1.0),
52 random.uniform(0.4, 1.0),
53 random.uniform(0.4, 1.0),
54 1.0,
55 )
56
57 def on_update(self, dt: float):
58 self.position += self.velocity * dt
59
60 # Bounce off walls
61 if self.position.x < self.radius or self.position.x > WIDTH - self.radius:
62 self.velocity.x *= -1
63 self.position.x = max(self.radius, min(WIDTH - self.radius, self.position.x))
64 if self.position.y < self.radius or self.position.y > HEIGHT - self.radius:
65 self.velocity.y *= -1
66 self.position.y = max(self.radius, min(HEIGHT - self.radius, self.position.y))
67
68 def on_draw(self, renderer):
69 renderer.draw_circle(self.position, self.radius, colour=self.colour, filled=True)
70
71
72class BouncingBalls(Node2D):
73 def on_ready(self):
74 for i in range(8):
75 self.add_child(
76 Ball(
77 name=f"Ball{i}",
78 position=Vec2(random.uniform(50, WIDTH - 50), random.uniform(50, HEIGHT - 50)),
79 )
80 )
81
82 def on_draw(self, renderer):
83 renderer.draw_text("Bouncing Balls", (10, 10), scale=2, colour=(1.0, 1.0, 1.0))
84 renderer.draw_text(f"{len(self.children)} balls", (10, 35), scale=1, colour=(0.71, 0.71, 0.71))
85
86
87if __name__ == "__main__":
88 App(title="Bouncing Balls", width=WIDTH, height=HEIGHT).run(BouncingBalls())