Building a Simple Game with the SimVX Editor

This tutorial walks through building a 2D game scene using the SimVX editor. Each step has been validated with automated tests to ensure the editor is functional.

Prerequisites

# Install in dev mode
uv pip install -e packages/core -e packages/graphics -e packages/editor

# Launch the editor
uv run python -m simvx.editor.main
# Or: from simvx.editor.app import launch; launch()

Step-by-Step Tutorial

Step 1: Launch the Editor

Action: Run the editor. A window appears with:

  • Top bar: menu bar (File, Edit, Scene, View, Help) on the left, 3D/2D/Code mode switcher in the centre, Play/Pause/Stop controls on the right. There is no separate toolbar – viewport tools live inside the viewport tab.

  • Left panel: tabbed Scene Tree / File Browser.

  • Centre: tabbed workspace (Viewport, Code Editor, …).

  • Right panel: Properties (inspector for the selected node).

  • Bottom tabs: Output, Profiler (other tabs are added by plugins such as the IDE bridge).

  • Status bar at the very bottom.

The startup scene is a populated 3D demo (Node3D root with floor, cube, sphere, cylinder, sun, orbit camera, and a WorldEnvironment). Choose File -> New Scene to start from a fresh root.

Verify:

  • [x] The editor window opens without errors

  • [x] All panels are visible and labelled

  • [x] The Scene Tree displays the active scene’s root node

  • [x] The undo stack is empty

  • [x] The editor is not in play mode


Step 2: Add a Game Node

Action: Click the “+” button in the Scene Tree header. The Add Node dialog appears.

In the dialog:

  1. Available node types are grouped into collapsible categories (2D Nodes, 3D Nodes, Physics 2D, Physics 3D, Lights, Animation, Audio, Navigation, Particles, TileMap, UI, Containers, Viewport, Misc). 2D Nodes, 3D Nodes and UI are expanded by default.

  2. A filter text field at the top narrows the list in real time.

  3. Click “Node2D” under 2D Nodes.

Or without the mouse. The dialog opens with the filter field focused, so you can start typing straight away. Tab and Shift+Tab move focus between the filter and the list, and Down from the filter steps into the list directly. While the list holds focus the arrow keys move the selection, Enter adds the selected type, and Shift+Enter adds it in placement mode so you position it with the mouse. Typing a character hands the keyboard back to the filter, so a search can be refined without tabbing back first, and Escape closes the dialog from anywhere in it.

Result: A new Node2D appears under the selected node (or the root) in the Scene Tree.

Verify:

  • [x] Clicking “+” opens the Add Node dialog

  • [x] The dialog lists the available types grouped by category

  • [x] Clicking a type creates the node and closes the dialog

  • [x] The new node appears in the Scene Tree

  • [x] The new node gets selected after creation

  • [x] The operation is recorded in the undo stack


Step 3: Rename the Node

Action: With the Node2D selected, press F2 to open the rename overlay.

  1. The rename field appears with the current name highlighted

  2. Type “Game”

  3. Press Enter to confirm

Result: The node is renamed from “Node2D” to “Game” in the Scene Tree.

Verify:

  • [x] F2 key opens the rename overlay

  • [x] Typing a name and pressing Enter renames the node

  • [x] The tree view updates to show the new name

  • [x] Renaming is undoable with Ctrl+Z


Step 4: Add Child Nodes

Action: With “Game” selected, click “+” again.

  1. Select “Node2D” from the dialog

  2. The new node appears under Game (as a child)

  3. Press F2, type “Player”, press Enter

  4. Select “Game” again in the tree

  5. Click “+” and add another “Node2D”

  6. Rename it to “Enemy”

  7. Select “Game”, add a “Camera3D”

Result: Your scene tree looks like:

Root
  └─ Game (Node2D)
       ├─ Player (Node2D)
       ├─ Enemy (Node2D)
       └─ Camera3D

Verify:

  • [x] Nodes are added as children of the selected node

  • [x] If nothing is selected, nodes go under Root

  • [x] Multiple nodes can be added sequentially

  • [x] Each add is a separate undo operation


Step 5: Select a Node and View Properties

Action: Click on “Player” in the Scene Tree.

Result: The Properties panel updates to show:

  • Type label: “Node2D” (with a Convert to Custom Class button next to it for built-in types)

  • Name field: “Player”

  • Node section: Visible checkbox

  • Transform section: Position (X, Y), Rotation, Scale (X, Y)

Verify:

  • [x] Clicking a tree item updates the inspector

  • [x] Inspector shows the correct type name

  • [x] Inspector shows the correct node name

  • [x] Transform section appears for spatial nodes

  • [x] Node section appears with visibility toggle

  • [x] Switching selection updates the inspector


Step 6: Edit Transform Properties

Action: In the Properties panel, change the Player’s position:

  1. Click on the Position X spinbox

  2. Type 100 or use the increment buttons

  3. Change Position Y to 300

