Devlog, Engineering
Ragnaroll devlog #1: flicking real 3D dice with Three.js and Rapier
First devlog for Ragnaroll, my Norse dice roguelite — why the dice are real rigid bodies, how Rapier and React Three Fiber run the table, and the fight to make a flick feel fair.
Indie game developer · Aarhus, Denmark
Ragnaroll is the game I describe as Balatro and Hades having a dice-obsessed child: flick six dice across a dead god’s felt table, score poker hands from where they land, forge runes into the dice, and fight through fifteen Norse realms. This is the first devlog — and it starts with the decision everything else hangs on.
The dice are real
Most digital dice are theatre: the game rolls a random number, then plays a tumbling animation that lands on it. Efficient, controllable — and hollow. Players can smell it.
In Ragnaroll, the dice are rigid bodies in a physics simulation. When you flick one, your finger’s velocity becomes an impulse; the die tumbles, collides with the rim, knocks its neighbours, and settles wherever physics says it settles. The face you score is read from the resting orientation of the mesh. Nothing is predetermined. The randomness sits on the table where you can watch it happen — and later in the run, bend it with runes.
The stack: R3F + Rapier
The table is rendered with Three.js via React Three Fiber, which lets me express the scene as components while keeping full control of materials and lighting — a felt table at night, dice that catch the forge light.
The simulation is Rapier — a physics engine written in Rust, compiled to WebAssembly. It’s fast enough to run six convex-hull dice plus the table at a locked timestep on a phone, and its rigid-body API maps cleanly to what dice need: restitution, friction, angular damping, sleep thresholds.
Game state — the run, the realm, Spirit, the Forge — lives in Zustand stores, strictly outside the render tree. React renders the state; it never is the state. That separation is what keeps a physics game in React from melting.
Reading a die without lying
When the simulation sleeps, each die reports its face by comparing its local axes against world-up — largest dot product wins. Two failure cases needed real work:
- The leaner. A die resting against the rim at 30 degrees is not a result. If no axis dominates decisively, the die gets a gentle nudge impulse and the table waits.
- The eternal wobble. Sleep thresholds have to be tuned so dice settle quickly but never freeze mid-tip. Too aggressive and you score a die that was still falling; too lax and the game feels sluggish.
The real boss fight: making a flick fair
The hard problem isn’t rendering or physics — it’s feel. A flick must be chaotic enough to be gambling and readable enough to be skill. That line lives in the numbers: dice mass, felt friction, rim restitution, how finger velocity maps to impulse, and how much spin a flick imparts.
My tuning loop is brutally physical: change one parameter, throw two hundred dice, log the scatter. Too bouncy and results feel random regardless of skill; too damped and good players just place dice like chess pieces, which kills the drama. The current build sits where a deliberate soft flick can aim a die at a cluster — and still gasp when it clips the rim.
Where it stands
Playable builds exist and are eating my evenings, which is the best sign there is. Firebase telemetry is wired into the playtest builds so tuning arguments get settled by data. Next post: the Forge, and how carving runes into individual dice turns a random toss into an engine you built on purpose.
Follow along on the Ragnaroll page or the RSS feed.