A crossing game asks the player to track a dozen moving things at once. Almost every problem I hit
was the game being unreadable, or my own code disagreeing with itself about what was where.
Chimp Hop is a chimp getting out of a zoo. Six screens between the enclosure and the jungle — zoo, city,
highway, docks, river delta, forest — each a grid of lanes with something moving along them. Some lanes kill
you. Some carry you. You hop one tile at a time toward the top of the screen, and the difficulty is not the hopping,
it is knowing what you are looking at.
Six screens, not one scroll
The first decision was discrete levels rather than a single long scrolling map. A scroll is more impressive and it is
the wrong shape for this.
Each screen introduces its own vocabulary — traffic in the city, ships at the docks, crocodiles and logs in the
delta — and a screen is a unit the player can learn. You see the whole problem at once, you fail, you try
again knowing the layout. A continuously scrolling world never lets you hold the whole thing in your head, so
failure teaches you less, and the thing that makes a crossing game satisfying is that the tenth attempt is genuinely
better than the first.
The grid is thirteen tiles across and fifteen deep at sixty-four pixels each. Fixed, not responsive: the playfield is
the same shape for everyone, and it is the page around it that adapts.
The crocodile the logs were hiding
The best bug in the build, and it hid for a while because it was intermittent in a way that looked like bad luck.
In the river you stand on logs, and crocodiles float among them. The collision code walked the list of obstacles,
found the first one overlapping the chimp, and returned what to do about it. If that was a log, you were riding; if
that was a crocodile, you were dead.
The flaw is the word first. When a log and a crocodile overlapped each other — which happens
constantly in a river full of drifting objects — whichever one came earlier in the array won. Land on a croc
that happened to sit behind a log in the list and the game told you that you were riding a log, cheerfully, while
standing on an animal that should have ended the run.
The fix is to stop returning early: gather everything the chimp overlaps, then decide, with death taking priority
over riding. Which is the general lesson, and one I have now walked into more than once — if the answer
depends on all the matches, you cannot compute it by finding one. An early return is an optimisation that
quietly changes the question from "what is happening here" to "what is the first thing I noticed".
The crocodile got a design ruling at the same time. The whole animal is deadly, not just the head. Being allowed to
ride a crocodile's back is a classic of the genre and it is also a fiddly, pixel-hunting distinction on a phone,
where the difference between the safe part and the fatal part is a few millimetres of thumb.
The cars were driving backwards
Reported exactly the way real bugs get reported: the cars in the second lane from the top on level two look like they
are going the wrong way, but the other lanes are fine.
They were. Every sprite is drawn facing one direction, and the renderer was drawing them as supplied regardless of
which way the lane ran. In lanes that happened to move the same way the art faced, everything looked right. In the
others, vehicles were travelling backwards, and it read as wrong long before anybody could say why.
The fix is a flip on the horizontal axis for left-moving entities, which is two lines. The interesting part is the
failure mode: the bug is invisible in roughly half the game, so it never presents as "the renderer is broken", it
presents as one lane looking odd. Anything that is correct by coincidence half the time is going to be found late.
A related fix in the same area: potholes and traps are part of the road, not things on it, so they are drawn in their
own pass underneath everything that moves. A car passes over a pothole. Before that, the draw order said the pothole
was sitting on top of the car, and the road stopped reading as a road.
The road does not move
Lane textures started out scrolling with their traffic, because that is what you would reach for. They are now
completely static — tiled once and never moved again — and the code that offset them was taken out
entirely rather than switched off.
The reason is that in a game like this the player is tracking six or seven moving objects and computing whether a gap
will still be there when they arrive. Anything else that moves is competing for that attention and giving nothing
back. A scrolling road surface does not tell you where the cars are; it just makes the screen busier and the gaps
harder to see.
The same applies more strongly to the safe lanes. The rows where nothing can hurt you never animate at all, so
stillness itself carries meaning: if a row is not moving, you are not in danger on it. That is a rule the player
learns without being told, and it only works if it is never broken for decoration.
The touches that would not stop the page scrolling
Swipe controls did not work on a phone. Swiping to move also dragged the page around, because the handler's request
to cancel the default behaviour was being ignored.
The cause is that React attaches its touch handlers passively — a promise to the browser that they will not
cancel anything, which lets it scroll without waiting for JavaScript. It is a good default for almost every page and
exactly wrong for a game, and an attempt to prevent the default from inside one is discarded without complaint.
The answer is to bypass the framework for those specific events: attach native listeners with the passive flag
explicitly turned off. Not elegant, but the framework's convenience layer cannot express what a game needs here.
The HUD was sitting on the exit
The first attempt to fit the game to a phone produced a screen with a header-sized gap along the top and the score
display parked directly over the goal row — the row you spend the entire level trying to reach, hidden behind
the numbers telling you how well you are doing at reaching it.
The HUD came off the canvas entirely and moved below it, in normal document flow rather than floating on top. That is
better on a phone for a second reason: the bottom of the screen is where your thumb already is, and the top is where
your eyes need to be.
Overlaying information on the play area is a habit from big screens, where there is spare room in the corners. On a
phone in portrait there are no spare corners. Every pixel of the canvas is load-bearing.
A number the browser already knew
The game lives in a frame inside the site, under a header bar, and getting it to fit took an embarrassing detour.
The first version had the page send the game its header height as a message, which the game then subtracted from the
viewport. It worked. It was also a lot of machinery for one number, it needed the page to remember to send it, and
until the message arrived the game was sized wrong — so the first frame was always incorrect.
The realisation, arrived at only after building the complicated version: inside a frame, the full viewport height
is already the frame's height, not the page's. If the page sizes its frame correctly — which it must
do anyway, for its own layout to work — then the game needs to know nothing, subtract nothing and be told
nothing. The header-height message was the game compensating for a mistake in the page.
So the message went away and the page took responsibility for the size of its own frame. The best fix for a
configuration problem is usually to notice that the configuration should not exist.
Bananas, and a slow death
Two small things that shape how the game feels more than their size suggests.
Bananas are scattered through the levels and ten of them buy an extra life. The count carries across levels rather
than resetting — seven bananas collected on the docks are still seven when you reach the delta, and there is a
row of pips in the display so you always know how close you are. Resetting it per level would have made collecting
them pointless on any level you did not finish, which is precisely the levels where you most need the encouragement.
And dying takes a while on purpose: the chimp spins, shrinks and comes apart over about a second and a half. In a
game where you will die a great many times there is an argument for making that instant and getting on with it. The
argument against, which won, is that a fast death does not tell you what killed you. The animation is long enough to
look back at the moment and see the truck.