First Scene¶
Open a window and draw a node.
▶ Run in browserTags: 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-runson_drawonly when the node changes, so a node whose drawing animates every frame (like our pulsing circle) setsdynamic = Trueto 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:
Propertydescriptors 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())