Second Chance DevlogThe Gazette

The coin pile never settled, and the frame rate was the reason

unreal-enginephysicsminigameunreal-engine-5.8

The betting shop in this game has a double-decker 50p coin pusher in it. Not an animation of one — an actual pile of several hundred simulated coins with two kinematic plates sweeping through them, a coin you aim and release down a moving rail, and folded notes wedged in the heap. The interesting engineering problem was never the machine. It was keeping a few hundred small, thin, contact-heavy rigid bodies stable and cheap enough to live in a shipping level.

This is what that took, including the failures, and one feedback loop that I think is worth the price of the whole entry.

What we were trying to do, and why it mattered

A pusher is trivial to fake. Play a looping shelf animation, roll a random number, spawn a payout. Every mobile version does roughly that, and it reads as fake immediately, because the appeal of the real machine is entirely in the pile: the way a stake wedges, the way one coin two rows back suddenly shifts, the way a note creeps forward a millimetre a stroke for a week. That behaviour is emergent. You cannot script it, and you cannot fake its tells.

So the pile is real. Every coin is its own rigid body with a single convex hull — a 50p is a heptagonal prism, which is genuinely convex, so one hull is exact rather than an approximation, and one analytic hull per body is what several hundred of them can afford. The coin is 27.3 mm across and 1.78 mm thick, and it weighs 8 g, because that is what a real one weighs.

That thinness is the whole difficulty. Almost every stability problem below traces back to a body whose smallest dimension is under two millimetres.

The tools and the method

Blueprint-only project, driven through two editor automation servers into a live Unreal session: one for Blueprint graphs and assets, one for editor Python, physics inspection and PIE. The machine's geometry is generated by a Blender script that also emits a manifest of every dimension; the import, placement, seeding and pile-baking scripts all read that manifest rather than carrying their own copies of any number. That mattered more than it sounds like it should — the one value that stayed a hand-typed literal, a height threshold on the player that decides "did you click the playfield or the payout tray", silently broke the collect action the day the cabinet was raised 30 cm.

The play harness is deliberately dumb: one script inserts a coin, another samples the world. One action per call, because the game only ticks between editor calls and never during one.

The trap: the pile never goes to sleep and the frame rate is in single figures

The symptom, in the words you would type into a search box: my physics objects never fall asleep, the frame rate is terrible, and my kinematic movers look like they are teleporting.

The first dense seed put 778 live coins in the cabinet. Measured with editor throttling explicitly disabled, that ran at 242 ms a frame — 4.1 FPS — with 6% of the bodies ever reaching sleep. The shelves also looked far too fast, so I nearly went and re-tuned their speed.

Both symptoms are one thing, and it is a loop:

slow frames → large physics timesteps → the solver never converges the pile → nothing

drops below the sleep threshold → every body stays awake → the frames stay slow

And the shelf speed was never wrong. At 242 ms a frame, a plate moving 10 cm/s advances 2.4 cm between frames. It does not push coins, it jumps through them and skids them. A kinematic mover's apparent violence is a frame-time readout in disguise.

The lesson generalises well beyond coin pushers: contact-heavy piles do not degrade gracefully. Measured cost across four pile sizes scaled at roughly N^1.6, not N. There is a size at which the system is fine and a size at which it is in a hole it cannot climb out of, and they are not far apart.

The instrument that was lying

Before any of that could be fixed, the measurement had to be trusted, and it could not be.

Frame time read from the world's delta-seconds inside an unfocused editor PIE went 19.7 → 81 → 163 → 400 ms with no matching change to the scene, and the 400 ms reading came from a pile smaller than the 173 ms one. That is not physics. That is the editor throttling a background window.

The correct method is to pull the engine's own stat group and compare the frame total against the game, render and GPU components. Any reading where the total hugely exceeds all three is throttle and must be thrown away. Throttled, the same scene reports 333 ms a frame with 42 ms on the game thread — which looks exactly like a physics overload and isn't. There is also a console variable to disable the throttling, and it has to be set inside the running PIE world; setting it on the editor world beforehand does nothing.

