Bouncing Balls

Properties, velocity, and screen-edge collision.

▶ Run in browser

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