Skip to main content
Part of the scene-scripting skill. The skill links here when your agent needs the detail.
Every recipe below is the source of one call: summer_run_script(scenePath="res://<the scene>.tscn", source=<the recipe>). Name the scene every time; it is opened first when it is not the edited one, and the person’s tab comes back after the run. The core helpers (add_node, find, add_mesh, make_material, save_scene …) are in the scene-scripting skill itself. These use the game-completeness tier (ctx-api.md, next to this file); on an engine that predates it, use the fallback table there.

2D platformer level — tilemap paint, character body, camera limits, parallax (game-completeness tier)

One script lays down the level: paint the ground, drop a character body with collision, bound the camera to the level, stack parallax behind it. Tile coordinates are cells; camera limits are pixels (cells × tile size — 32 here).
Compare tiles_painted with the cell areas you asked for (256 + 6 + 3 here) — a short count means cells were skipped. Then summer_snapshot_diff: added must list Ground, Player with its CollisionShape2D child, the camera under Player, and the Background group with one Parallax2D per layer. Screenshot target:"scene" nodePath:"Player" to see the body standing on the floor row, then the whole scene to see the platform is reachable. Movement needs the controller script and the input actions — two recipes below. Want a working baseline instead of a blank scene? The 2d-platformer template already has tilemap + CharacterBody2D + parallax wired.

Animated sprite (game-completeness tier)

add_animated_sprite builds the SpriteFrames resource from per-frame texture paths — one entry per animation, fps and loop per clip — and optionally autoplays one. Cut a sheet into frames first (assets.slice_sheet, or the sprite-sheet skill when generating them).
A missing texture path comes back as a report entry — read it before wondering why a clip is short. Which clip plays when is gameplay code (anim.play("run") from the controller’s _physics_process), not scene setup. Verify by playing: summer_play, move, summer_screenshot target:"game" — an edit-time preview shows frame 0 only.

HUD — canvas layer, margin, vbox, label/progress/button, anchors, theme overrides (game-completeness tier)

props.anchor runs set_anchors_and_offsets_preset — the right way to pin a Control; hand-set anchor_*/offset_* values drift the moment the window resizes. The preset is applied before the other props, so an explicit size/position in the same dict wins. add_ui("panel", …) is a PanelContainer (it sizes to its child — a backdrop for a vbox/hbox); a plain Panel is add_node("Panel", …). set_theme_overrides returns how many overrides it applied — compare with what you passed (an int on a key that is not font_size becomes a constant override, as with the margins above). Unknown props keys land in prop_warnings as always (use real property names: custom_minimum_size, not min_size or a guess). HUD Controls belong under a CanvasLayer — a Control parented under a Node2D scrolls with the camera and gets a prop_warning. For the layout and theme design — which container for which job, theme vs inline styling — use ui-basics; this is its one-script form.

Wire a button with connect_signal() (game-completeness tier)

ctx.connect_signal makes a persisted connection (CONNECT_PERSIST) — it survives in the .tscn, exactly what the editor’s ConnectionsDock writes. The target needs a method to call, so attach the script first; a script attached in the same run has not compiled yet, and connect_signal accepts it because the source declares func _on_pause_pressed( (the method gate):
Never wire with a bare button.pressed.connect(...) inside the run script: that connection lives only in the editor process and is not saved. And the helper is connect_signal, not connect — ctx.connect(...) is Object.connect on the ctx object itself and fails with a wrong-signature error. true from ctx.connect_signal says the connection was made and flagged persistent (an already-connected pair also returns true, with already_connected in the report); both ends must be owned by the edited root or the .tscn cannot carry it. The proof is summer_play, click, summer_screenshot target:"game".

attach_script — behavior with the parse-error loop (game-completeness tier)

attach_script validates the source before writing anything: a parse error comes back as a report entry (with the line) and the return is null — no half-written file, no broken node. Empty path → res://scripts/<SceneName>/<NodeName>.gd.
The loop: read the exact parse error (line and identifier) from the report → fix that line → re-run. Do not guess-and-mutate. An existing file at path is overwritten and its previous sha recorded in the report — the pre-run checkpoint is the rollback for that, not the undo action. Idioms (typed declarations, signals, _ready vs _process): gdscript-patterns. Later edits to a script already on disk: summer_write_file + summer_get_script_errors.

make_prefab — turn a built subtree into a reusable .tscn (game-completeness tier)

Build the thing once in-scene, then pack it. With replace_with_instance (the default) the inline subtree is swapped for an instance of the new file — same name, transform, parent — and the instance is returned.
The owner fix-up runs before packing, so a child you forgot to own is included instead of silently dropped. The prefab is authored at its origin — the node’s position is zeroed for the pack (the editor’s Save Branch default) and copied back onto the instance, so first.position above is still where the torch stood; rotation and scale stay in the prefab. make_prefab refuses the scene root (pack a child subtree, never ctx.get_scene_root()) and a path that is open in an editor tab. The receipt: res://prefabs/torch.tscn in the result’s files_written, Torch now reporting a scene_file_path, and the copies in the diff’s added. From here on, edit the prefab file, not the instances.

Input actions, autoload, main scene (game-completeness tier)

Project-level wiring in one run — the platformer controller above already reads these actions:
An unknown event shape → report + false, never a half-written action (every event is built before anything is written). A new action gets the engine’s default deadzone, 0.2; re-running on an existing action keeps its deadzone and appends only the events it does not already match, so the recipe is safe to re-run. Keycode strings are find_keycode names ("Space", "Left", "Ctrl+S" — modifiers become the event’s modifier flags). Write the autoload’s script before registering it (summer_write_file, or attach_script on a node you then make_prefab) — add_autoload applies the editor dock’s gates (valid identifier, no class-name collision, an existing res:// Script or PackedScene). These helpers write the res:// path form ("*res://…", "res://…"); the editor UI writes uid:// forms and both resolve — leave them be. They write project.godot, not the scene: the result’s undo_action note lists them under project_settings_changed, outside the scene undo — checkpoint territory. Proof: summer_play → summer_get_runtime_tree shows /root/GameState; the keys move the body in a target:"game" capture.

Verifying 2D and UI work

Same loop, different framing:
  • Diff first. added must carry the bodies’ CollisionShape2D children, the CanvasLayer with its Controls, one Parallax2D per layer. Anchors and theme overrides show up as changed properties on the Controls.
  • 2D scene captures. summer_screenshot target:"scene" on a 2D scene synthesizes a Camera2D and auto-fits the CanvasItem bounds — the 3D presets do not apply, and framing:"camera" errors on a scene with no 3D content. nodePath:"Player" frames one node (its 2D rect, children included); size:[1280, 720] sets the resolution that anchors resolve against.
  • UI and anything input-driven is only proven in the running game: summer_play, then summer_screenshot target:"game". An edit-time preview of a HUD approximates a viewport the editor does not have.
  • Read the confession fields in every capture (synthetic camera, no camera, project mismatch). A capture you did not receive is not evidence — verifying-scenes has the full discipline.

Pitfalls


Source: library/skills/scene-scripting/references/recipes-2d-ui.md, rendered from summer-engine@4.3.0 on npm.