A related one, same family: I once "proved" a bug fixed with an instantaneous velocity sample — maximum upward velocity of 0.0 across 321 coins. Worthless. A coin flicking out of the machine lasts a few frames and the poll ran every twenty seconds. Measure the outcome, which persists, not the event, which does not. Counting coins that had ended up outside the cabinet footprint found the escapee immediately.

The settings that turned out to be load-bearing

Once the measurements were honest, the stability work was a short list. Each of these was paid for with a specific misbehaviour.

Position solver iterations, 16 instead of the default. At the default count the solver allowed roughly a millimetre of penetration between coin and bed. On a body 1.78 mm thick that is over half of it — so the sweeping plate rode over the front of a coin and dragged it backwards. It reads as "the pusher is broken" and it is solver tolerance. This is a per-body override, so it costs nothing globally.

Maximum depenetration velocity, 20 instead of the default 1000. A pusher is, by definition, a heap of wedged and overlapping bodies. At the default, resolving those overlaps fires coins across the room.

Maximum angular velocity raised to 20000. At the default, a body with a 13.6 mm radius is clamped below the spin rate it needs, so it permanently skids instead of rolling.

The moving plate is buried 6 mm below the bed surface. Coplanar contact between an advancing kinematic edge and a static bed let coins slip under the edge, get pinched, and launch. Burying the plate's underside costs nothing — the plate is kinematic, the bed is static, they never collide, and the buried part never renders.

One friction dial per surface, not per pair. Every physical material uses MAX combine with the override on, and the coin itself is set to near-zero friction. So the surface dictates behaviour and the coin merely complies: the bed grips at 0.30 so the pile can shed energy and settle, the plate at 0.35 so coins are carried rather than squirted, the rail at 0.16 so a released coin slides and never sticks. Tuning the bed cannot accidentally change how a coin behaves on the rail.

Sleep thresholds tightened on the materials, not the bodies. Engine defaults are far too sluggish for a 27 mm object; a coin at rest has to sleep quickly, but not so eagerly that a slow shelf nudge fails to wake it. One edit re-tunes the whole pile.

Worth recording: a kinematic static mesh genuinely does push Chaos bodies, with sweep off and teleport off, and Chaos wakes sleeping bodies on kinematic contact by itself, so no wake-the-pile tick is needed. Every other mover in this project uses teleport-true, which bypasses physics entirely — that would have been the wrong default here, and it took a purpose-built probe rig to establish which was true.

The bigger fix: author the drained equilibrium

Settings alone do not get several hundred bodies down to a sane cost. The pile itself has to start settled.

The pipeline is: seed a fresh grid, run it in PIE with the player parked far away until the coin count plateaus (about five minutes of game time), capture every coin's position and rotation to a bake file, then re-seed from that bake. What the player walks up to is therefore a machine that has been played all day, which is both what a real one looks like and much cheaper — a fresh grid throws a third of itself over the lip in the first minute, and a churning pile never sleeps.

Two things about baking that cost time.

A bake is only valid for the physics it was captured under. Change coin mass, damping, solver counts, a physical material or the bed geometry, and it must be recaptured. It also has to record the shelf phase, because the pile is packed against wherever the plates were at capture; re-seeding while they sit elsewhere shoves the entire front row on frame one.

And a stale bake looks exactly like a physics bug. After one geometry change, 61 of 203 baked coins were spawning 18 mm above bare bed, dropping on frame one and bouncing off each other. Nothing about that scene says "your bake is out of date"; it says "your physics is unstable". The check is arithmetic on the bake file — count how many baked coins sit in the old plate's span but not the new one — and it takes a minute against an afternoon of tuning the wrong thing.

Two more of the same species, both of which presented as physics failures and were not.

