First Scene

Open a window and draw a node.

▶ Run in browser

Tags: beginner tutorial

First scene

Your first SimVX scene in a few dozen lines. By the end you will have opened a window, drawn a pulsing circle that centres itself on whatever window it is given, and seen three of the lifecycle hooks every node has access to.

Step 1: Subclass Node2D

Every visible thing in SimVX is a node. Node2D is the 2D-aware variant: it knows about position, rotation, and lets you override on_draw() to issue immediate-mode draw commands.

from simvx.core import Node2D

class FirstScene(Node2D):
    pass

Step 2: Hook into the frame loop

Three hooks fire automatically once the node is in the scene tree:

  • on_ready(): called once after the node enters the tree. Initialise state here.

  • on_update(dt): called every frame with the seconds since the last frame.

  • on_draw(renderer): issue draw commands here. The 2D renderer is retained: it re-runs on_draw only when the node changes, so a node whose drawing animates every frame (like our pulsing circle) sets dynamic = True to opt into per-frame redraws.

import math

RADIUS, PULSE = 80.0, 20.0  # the circle breathes between 60 and 100 px

class FirstScene(Node2D):
    dynamic = True  # on_draw animates, so redraw it every frame

    def on_ready(self):
        self.t = 0.0

    def on_update(self, dt: float):
        self.t += dt

    def on_draw(self, renderer):
        centre = self.viewport_size / 2
        renderer.circle(centre, RADIUS + PULSE * math.sin(self.t * 2), fill=(0.4, 0.8, 1.0, 1.0))
        label_y = centre.y + RADIUS + PULSE + 40
        renderer.text("Welcome to SimVX", (centre.x, label_y), fill=(1.0, 1.0, 1.0), scale=2, align="centre")

align="centre" anchors the text’s centre on the x you pass, so there is no width arithmetic to hand-tune.

Step 3: Measure the window, do not remember it

self.viewport_size is the live size of whatever this node draws into, and it is read fresh every frame. Writing WIDTH, HEIGHT = 800, 600 at the top of the file and centring on (WIDTH / 2, HEIGHT / 2) would be right in the window the example opens at and wrong in a resized one, on a phone, and in a browser canvas that follows the page. The only size in the file is the window you ask for, in App(width=..., height=...).

viewport_size is valid from on_enter_tree() onward, which is before any hook that draws or lays out. Off the tree it raises rather than handing back a size nobody asked for, so a baked fallback can never creep back in.

Step 4: Launch it

App creates a Vulkan-backed window and runs your scene tree.

from simvx.graphics import App

App(title="First Scene", width=800, height=600).run(FirstScene())

Run it

# In your own copy of this directory
python main.py

# Anywhere, with the example library installed
simvx examples run tutorials_first_scene

A window opens with a pulsing blue circle and a welcome message.

What’s next

  • Nodes and Signals: the next tutorial, which wires several nodes together so they react to each other.

  • Input and Movement: drive a node from the keyboard with input actions.

  • Bouncing Balls: Property descriptors and many children at once.

Source

 1# /// simvx
 2# tags = ["beginner", "tutorial"]
 3# web = { width = 800, height = 600, responsive = true }
 4# ///
 5"""First Scene: Open a window and draw a node.
 6
 7A scene is a tree of nodes, and a node is a Python class. This one subclasses
 8`Node2D` and overrides three lifecycle hooks:
 9
10- `on_ready()` runs once, after the node enters the tree. It starts a clock at zero.
11- `on_update(dt)` runs every frame with the seconds since the last one. It
12  advances the clock.
13- `on_draw(renderer)` paints the node: a blue circle whose radius breathes
14  between 60 and 100 pixels, and a welcome line below it. `align="centre"`
15  centres the text on the x it is given.
16
17The 2D renderer is retained: it re-runs `on_draw` only when the node changes.
18A node whose drawing animates every frame sets `dynamic = True` on the class so
19it is redrawn each frame.
20
21`self.viewport_size` is the live size of whatever the node draws into, so the
22circle stays centred in a resized window, on a phone and in a browser canvas.
23The window size appears once, as what `App(width=..., height=...)` asks for, and
24`App(...).run(scene)` opens the window and runs the scene until it closes.
25"""
26
27import math
28
29from simvx.core import Node2D
30from simvx.graphics import App
31
32RADIUS, PULSE = 80.0, 20.0  # the circle breathes between 60 and 100 px
33
34
35class FirstScene(Node2D):
36    dynamic = True  # on_draw animates, so redraw it every frame
37
38    def on_ready(self):
39        self.t = 0.0
40
41    def on_update(self, dt: float):
42        self.t += dt
43
44    def on_draw(self, renderer):
45        centre = self.viewport_size / 2
46        renderer.circle(centre, RADIUS + PULSE * math.sin(self.t * 2), fill=(0.4, 0.8, 1.0, 1.0))
47        # Below the circle at its widest, so the text does not bob with the pulse.
48        label_y = centre.y + RADIUS + PULSE + 40
49        renderer.text("Welcome to SimVX", (centre.x, label_y), fill=(1.0, 1.0, 1.0), scale=2, align="centre")
50
51
52if __name__ == "__main__":
53    App(title="First Scene", width=800, height=600).run(FirstScene())