Odd & Out: a co-op alien game built in six days

· 4 min read · game-dev, three.js, webgpu, multiplayer, performance

An isometric view of a green floating island with trees, a winding road, a pink car and a mushroom-shaped alien next to a glowing coin

Odd & Out is a free co-op game for the browser where two aliens, Odd the mushroom and Out, have crashed on Earth with a broken ship. Every game has new floating islands to cross, with elevators between levels, odd creatures to avoid and ship pieces to find.

You can play solo, online with a friend through an invite link, or in split screen on one computer, with keyboard, gamepad or touch.

I built it in six days, from October 7 to October 12, 92 commits in total, and that was only possible because it did not start from zero. Its renderer, post-processing, stylized lighting, input and loading all came from Lantern Valley. This post is about what I had to build on top of that: the worlds, the multiplayer and the phones.

A green floating island seen from above, with a winding road and a mushroom-shaped alien

A world is just a number #

Every island is generated from a seed. The generator is plain numbers, with no Three.js in it, so it runs and is tested in Node:

const shape = islandShape( 7, { radius: 14 } );
shape.radiusAt( angle )    // distance from the centre to the coast
shape.inside( x, z, 0.4 ) // is there standing room for something 0.4 m wide?

The coast is a radius for every heading, wobbled by noise sampled on a circle, so the two ends meet and the shoreline closes without a seam. Several noise octaves, some big lobes and bays, medium bumps and small ragged detail, make sure no island looks like a circle.

The tests say what matters: the same seed gives the same island, different seeds give different coasts, the coast closes and props land inside it, apart and out of the clearings.

Later the islands became irregular terrains, with lobes, necks, holes, islets, lakes you can swim in, rivers, moats with bridges, rope and stone bridges, asphalt streets and slippery ice. The invite link carries the seed and the terrain style, so both players build the same worlds.

Co-op without simulating anything twice #

Because a world is a number, multiplayer got much simpler than I expected. Each player runs the game on their own computer. The server does not simulate the world, it only relays three things:

  • where each hero is (about 15 times per second),
  • which hero each player picked,
  • which ship parts have been collected.

The seed travels in the invite link, ?room=CODE&seed=N. Both players generate the same islands from the same seed, so nobody has to send terrain. The creatures, the fog and the hearts belong to each player. The ship parts and the map are shared, and a friend shows up on the minimap when you are on the same level.

The server is a Cloudflare Worker with one Durable Object per room, the same scheme I used for Threejs-Punk. A room takes two players at most.

The phone that got hot #

The first phone builds ran warm, and my first guess was pixels. It was wrong. The internal resolution was already small, and with the CPU throttled 4× (a stand-in for a phone's main thread) the frame was dominated by JavaScript: about 235 draw calls and about 30 post-processing passes, just to be encoded for a GPU that sat mostly idle.

What counts is how many draws, how many passes and how many frames per second:

Before After
Draw calls per frame ~235 128
Post-processing passes ~30 17
GPU time per frame 1.98 ms 0.96 ms
JavaScript per frame 9.0 ms 4.9 ms
Main thread busy (touch, 30 fps cap) 54% 18%

The biggest single change was a frame-rate cap: 30 fps by default on touch screens, 60 on desktop. A 120 Hz phone used to draw 120 frames a second for no reason. A "lite" layer, on by default on touch, keeps the same bloom and depth of field in fewer passes (bloom from 12 passes to 5, depth of field from 10 to 5) and redraws the sun's shadow map at most 30 times a second.

The game on a phone, with a virtual stick under the left thumb and a jump button on the right

The rest was making the game stop doing work nobody could see: creatures and the merchant outside the view are not drawn, coins and chests under the fog are skipped, and shaders do not compute the fog where everything has already been explored.

The first visit #

On a cold first visit, a game like this used to take about 40 seconds to be ready, mostly compiling shaders. The same lesson as in Threejs-Punk and Lantern Valley, with a different set of numbers:

Before After
Cold first visit ~40 s ~12 s
Shader modules 573 144
Synchronous pipelines 328 68

Same picture, three changes: one compile for all levels instead of one per level, instance matrices passed as instance attributes and the shader text normalized so equal programs share a module, and colors as uniforms, so there is one shader per material and not one per color.

Gags #

Most of the late commits are jokes, and I think that is the right ending for a game like this: trampoline mushrooms that throw you higher if you fall on them again, a banana peel, a human cannon that shoots you over the island, a cow taken by a flying saucer, sharks in the big lakes and a piano that falls from the sky and turns you into a pancake.

Play it #

Odd & Out is free at odd-and-out-game.vercel.app, and the project page here has the video: Odd & Out.

The lesson I keep from it is that a good foundation is the most effective optimization. Most of the six days went into content because the engine already existed.

Related projects