MarbleMagnificence

Dev log

The level editor saves a diff, not a level

Tooling commit 94f563b

Placing blocks by hand usually means abandoning the generator that made the level. This keeps both, by never writing a level file.

The Mountain is generated: five octagonal terraces and a set of ramps falling out of a height field in MountainLevel.swift, so “make the terraces wider” is a constant you edit rather than a level you rebuild.

A level editor usually eats that property. Once a tool writes out a level file, the generator becomes the thing that produced the starting point once, before a human took over. So MarbleEditor does not write a level.

What it writes

mountain.edits.json records only what you added and what you removed. At launch, GameBoot applies that diff over MountainLevel.make(). The generator stays in charge of the shape; hand edits survive it changing.

Removals are keyed by cell and block type rather than by index. That is the detail that makes it hold: reorder the generator, add a terrace, change the routes, and the overlay still knows which block you meant to delete. The applier is LevelOverlay — pure data, in MarbleCore, where tests reach it without a simulator.

Contact sheet of 36 block shapes on pale tiles: arches, chamfers, inside and outside corners, curves, a bowl, a bullseye, a funnel, a hill, a ridge, a saddle, and a run of slopes, each drawn in yellow and grey with its triangle edges visible.
The palette the editor places from: every case of BlockType, rendered by marble-ci-renderer --block-gallery from the same MeshData used for collision. Wireframe on, so the triangles the physics actually sees are visible.

The tool

A palette of every block type and bundled scenery prop, with the selection following the cursor as a ghost preview. Q/E rotate, R resets, F cycles a prop’s footprint, ⌘-click or X deletes, ⌘S saves.

Clicking places against whichever face the pointer is on — on top, beside a wall, under an overhang — so placement follows the surface normal rather than grid coordinates. The camera is orthographic at the game’s own isometric pitch: a tool showing a different projection than the game lies about what you built.

The refactor inside the commit

SurfaceGeometry.swift pulls the face-orientation split and the MeshData → SCNGeometry conversion out of SceneKitRenderer — about 90 lines leaving the renderer to become 112 shared ones.

Editor and game now bake pixels through the same code, which is why the ghost preview is worth trusting: what you see under the cursor is the block, shaded by the renderer’s own path. MarbleEditor is a third target over MarbleCore, alongside the iOS and tvOS apps, and the game never links it.

The trade

Edits reach the game at the next build of MarbleApp, not live. Save, rebuild, roll. The alternative was a file watcher and a live level-swap path in the running game — real complexity in the part of the codebase least able to absorb it, for a few seconds per iteration.