Result: The Player node’s position updates to (100, 300).

Verify:

  • [x] Position widgets exist for 2D nodes

  • [x] Initial values match the node’s position (0, 0)

  • [x] Changing a value updates the node immediately

  • [x] Position changes are undoable with Ctrl+Z

  • [x] 3D transforms work similarly with X, Y, Z components

  • [x] Rotation and Scale can also be edited


Step 7: Undo and Redo

Action: Try the undo/redo system:

  1. Press Ctrl+Z to undo the last position change

  2. Press Ctrl+Shift+Z to redo it

  3. Undo multiple times to step back through all changes

  4. Redo to restore them

Verify:

  • [x] Ctrl+Z undoes the last operation

  • [x] Ctrl+Shift+Z redoes an undone operation

  • [x] Multiple undos can be chained

  • [x] Undo works for: add node, rename, property change, delete, duplicate


Step 8: Delete and Duplicate Nodes

Action:

  1. Select “Enemy” in the tree

  2. Press Delete key – Enemy is removed

  3. Press Ctrl+Z – Enemy is restored

  4. Select “Player”

  5. Use the context menu (right-click) -> Duplicate (or press Ctrl+D) – creates “Player_copy”

  6. Select “Player_copy” and press Delete to remove it

Verify:

  • [x] Delete key removes the selected node

  • [x] Cannot delete the root node

  • [x] Delete clears the selection

  • [x] Deleting is undoable

  • [x] Duplicate creates a copy with “_copy” suffix

  • [x] Duplicate is undoable

  • [x] Right-click context menu appears with all options


Step 9: Copy and Paste

Action:

  1. Select “Player”

  2. Right-click -> Copy (or Ctrl+C)

  3. Select “Game”

  4. Right-click -> Paste (or Ctrl+V) – creates “Player_paste” under Game

Verify:

  • [x] Copy stores the node in the clipboard

  • [x] Paste creates a new node from the clipboard

  • [x] Pasted node gets “_paste” suffix

  • [x] Paste is undoable


Step 10: Save the Scene

Action:

  1. Press Ctrl+S to save

  2. If no path is set, the Save As dialog appears

  3. Navigate to your project directory

  4. Enter filename: “game.py”

  5. Click Save

Result: Scene is saved to disk as a Python source file – the scene tree and its node properties round-trip as a Node subclass.

Verify:

  • [x] Save creates a Python (.py) file on disk

  • [x] Save clears the “modified” flag

  • [x] Save remembers the path for future saves

  • [x] The path is stored in current_scene_path


Step 11: Load a Scene

Action:

  1. Press Ctrl+N to create a new empty scene

  2. Press Ctrl+O to open the file dialog

  3. Navigate to and select “game.py”

  4. Click Open

Result: The saved scene is loaded with all nodes restored.

Verify:

  • [x] New Scene clears everything (nodes, selection, undo)

  • [x] Open Scene loads nodes from the Python scene file

  • [x] Loading clears the undo stack

  • [x] Loaded scene has all the original nodes


Step 12: Custom Properties

If you create a custom node class with Property descriptors:

from simvx.core import Node2D, Property

class Player(Node2D):
    speed = Property(5.0, range=(0, 20))
    health = Property(100, range=(0, 200))
    name_tag = Property("hero")
    is_alive = Property(True)
    mode = Property("walk", enum=["walk", "run", "idle"])

The Properties panel automatically picks a widget for each property:

Type

Widget

int / float (with or without range)

SpinBox

bool

CheckBox

str

TextEdit

str with enum

DropDown

Colour property

ColourPicker

Vec2 / Vec3

Multi-SpinBox row

list / dict

Expandable per-element editor


Step 13: Play Mode

Action:

  1. Press F5 (or click Play) to enter play mode

  2. Press F7 (or click Pause) to toggle pause

  3. Press F6 (or click Stop) to exit play mode

Result: The scene is serialised before play, and restored when stopped.

Verify:

  • [x] Play enters play mode and emits play_state_changed

  • [x] Pause toggles the paused state

  • [x] Stop restores the scene to pre-play state

  • [x] Stop when not playing is a no-op


Step 14: Properties Section Collapse

Action: Click on a section header (e.g., “Transform”) to collapse it.

Result: The section rows hide. Click again to expand.


Step 15: Gizmo and Viewport Mode

Action:

  • Press Q to cycle through gizmo modes (translate / rotate / scale)

  • Click 3D / 2D / Code buttons in the top bar to switch the centre workspace mode


Authoring custom classes

Beyond the basic Add Node / Duplicate flow above, the editor exposes the Node = Class = File model: instances configure via __init__ kwargs; classes are real Python files; refactorings update the project end-to-end.

Convert to Custom Class (give a node behaviour)

