MarbleMagnificence

Dev log

Two players, one world, and two simulations that never talk

Multiplayer commit 3b77628

Both marbles roll around the same scene graph. Neither simulation knows the other exists, except for the one frame they touch.

Two multiplayer modes landed together: Twin Explore, a tvOS split screen for two controllers, and Battle Royale, an Apple TV arena that up to three phones drop into over the local network. They share one decision.

Isometric render of the Twin Explore basin: a wide grey floor inside a continuous yellow boundary wall, with a terraced mountain at the back, a long overlook to the right, a low ridge across the front, and a broken line of columns through the middle.
TwinExploreLevel, drawn headlessly from the same LevelData both simulations build their collision mesh from. No props, no gates, no gravity zones — nothing either sim could change and then disagree about.

One scene, two simulations

A GameSim is one marble, one camera, one collision mesh. Widening it to carry a second marble means every camera rule, ride and reset path grows a player index — and the single-player game, which is the actual game, pays for that forever.

So each player runs their own, and the rendering is what gets shared. One renderer, one scene graph, one copy of the level geometry, two SCNViews pointed at two camera nodes in the same scene. Player two’s marble is not a copy kept in step in player one’s view; it is the same node, drawn twice. Both sims build their own collision mesh from an identical level, so they agree about the world without exchanging a byte.

The bill for that

That only holds while nothing in the world can change. Props, gates and gravity zones are per-simulation state — the instant one player topples a statue, the two sims disagree about a world they are supposedly sharing, and the renderer draws whichever one it happened to ask.

TwinExploreLevel therefore has none of them, and the constraint is a test rather than a comment: “Level carries no state the two simulations could disagree about.” Boing pads survive it because firing one is an event, not state.

A quieter bill: both views share one set of materials, and the aerial-perspective band is derived from camera scale. Speed zoom is therefore off here. Leave it on and the distance fade pumps in player one’s half of the screen every time player two accelerates.

Battle Royale is the same shape with a network across it. The phone owns ordinary movement of its own marble and predicts its own camera, so packet timing never drives the view of the person holding it; the TV overrides that only for a collision or a respawn, each stamped with an epoch so a late packet cannot undo an impact that already happened.

Isometric render of the Battle Royale arena: a raised grey slab edged by a yellow bumper that breaks into four gaps, with eight small cover blocks scattered across it and the marble near the back.
The arena. The gaps in the bumper are the four knockout gates — reaching open air takes an intentional shove rather than one wide turn.

The one contact where both sides move

The rest of the physics is sphere-against-world — the solver pushes a marble out of triangles that never push back. Marble-against-marble is the exception, and the one thing neither sim can work out alone, so it lives on its own in MarbleContact: two states in, an outcome out, no level or gravity frame in sight. Which makes it the most testable physics in the project, and it has thirteen tests.

It is normal-only, no spin transfer. Not a shortcut: the marble’s roll is derived from its velocity rather than simulated, so there is no angular state for a tangential impulse to write to.

Both marbles take the full closing speed, not half each. They share one impulse, so a marble sitting still that gets hit at speed squashes exactly as hard as the one that hit it. Correct, and also the funnier read.

The separation is applied to the previous frame’s state as well as the current one. Render interpolation blends from the previous frame, so moving only the current state has the drawn marbles arrive at their separation a fraction late — sinking into each other on contact and popping apart after.

Costs

Contact runs once per rendered frame while the sim runs at 120Hz, so two marbles closing flat out can overlap by up to 0.43 of their 1.0-unit diameter before anything separates them. One frame of visible squish at a speed neither player reaches often, and nothing can tunnel: that worst case is well under a diameter. Making it exact means opening update into begin/substep/end so a caller could interleave two sims — a larger change to the sim’s contract than the effect is worth.

Neither mode is release content. The physics and the roster have tests; how it feels with four people shoving each other has been measured by nobody.