8 Scene Tree
dxtr edited this page 2026-10-20 10:09:08 +02:00

Scene Tree

The systems pyssel/scene and pyssel/scene-2d hold nodes in a tree, as Godot does, and call their callbacks each frame:

(asdf:load-system :pyssel/scene-2d)

(defpackage #:walk
  (:use #:cl)
  (:local-nicknames (#:sc #:pyssel/scene) (#:s2d #:pyssel/scene-2d) (#:p2d #:pyssel/2d)
                    (#:gm #:cl-gmath)))
(in-package #:walk)

(sc:define-node player (s2d:node-2d)
  ((velocity :initarg :velocity :initform 120.0 :accessor player-velocity
             :type single-float :property t :hint (:range 0 500)))
  (:signals reached-edge))

(defmethod s2d:draw ((player player) canvas)
  (p2d:fill-rect canvas -10 -10 20 20 '(1 0.6 0.2) :radius 4))

(defmethod sc:process ((player player) delta)
  (let ((step (* (player-velocity player) delta)))
    (when (sc:action-pressed-p :right) (incf (s2d:node-x player) step))
    (when (sc:action-pressed-p :left) (decf (s2d:node-x player) step))
    (when (> (s2d:node-x player) 300)
      (sc:emit-signal player 'reached-edge))))

(sc:define-node level (s2d:node-2d) ())

(defmethod sc:ready ((level level))
  (sc:connect (sc:find-node level "player") 'reached-edge level 'back-to-start))

(defun back-to-start (level)
  (setf (s2d:node-x (sc:find-node level "player")) 20))

(defvar *tree*
  (let ((level (make-instance 'level :name "level")))
    (sc:add-child level (make-instance 'player :name "player"
                                               :position (gm:make-dvec2 :x 20 :y 100)))
    (s2d:run level :title "Walk" :width 320 :height 200
                   :input-map '((:left (:key :left) (:key :a))
                                (:right (:key :right) (:key :d))))))

Hold the right arrow, or D, and the square walks right, and back to the start when it reaches the edge. Redefine draw or process at the REPL and the next frame uses the new method; redefine player with another slot and the node gets it. The game's state is in its nodes, as the player's velocity; *tree*, the tree run returns, is only the REPL's way in to it (see Getting Started).

  • Node types are CLOS classes defined with define-node, whose metaclass knows more than defclass does: :property t marks the slots a saved scene keeps and the inspector shows, each with an :initarg, and :hint says how to edit them; (:signals ...) names the signals the nodes emit; (:tool t) marks a class whose behaviour runs while editing; and (:internal t) a class of parts that controls make of themselves, or of the editor's own nodes, which the editor does not offer for scenes, as Godot's internal classes. class-properties, property-hint, class-signals, tool-class-p and internal-class-p read them. make-instance checks what the initargs give the properties against their types, as a saved scene's loading does, and takes any real for a float property, as :velocity 200; a wrong one is refused in words read from the type, as "PYSSEL/UI:CONTROL's :GROW-HORIZONTAL is a grow direction: :START, :END or :BOTH; not :UP." A slot's :copy t keeps a copy, by copy-seq, of what an initarg gives it, and :copy f what the function f returns of it, as a 2D node keeps copies of the vectors it is given.
  • Names: a boolean a program sets takes a plain name, after Godot's property where there is one, as embedded-window-modal, viewport-fullscreen and pyssel/ui's button-pressed; one an object reports about its own state ends in -p, as node-freed-p, tree-quitting-p and event-handled-p.
  • Callbacks are generic functions: enter-tree, ready (once the children are ready) and exit-tree as a node joins and leaves a tree; process each frame, with the seconds since the last; physics-process at a fixed rate, 60 a second by default; input and unhandled-input for input events. A canvas item's draw draws it. The tree's tweens step after each physics step and after process. After processing, the root's ui-process is the user interface's turn, where it lays out its controls.
  • The tree: add-child, remove-child, move-child and queue-free, which frees a node at the end of the frame. find-node follows a path such as "hud/score", "../player" or "/root/level"; node-path gives a node's. Groups (add-to-group, nodes-in-group, node-groups) gather nodes across the tree. Metadata, as Godot's set_meta, is data kept on a node beside its class's properties, by a symbol: (setf (node-meta node :edit-lock) t) sets it, node-meta reads it, with a second value saying whether the node has it, remove-node-meta takes it away, and node-meta-names lists the names. It is saved with the scene and copied with the node, and changes nothing of how the node behaves: the editor keeps there whether a node is locked or grouped, and the scene's guides, as Godot's does. A node's parts, such as a scroll container's scrollbars, are internal children, added with :internal :front or :internal :back: they go before or after its ordinary children, are processed and drawn as the others, are left out of node-children and the counting and indexing of children unless asked with :internal t, and scenes do not save them.
  • Changing scenes: a tree's current scene, tree-current-scene, is the scene that runs under its root beside the autoloads, as Godot's current_scene, which run and a play set. change-scene-to-file changes it to a new instance of a scene file, relative to the project's directory, from a menu to a level and back, and change-scene-to-packed to one of a packed scene: the scene leaves the tree at once, and at the end of the frame's processing it is freed and the new one joins, after which the tree emits scene-changed, a signal of the tree. reload-current-scene plays a level again from its file, and unload-current-scene frees the current scene at once. The tree is by default the one whose callback calls them.
  • Signals: (connect source signal target function) and emit-signal, which Signals describes for nodes, resources and other objects. Give the function as a symbol with a target node, as above, and add :persist t for a saved scene to keep the connection; a function object with no target suits code that connects at run time. signal-connections lists a node's connections. Every node emits tree-entered, ready and tree-exiting.
  • Input, actions and shortcuts reach nodes as Input and Actions says, and the 2D nodes, a viewport, canvas items and nodes with a position, are on 2D Nodes.
  • Modes: a tree in :play mode runs every node; in :edit mode, as an editor's, only nodes of tool classes behave, though every canvas item draws, so that an editor shows a scene as it will play; and the engine draws only when something changes, or when wake-tree asked for a frame after a delay, for timed work such as a tooltip's. (setf tree-mode) switches. :low-processor-mode, an option of make-scene-tree and run, as Godot's, has a tree in :play mode draw only so too, as a user interface that changes only as it is used wants: the editor's own tree is one.
  • Quitting: (sc:quit) in a callback ends the tree's run, as Godot's get_tree().quit() does: its engine stops after the frame. A game ends so wherever it plays, and tree-quitting-p says it asked, for a tree without an engine. A tree in :edit mode does not quit.
  • Errors: an error in a callback stops that node, with a node-stopped warning, and the rest of the tree goes on. Redefining a node class or a callback's method resumes it, as does resume-node. Stopping and resuming call behaviour-changed for the node, as a change of the tree's mode calls mode-changed: a canvas item draws again, since its draw method may draw less while it does not behave. A system that calls callbacks of its own calls them in behave, which does the same. tree-error-version moves on as a node stops or resumes, and as a canvas item's drawing fails or draws again, so what shows which nodes failed notices without looking; errors-changed moves it on, and asks for a frame, for a system whose nodes report errors of their own.

A tree is used from its own thread: its callbacks run there, and code on another thread, such as the REPL's, goes through call-in-tree:

(sc:call-in-tree *tree*
  (lambda ()
    (setf (player-velocity (sc:find-node (sc:tree-root *tree*) "level/player")) 300.0)))

call-in-tree waits for the function's values. send-to-tree hands a function over and returns at once, for code that must not wait on a tree that may hang, as an editor must not wait on a game; its :reply, a function of the values and the error, runs at the next frame of the tree whose callbacks sent it, or straight after, from outside every tree. A tree destroyed already makes it signal an error, unless :if-destroyed nil has it return nil instead, for a thread that hands a tree what it finished while the tree may have gone, as a loader does:

(sc:send-to-tree game-tree (lambda () (length (sc:nodes-in-group game-tree :enemies)))
                 :reply (lambda (values error)
                          (if error
                              (format t "The game could not count: ~A~%" error)
                              (format t "~D enemies~%" (first values)))))

For now that thread is the engine's render thread, which processes the tree before drawing each frame; whether it stays so is an open question, so code should not rely on it. make-scene-tree makes a tree for an engine you started yourself, or for none, to process with advance-tree in tests and tools. A tree for an engine takes the engine's window, or its offscreen target: the window's input, and its root drawing there. Made with :window nil, as a tree drawn into a texture by another tree's control is, it takes none of the window's input, and its root's part has no window, while the engine's frames process it as any other; tree-window tells. Its root viewport has the size it is given, as set-viewport-size gives it at once, embeds the popups shown in it, and draws where draw-viewport is called, as in the :draw function of a render target, as a viewport-container shows it; prepare-viewport, the target's :prepare, has it drawn only when what it shows changed. Its quit only says so, in tree-quitting-p, as its engine is not its own to stop. check-tree-thread refuses a call from another thread than a tree's, for a system built on the tree whose functions are called on its thread only. Processing nodes and dispatching input allocate nothing in steady state.