Select a built-in node (Sprite2D, Node3D, etc.) and click the Convert to Custom Class button shown next to the type label in the Properties panel header – or right-click in the scene tree and choose Convert to Custom Class. Both entry points act while the node is still on a class the engine ships: once it has a class of your own the header shows that class’s module, its file and an Edit class file button instead, and the menu entry does nothing. The dialog lets you pick:

  • A class name (defaults to the node’s name).

  • A template: Empty writes a class with nothing in it, and the others write the code a node of that kind usually needs – the character templates bring movement and a collider, Timer wires its own timeout. Only templates written against a class the node already descends from are offered, and the class extends the node’s own type rather than the template’s.

  • A file: <project>/<class_files_dir>/<snake_case>.py by default (class_files_dir is set in simvx.toml [editor], default src/), which you may edit; or Inline in parent scene file, which puts the class at the top of the scene file itself. A file that already exists gains the class beside what is already in it.

On accept the class file is written, the scene’s own source is pointed at it – the construction is retyped where it stands, keeping your arguments and comments, and the import goes in beside it – the scene is saved, and it is reloaded. The node in the tree afterwards is an instance of the new class, built by running it, so anything the class declares (a Child collider, its Properties) is already there, exactly as it will be when the game runs. The new file opens in the code workspace at the class it defines.

Converting the scene root is the same action on the class the file defines: it is rebased so that it extends the new class, and what the file already gives the root goes on reaching the old base through it. The rebase moves one name and touches nothing else, though the save that carries it is an ordinary save of the scene, so a value the file already sets can end up set twice: the line the author wrote, and the same value again as a kwarg the save writes beside it. The conversion refuses, without writing anything, when the file says something it cannot rewrite without guessing – a class with two bases, a base written as simvx.core.Node3D rather than a plain name – or when the scene has never been saved, or when the node is deeper in the tree than a save reaches (a save writes the root and its own children).

It also refuses when __init__ builds the node with a line no save rewrites – a factory (self.add_child(make_player())), a name bound somewhere else, a construction the save would have to take other statements out to repoint. Nothing would point at the class file and the scene would go on building the type it always built, so the class file is put back as it was found and the dialog stays open with the reason on it; build that node with the class by hand.

The class name also decides a module name, and that name is Python’s rather than the project’s: a class called Json would be written to src/json.py and imported as json, which is the standard library’s. A name a module Python can already find answers to is refused before anything is written, since the scene’s own from json import Json would afterwards mean one module or the other depending on how the game was started.

Converting a node that is already on a class of your own – a second class extending the first – is something simvx.editor.convert_node_to_class does, writing class Frost(Hero) and importing Hero from the file it lives in. Only the API reaches it: the button and the menu entry are offered for engine classes, so from the editor a node is converted once.

Duplicate Node (3 variants)

Right-click -> Duplicate… opens a dialog with three options:

  • New Instance (default): adds another <SameClass>(...) to the parent with kwargs copied from the original. Same class definition, two instances.

  • Subclass: prompts for a class name (default <Original>2), creates <class_files_dir>/<snake_case>.py containing class <Name>(<Original>): pass, and adds an instance of <Name> to the parent.

  • Detached Copy: prompts for a class name. Creates a new file containing class <Name>(<OriginalBase>): <body> – the body is copied verbatim from the original class file, but rebased on the original’s base class (not subclassing the original). For built-in originals with no class file to copy from, this degenerates to a pass body (the same shape as Subclass).

The keyboard shortcut Ctrl+D (and the plain Duplicate menu entry) keeps the quick “New Instance” behaviour without the dialog.

Rename Class (project-wide)

Select a user-class instance and right-click -> Rename Class… – the dialog prompts for the new name and offers an “Also rename file to match” checkbox. On submit, every file in the project is rewritten in one atomic two-phase step (compute all new sources in memory, then write; rollback on any write failure):

  • The class definition.

  • Every from <module> import <ClassName> (alias preserved when present).

  • Every base-class reference (class Frost(Player): -> class Frost(Hero):).

  • Every <ClassName>(...) instantiation.

  • Every isinstance(x, <ClassName>) and annotation site.

When Also rename file is ticked, the defining file (or folder, for package-style classes) moves to the new snake_case name and importers’ module paths are rewritten to follow.

Extract / Inline files

Right-click on a .py file with multiple top-level classes in the file browser -> Extract classes to folder. The file becomes a folder of the same name, one class per <snake_case>.py, with an __init__.py re-exporting each so absolute imports of the original module path keep working. Refuses when the file has fewer than two classes, has non-class/non-import top-level statements, or a target folder of that name already exists.

Inverse: right-click on a folder containing __init__.py -> Inline folder to file concatenates every class back into a single .py. Refuses on unsupported constructs (module-level side effects, conditional imports, free functions); a force path proceeds best-effort and reports each suspect construct on the returned result – the generated source itself is left clean (no # noqa markers, no inserted comments).