Coins seeded inside a kinematic slab. The plates sit on top of the beds, so where a plate is at rest the resting surface is the plate's top, not the bed. Seeding to the bed buried 793 bodies inside the slab and launched the lot — which looks precisely like the coins being too light. Seed spacing is asserted now too: centre pitch must exceed coin diameter plus twice the jitter, or neighbours are born overlapping.

The blade that pushed nothing. Adding a component to an empty Blueprint makes it the root, so moving it by relative location moved the whole actor. The blade was faithfully oscillating around the world origin, several metres from any coin. That reads as "kinematic bodies do not push Chaos bodies", which is a much more interesting and much more wrong conclusion. Diagnose a mover by reading its world location, never its relative one.

What the pile costs now, honestly

Measured with throttling off: 778 live coins at 242 ms a frame and 6% asleep; a fresh 321-coin grid at 45 ms; a baked settled 214-coin pile at 27.8 ms with 53% asleep; and after the redesign, 425 coins at 19.7 ms. The shipping pile settles to a few hundred on the field, with 45 more sitting frozen in the hopper as the machine's float.

Do not use sleep percentage as the settled test, though. High asleep counts were mostly the hopper — coins the machine had already collected and deliberately frozen. On the field itself the honest figure, on the short-plate build, is 281 of 284 coins at rest. The real performance saving is freezing what has fallen out of play and can never return. Freezing bed coins would be faking it: the plates reach nearly every coin on both decks, so frozen ones would visibly refuse to move.

Later, when the machine was transplanted into the open-world level, the whole pile learned to sleep when the player is more than ten metres away and wake on approach — recording exactly which coins were simulating at sleep time, so the frozen hopper stays frozen.

What is not finished, and I am not going to pretend otherwise

The machine did not pay out. As of the last measured session, sixteen consecutive inserts produced zero payouts through the front lip. The front row settles about 7 mm behind the edge, and a coin tips when its centre crosses the edge, not when it merely overhangs — so the row balances there forever. Moving the lip does not fix it: the pile just re-settles 7 mm short of the new edge, because the binding constraint is the drive, not the edge. The recommended fix is tilting the lower bed forward by a degree or two so gravity does the last centimetre. It has never been tried.

That bug hid for the entire build because the side gutters were shedding everything, so it presented as "the gutters take too much". Which leads to the single most useful method lesson from all of it: measure the split, never the headline percentage. Five separate tuning changes netted approximately zero, because a payout percentage cannot tell you where the money went. Each time one leak was closed, the loss relocated. Tracking coins on the field, coins credited and coins departed as three separate numbers makes the two failure modes distinguishable — rising departures with flat credits is a gutter problem, both flat is an absorption problem, and they need opposite fixes.

Also still open: the true frame cost at the largest pile sizes was never cleanly obtained; coins may still oscillate on the upper deck, where a plate carries them forward on the out-stroke and drags them back on the return, and the shortened plate may have fixed it but that was never re-verified; and while the machine has been driven by scripts and watched in PIE, no human has sat down and actually played it.

What to take from it

  • A physics pile has a cliff, not a slope. Cost scaled at about N^1.6 here. Find your

number by measuring, and treat "it runs fine" at one body count as no evidence at all about a count 50% higher.

  • Slow frames cause instability, not just poor presentation. Large timesteps stop a

pile converging, which stops it sleeping, which keeps the timesteps large. Fix the frame time before touching a single tuning value.

  • Check the instrument before believing a bad number. A frame-time reading that moves

twentyfold with no change to the scene is measuring your window manager.

  • Measure outcomes, not instants. A poll every twenty seconds cannot see a three-frame

event. Count what it leaves behind instead.

  • Thin bodies expose solver tolerance. A millimetre of penetration is nothing on a crate

and more than half of a coin. Raise position iterations per-body rather than globally.

  • Half of "the physics is broken" is not physics. A bad spawn position, a stale bake, a

component that is secretly the actor root — they all present identically, so rule out the boring causes first.

← All devlog entries

Watch it get built. All of this goes up on YouTube as it happens — broken animations, buildings hovering a foot off the ground, the lot.

Subscribe on YouTube