Game Rig

Home/Blog/Merge physics

Why merge games feel good

A merge game is a stacking solver wearing a scoreboard, and the two halves are tuned against each other rather than together.

Suika Game appeared on the popIn Aladdin ceiling projector in April 2021 and reached the Japanese eShop on 9 December 2021, written in Unity by Aladdin X. Its rules fit in a sentence: two touching fruits of the same kind become the next fruit in an eleven-step ladder. The part people describe as satisfying is not the ladder. It is the behaviour of circles under a solver, and that behaviour is four constants.

Merge Drop is the same machine with seven tiers instead of eleven. Orbs of radius 16, 24, 34, 46, 60, 76 and 94 pixels fall into a tank, and two touching orbs of one tier leave behind one orb of the next. The radii are not geometric. The gaps run 8, 10, 12, 14, 16, 18, so every step up grows the orb by less than the step before it, and the top of the ladder is where the tank gets tight fastest.

The first version here ran restitution at 0.9, on the theory that bouncing is what physics looks like. A body dropped from the top of the tank came back 81 percent of the height it fell, every orb skittered off the pile, and nothing ever stacked. Height recovered goes as the square of the restitution coefficient, which is worth internalising early: 0.9 is a superball, and a piece of fruit is not.

Why 0.2 is the restitution a merge tank wants

Merge Drop settles on REST = 0.2 between orbs and REST_WALL = 0.16 against the tank. A body dropped the full height of the tank returns about 11 pixels on the first bounce, which reads as mass rather than spring. The reason the coefficient is not zero is the wedge: an orb arriving at speed with no rebound at all dies exactly where it lands, and the worst place to land is the seam between two others.

Both numbers sit below what the mainstream engines ship, which surprises anyone who expects a physics library to be bouncy out of the box. Box2D's b2FixtureDef defaults to friction 0.2 and restitution 0.0, skipping the rebound entirely below an approach speed of one metre per second. Matter.js defaults to restitution 0, friction 0.1 and frictionStatic 0.5, and takes the larger of the two bodies' restitutions for a contact. Neither engine bounces a circle unless it is told to.

The threshold is the idea worth stealing. Merge Drop uses BOUNCE_MIN = 45 units per second: below that approach speed the rebound term is set to zero for that contact, and above it REST applies. A pile of circles left in permanent low-amplitude vibration is the most common way a merge game reads as broken, and a velocity floor is the cheapest available cure for it.

The pile that never quite goes to sleep

Nothing in a discrete solver comes to rest. It comes to a low-amplitude argument. Each step every overlapping pair is pushed apart by a fraction of its penetration, and four iterations over a list that is changing underneath them do not cancel cleanly, so the pile breathes by a fraction of a pixel forever.

Three constants decide how bad the breathing is. Merge Drop runs four solver iterations per step at a fixed 1/120 second, corrects 70 percent of any penetration beyond SLOP = 0.5 units, and pools 280 orbs. The slop is an allowance rather than a tolerance: a pair overlapping by less than half a unit is left alone, because chasing the last half-pixel of overlap is precisely what turns a settled pile into a buzzing one.

Nothing here sleeps. Box2D and Matter.js both stop simulating a body after it has been near-stationary for long enough, which fixes the jitter and the frame cost together. Without it, every orb is integrated every step, including the thirty at the bottom that have not moved in a minute.

What Merge Drop's seven tiers cost in area and pay in points

The ladder is the difficulty curve, and Merge Drop writes it as two arrays that turn out to be related. Radius climbs arithmetically. The score awarded for a merge into tier k is the triangular number 5k(k+1), which is why the payout grows by 10, then 20, then 30, then 40, then 50, then 60.

The seven tiers of Merge Drop, the area each orb occupies, and the points a merge into it awards
TierRadius (px)Area (px squared)Points on merge
1241,81010
2343,63230
3466,64860
46011,310100
57618,146150
69427,759210

The radii and the points come straight out of the game's source. The areas are pi times the radius squared, which is arithmetic on the page rather than a measurement, and they are what makes the table worth reading. Going from tier 5 to tier 6 costs 53 percent more of the tank and pays 40 percent more points. Going from tier 1 to tier 2 costs 101 percent more and pays 200 percent more. Early merges are loud and cheap, late merges are quiet and expensive.

That shape is not an accident, and it is what makes the last thirty seconds of a run tense. The payout rises with every tier, so a large orb is always worth more than the two that made it, while the space it needs rises faster than the payout. Holding a tier-6 orb and waiting for its partner is a bet on a tank that is already more than half full.

MAX_TIER = 6 caps the ladder at the top. Two tier-6 orbs do not produce a tier 7; they vanish and pay TOP_BONUS = 1500 instead. A nullable next-tier index cannot express that, and it is the mistake most clones make in their payout rules: the absence of a next tier is not the same as the absence of an outcome.

One merge per orb, and the three-body contact

The bug that cost the most time here was not in the solver but in the mapping from contact to rule. Three same-tier orbs resting in a line produce two contacts in one batch, one per adjacent pair, and a handler that reacts to each contact on its own consumes the middle orb twice and turns three orbs into two for a single payout.

Merge Drop never sees that problem, because it does not read contact events. Its merge pass walks its own body list in one synchronous sweep, skips any orb already marked dead, and breaks out of the inner loop after a successful merge. The only hitbox in the whole exercise is a circle test against the sum of two radii, which is the cheapest one there is. The newborn inherits the average of its parents' velocities, which for two equal masses is also the momentum-preserving choice, and its position is clamped inside the tank, because a fused orb is wider than either parent and can otherwise be born through a wall.

A game built on engine contact callbacks needs the other design. The fix is a reservation set of body ids owned by the handler: both ids are tested against the set and added to it in the same synchronous block, before any deferred removal, so the second contact in the batch finds its partner already claimed and returns. Deferring the world mutation to the next frame then obliges the callback to revalidate that both bodies still exist, because a restart can empty the board between the contact and the merge. The sweep costs O(n squared) in live orbs, so a pool of 280 can in principle run 39,000 distance tests in a step, which a tank holding a few dozen orbs can afford and a tank holding hundreds could not.

Pros and cons

Pros

  • A circle solver gives the pile its shape for free, with no stacking rules and no grid, and the only test a merge needs is a distance against the sum of two radii.
  • Two constants carry the whole feel of the drop, and both are readable as physical intuition: 0.2 is heavy, 0.9 is a superball.
  • The payout table is separate from the simulation, so the weight of the drop and the difficulty of a run can be moved without touching each other.
  • A chain reaction uses the same code path as a single merge, so three orbs collapsing into one costs nothing extra to write.

Cons

  • A pile that never sleeps jitters, and every cure, from a velocity floor to more solver iterations to a real sleeping-body system, spends frame time.
  • Merge order inside one sweep leaks into the score, so two runs agreeing is a property of the loop rather than of the simulation.
  • The numbers that make the game feel fair are the ones the player never sees, which makes them the hardest to justify spending a day on.

Sources

  • Wikipedia: Suika Game — the April 2021 projector debut, the 9 December 2021 eShop release, Aladdin X, and the eleven-step fruit ladder.
  • Box2D: b2_fixture.h — the friction 0.2 and restitution 0.0 defaults, and the one metre per second restitution threshold that skips small bounces.
  • Matter.js: Body — restitution 0, friction 0.1 and frictionStatic 0.5, and the rule that pairwise restitution takes the larger of the two bodies.
  • Preventing Double Merges in a Fruit-Drop Physics Game — the middle-body double-consume bug and the reservation set of body ids that prevents it.