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

Tests: TestStep1_LaunchEditor


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.

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

Tests: TestStep2_AddNodes, TestAddNodeDialog, TestAddNodeDialogFilter


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

Tests: TestStep3_RenameNodes, TestUndoRedoViaUI


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

Tests: TestStep2_AddNodes::test_add_node_under_selected_parent


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 Make 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

Tests: TestStep4_SelectionInspector


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

Tests: TestStep5_EditTransforms, TestSpinBoxInteraction


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

Tests: TestStep6_UndoRedo, TestKeyboardShortcuts


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

Tests: TestStep7_DeleteDuplicate, TestContextMenu


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

Tests: TestStep8_CopyPaste


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, properties, and attached scripts 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

Tests: TestStep9_SaveLoad


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

Tests: TestStep9_SaveLoad


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

Tests: TestStep12_CustomProperties


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

Tests: TestStep10_PlayMode


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.

Tests: TestStep14_SectionCollapseExpand


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

Tests: TestStep15_GizmoMode, TestStep16_ViewportMode


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.

Make Custom Class (promote to a user class)

Select a built-in node (Sprite2D, Node3D, etc.) and click the Make Custom Class button shown next to the type label in the Properties panel header – or right-click in the scene tree and choose Make Custom Class. The dialog lets you pick:

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

  • New file in project src (default): creates <project>/<class_files_dir>/<snake_case>.py with class <Name>(<Base>): pass. class_files_dir is set in simvx.toml [editor] (default src/).

  • Inline in parent scene file: inserts a top-level class <Name>(<Base>): pass into the parent scene file just above the scene class. Disabled when the active scene is not yet backed by a file.

After creation, the editor opens the new class file (or jumps to the inline block) in the workspace. The scene’s runtime node is reclassed to the new type, and the next save reflects the type swap in the source.

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).

Tests: test_make_custom_class_dialog.py, test_duplicate_node_dialog.py, test_project_classes.py::TestRenameClass, test_refactor.py, test_rename_refactor_integration.py.