3 Saving Scenes
dxtr edited this page 2026-10-20 10:09:08 +02:00

Saving Scenes

save-scene saves a node and its descendants to a file, and load-scene loads the file as a packed scene, which instantiate makes new nodes from, as often as needed: a level, or each bullet a gun fires.

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

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

(res:define-resource stats ()
  ((health :initarg :health :initform 3 :accessor stats-health
           :type (integer 0) :property t)))

(sc:define-node enemy (s2d:node-2d)
  ((stats :initarg :stats :initform (make-instance 'stats) :accessor enemy-stats
          :type stats :property t)
   (target :initarg :target :initform nil :accessor enemy-target
           :type (or null sc:node) :property t))
  (:signals died))

(defmethod s2d:draw ((enemy enemy) canvas)
  (p2d:fill-circle canvas 0 0 (* 4 (stats-health (enemy-stats enemy))) '(0.8 0.2 0.2)))

(defun cheer (arena)
  (format t "~A lost an enemy.~%" (sc:node-name arena)))

(let* ((arena (make-instance 's2d:node-2d :name "arena"))
       (player (sc:add-child arena (make-instance 's2d:node-2d :name "player"
                                                               :position (gm:make-dvec2 :x 40 :y 100))))
       (brute (make-instance 'stats :health 6)))
  (dolist (x '(160 240))
    (let ((enemy (sc:add-child arena (make-instance 'enemy :position (gm:make-dvec2 :x x :y 100)
                                                            :stats brute :target player))))
      (sc:connect enemy 'died arena 'cheer :persist t)))
  (sc:save-scene arena "arena.scene"))

(defvar *tree*
  (s2d:run (sc:instantiate (sc:load-scene "arena.scene")) :title "Arena" :width 320 :height 200))

Two red circles appear, drawn by nodes made from arena.scene, which holds:

;;;; A Pyssel scene file.  -*- mode: lisp -*-
(:scene
 :format 1
 :resources ((1 :embedded (arena::stats :health 6)))
 :root
 ((pyssel/scene-2d:node-2d :name "arena")
  :children
  ((pyssel/scene-2d:node-2d :name "player" :position (:vector double-float 40.0d0 100.0d0))
   (arena::enemy
    :name "enemy"
    :position (:vector double-float 160.0d0 100.0d0)
    :stats (:resource 1)
    :target (:node "../player"))
   (arena::enemy
    :name "enemy-2"
    :position (:vector double-float 240.0d0 100.0d0)
    :stats (:resource 1)
    :target (:node "../player"))))
 :connections (("enemy" arena::died "." arena::cheer) ("enemy-2" arena::died "." arena::cheer)))
  • What a scene keeps: each node's class, name, groups, metadata, and the properties whose values differ from a new node's, so a changed initform reaches every scene that never set the property; a property that holds a node as the node's path, one that holds a resource as Resources says; and the connections made with :persist t. Connect so where a scene is put together, as an editor does, and not in ready, which runs again each time the scene is loaded.
  • Properties go through make-instance: instantiate makes each node by giving it its saved properties as initargs, so initialize-instance methods see them, and the property metaclass checks them against their types, as for any make-instance. Only a property whose value holds a node is set afterwards, once every node is made. Values are made anew for each instance; resources are shared. Saving refuses a value that loading would refuse: one not of its property's type, or one that cannot be saved, such as a function.
  • Instances: the nodes instantiate makes have the root as their node-owner, and the root has the scene as its node-scene. A scene that holds an instance of another refers to that scene's file and keeps only what was changed in the instance's nodes, and added to them, since; loading applies the changes to a new instance. So a change to enemy.scene reaches every level that holds enemies. (node-instance-root node root) says which instance below root, the root of the scene meant, made a node, or nil for the scene's own: the editor's Scene dock shows a scene's own nodes and hides what its instances made, as Godot's does. (node-defaults node root) returns a new node whose properties are the defaults saving compares a node's with: a new object of its class, or for an instance, the same node of a new instance of its scene; the editor's inspector reverts to them.
  • Copies: (duplicate-node node) copies a node and what is saved under it, as Godot's Node.duplicate does, through the same packing as a scene: properties, groups, metadata and the connections among the nodes copied, an instance staying an instance of its scene, with what changed in it. A property that holds a node outside the copy cannot be kept, and is refused.
  • Files are S-expressions, read without evaluation and written the same way each time, so they diff well. A file names the classes it makes and the functions its signals call, so it is code: load only files you trust, after the systems that define their classes. A property a class no longer has is dropped with a property-dropped warning, and a change to a node that an instanced scene no longer has with scene-data-dropped.
  • Threads: load-scene and instantiate may run on any thread, such as a job-pool worker's, since the nodes they make are in no tree: add them to one with call-in-tree. pack-scene and save-scene run on the tree's thread.