Eight numbers and a stack that will not jitter
Pebble Merge runs on a real rigid-body engine rather than a grid. That buys physics for free and costs us the illusion of control. Whether a stone feels like a stone is decided by eight numbers, and every one of them interacts with the others, which is why tuning this took days rather than the hour I assumed it would.
Why real physics at all
The obvious way to build a merge game is a grid. Drop a piece into a column, check the neighbours, merge. It is deterministic and cheap. It is also predictable in the worst sense, which is why so many games in this genre feel like spreadsheets wearing a texture pack. With a real engine, stones roll off each other and wedge into gaps, settling at angles you did not plan and could not have planned. The player is solving a spatial problem rather than filling in cells. I have never once enjoyed filling in cells.
The price is that nothing is guaranteed. Bodies can sink into each other and stacks can vibrate forever, and a single badly chosen constant will turn a jar of stones into a box of rubber balls, usually without anybody noticing for a while. Below are the eight numbers we ended up tuning, along with why each one sits where it does. None of them was obvious at the time. I want to be honest about that, because reading a finished table makes the whole thing look more deliberate than it was.
The eight
| Parameter | Value | What it actually controls |
|---|---|---|
| gravity | 1.0 | World gravity scale, or how heavy falling feels |
| restitution | 0.08 | Bounciness. Stone should not bounce |
| friction | 0.45 | Surface grip, which reads as rolling feel |
| frictionStatic | 0.6 | Resistance to starting to slide |
| densityScale | 1.0 | Bigger pebbles feel heavier |
| mergePop | 0.18 | Upward kick when two stones fuse |
| linearDamping | 0.02 | Slight air drag, to bleed off endless motion |
| settleSpeed | 0.65 | Speed below which a stone counts as settled |
Restitution is the one people get wrong
Restitution is the coefficient of restitution, meaning how much energy a collision returns. Everyone's instinct when tuning a physics toy is to raise it, because bouncing looks lively in a ten-second clip. Ours is 0.08. That is almost nothing. Accepting that it should be almost nothing took us a while, and I argued against it for most of that while.
The game is about stacking. Every bounce is energy that has to go somewhere, and in a stack of twenty bodies, energy that does not dissipate turns into jitter, which is what you get when stones never quite come to rest and vibrate against their neighbours indefinitely. At 0.08 a dropped stone lands and stops. At 0.4 the same stone lands, hops, nudges its neighbour, which hops, and the whole board shimmers. I tried 0.4 on purpose, expecting to like it. It reads as broken rather than as lively, and after a few seconds I wanted to put it back. A stone is supposed to be heavy. Heavy things do not bounce.
Settle speed is a game rule, not a physics constant
settleSpeed = 0.65 is the threshold below which we consider a stone at
rest. It is the most consequential number on the list and the least obvious one,
because it is not really a physics parameter. It is a game rule wearing physics
clothing. We keep forgetting that. Then something breaks in a way no amount of
re-reading the engine documentation explains, and we remember it again.
The game ends when a settled stone sits above the overflow line for long enough. That judgement depends entirely on what “settled” means. Set the threshold too high and stones count as settled while they are still visibly sliding, so the player loses to a stone that was on its way down. Set it too low and a board that is obviously doomed never triggers, because some stone somewhere is still microscopically moving. We landed on 0.65 with a hold time of 900 milliseconds. That is long enough to ignore a single unlucky frame and short enough not to feel arbitrary. I will also admit that the 900 was not derived from anything more rigorous than trying a smaller number, finding that it felt twitchy, and stopping there.
Friction does two different jobs
Friction and static friction are often set to the same value out of habit. They should not be. Ours are 0.45 and 0.6. Static friction governs how hard it is to start moving, while dynamic friction governs what happens once you are moving. Keeping static friction meaningfully higher is what makes a stack hold its shape: stones resist being nudged out of a pile, but once they do start sliding they travel rather than sticking halfway down the slope. That gap is the difference between a pile and something that behaves like a liquid, and it is the one change on this list I would defend without any qualification, because the obvious thing to try is setting the two values equal, and what you get then looks like wet sand collapsing in slow motion rather than like anything anybody would describe as stones.
The cost of a stable stack
Getting stacks to actually settle also required raising the solver's iteration counts, which I resisted for longer than I should have. The engine we use defaults to 6 position iterations, 4 velocity iterations and 2 constraint iterations. We run 8, 6 and 3.
Each iteration is another pass at resolving overlapping bodies and constraints, so more iterations means bodies sink into each other less and stacks stop drifting. It also means more CPU per frame, paid on every device including cheap phones, and that is the part that actually bothers me, because the people on cheap phones are not the ones who get a say in it. We raised them only as far as the visible artifact demanded. Going higher would have fixed a problem nobody could see while costing frame budget everybody would feel. That trade is not close.
Raising iteration counts is the blunt instrument, and I want to say that plainly, because it is the first thing most people reach for, including me. If your stack jitters, check restitution and damping first. Those remove energy from the system, whereas iterations merely resolve it more precisely, and a system that still has the energy in it will keep finding new ways to spend it no matter how precisely you resolve it. We reached for iterations second. We should have reached for them third.
They are not independent
The frustrating part is that none of these can be tuned in isolation. Raising gravity makes stones arrive faster, which makes collisions more energetic, which makes low restitution matter more. Lowering damping to make motion feel freer makes stones take longer to settle, which changes how the settle threshold and the overflow hold time behave. Raising merge pop makes fusions satisfying and also shoves neighbouring stones, which changes stacking. More than once I changed a single number, liked what I saw immediately, and then found, usually a few minutes later and usually on a board with far more stones on it than the one I had been testing against, that the improvement had quietly broken something sitting two headings away in the same table. That is the part of physics tuning nobody warns you about, and it is why the tuning never really ends.
Our practical answer was to stop tuning blind. The parameters are exposed in a panel inside the game and persisted to the browser's local storage, so a change can be made and felt immediately without a rebuild, and a setting survives a reload. That turned “which value is better” from a debate into something you can settle by trying it, which is a much better use of an afternoon than arguing. It is also why the eight numbers live in one object in the source with a comment above them saying they are the product. In this genre, they are. Honestly, they are most of the game.
What we would do again
- Keep bounciness near zero in any game about stacking.
- Treat “at rest” as a rule you own, with an explicit threshold and an explicit hold time, rather than a detail the engine decides for you.
- Separate static from dynamic friction and expect them to differ.
- Reach for damping before reaching for solver iterations, even though iterations feel more satisfying to turn up.
- Put the constants somewhere you can change them at runtime.
None of these numbers is correct in the abstract. They are correct for stones in a jar, falling about a screen's height, on a phone, and they took days of small adjustments to arrive at, most of which I could not reconstruct now if you asked me to sit down and do it again from a blank file. What generalises is not the values. It is knowing which number to suspect when the board starts behaving wrongly, and that is the only thing in this table I would genuinely miss if we deleted it, which is an odd thing to say about eight floats in a config object.