Inspiration

We grew up on couch games, the kind where four people crowd one screen and everyone's yelling. We wanted that again, except the people we play with now are spread across iPhones, Android phones, Macs and Windows laptops, and half of them aren't in the room.

So we gave ourselves one rule early on: BoatBrawl runs on iPhone, iPad, Android, Mac, Windows and the web, and all of them can join the same online match. If you're at home with friends, you should be able to play on one screen with whatever controllers and phones are lying around.

Boats came from wanting the arena to fight back. On water you get whirlpools that drag you in, currents that push you off your line and geysers that throw you in the air, and walls decide which weapons can actually reach you.

What it does

BoatBrawl is an arcade boat-combat game for up to 8 players. You can play against bots, online, or on one screen with friends.

You grab ? crates and fight with whatever comes out. There are 10 weapons:

  • torpedoes
  • a hold-to-fire deck gun
  • a snowball blaster that freezes a boat so you can ram it and shatter it
  • depth charges you lob over cover
  • the Maelstrom, a slow nuke that hits very hard
  • sea mines and decoy buoys

Power-ups like the invincible Ironclad ram and the spinning Cutlass mace change how a fight goes.

There are three arenas, and each one plays differently. Beacon Bay is an open bowl with a whirlpool in the middle, Tropical Forest has a waterfall and vines, and Arctic Vale has falling snow and icebergs. Currents, geysers and boost pads change the lines you take through all three.

Ways to play:

  • Offline against bots
  • Online with Find Match (bots fill empty seats, so a match always starts)
  • A private room code for friends, with a leaderboard and a quick ready-up for the next round
  • Local split screen for up to four players

Xbox, PlayStation and other gamepads work on every platform, with rumble. If someone doesn't have a controller, they scan a QR code and their phone becomes one in the browser. There's nothing to install, and they can steer by tilting.

There are six boats. Four are free and unlock as you level up, and the optional Premium Boats pack adds Eclipse and Imperial Tide.

How we built it

One Kotlin codebase. The whole game is Kotlin Multiplatform, and every menu and screen is Compose Multiplatform. The shared module is around 70,000 lines and holds the game rules, physics, netcode, audio and input. The same fixed-step engine runs on players' devices and on our server.

Our own renderer. We started on a third-party engine and outgrew it, so we wrote our own: OpenGL ES on Android, Metal on iPhone and iPad, OpenGL on Mac and Windows, and WebGL2 in the browser through Kotlin/Wasm. Boats load from .glb files through a glTF loader we wrote, and split screen draws every player's view into one GPU surface.

Online play. A Ktor server on Google Cloud runs the real game engine at 60 Hz and sends out 20 snapshots a second.

  • Your own boat moves the instant you press a button.
  • When the server's state arrives, your device corrects to it and replays the inputs the server hasn't seen yet.
  • Everyone else's boat is smoothly interpolated.

Game state goes over an unordered WebRTC data channel, so one dropped packet on a bad mobile connection doesn't freeze the match. If WebRTC can't connect, a WebSocket takes over. Servers are versioned by URL, so shipping an update doesn't lock out people who haven't installed it yet.

RevenueCat. Four boats are free. Premium Boats is a one-time lifetime purchase behind a single premium_boats entitlement.

  • On iOS and Android, we call the purchases-kmp SDK straight from shared Kotlin and use RevenueCat's native Paywall and Customer Center. Debug builds use the Test Store, and release builds use the real Apple and Google keys.
  • The Mac App Store build runs on the JVM, and RevenueCat has no SDK for that, so we wrapped their Apple SDK in a small Swift library called through JNA.
  • Customers are identified by the same Firebase user ID the game uses, so a purchase follows you to your other devices. Restore Purchases is in Settings.
  • The paywall only opens when you pick a locked premium boat. You never see it at launch or on a free boat, and nothing unlocks until the entitlement is confirmed.
  • On Android, there's one ad after each online match, through RevenueCat's AdMob integration. Premium Boats owners never see ads.

Accounts and progress. Firebase Auth with Google and Apple sign-in, plus Google Play Games and Game Center. Only the server writes levels and unlocks, so a modified client can't fake progress.

Tests. There are more than 600 automated tests, including deterministic replays of bad network conditions.

Challenges we ran into

Muting the game made it faster. On an iPhone 15 Plus, the game ran 6 fps slower with sound on, and 8.8% of frames were late. Native audio calls were running on the game loop. We moved every audio call to a single background worker, and now sound on and sound off both measure about 59 fps.

Android said 60 fps while the game ran at 39. The screen kept re-showing the last frame while the game itself was stuck, with some frames taking 350 ms. We fixed it in five steps:

  1. Let the GL thread run on its own instead of waiting on Compose.
  2. Rendered at 0.8 scale.
  3. Used cheaper shadows.
  4. Turned on sustained performance mode.
  5. Asked the phone for its 60 Hz display mode.

After that, 47 of 50 two-second windows held a steady 60.

Making online play feel smooth. One slow phone could freeze the whole match, inputs piled up behind a busy connection, and boats would surge forward and snap back. We tracked each of these down with deterministic replays and fixed them one at a time.

Why the server decides. Our simulation is almost deterministic, but atan2 gives a slightly different last bit on Android, the JVM and iOS. Over a full match, that's enough for two players to see different winners, so the server is the only source of truth.

Four controller systems. Windows, Apple platforms, Android and the browser all name buttons and flip axes differently, and tilt steering drifted or inverted on some devices. We normalised all of it so every player keeps their seat and their controls.

Selling on the Mac. With no RevenueCat SDK for a JVM app, we built the Swift bridge ourselves. Getting signing, entitlements and App Store review right took a lot of back and forth.

Accomplishments that we're proud of

  • It's live on the App Store and Google Play, with Mac, Windows and web builds from the same code, and everyone can play together.
  • Your phone becomes a controller with one scan, with no app and no account.
  • The paywall never nags. It only shows up when you want a premium boat, and buying one removes ads.
  • Every performance fix was measured on real devices, in release builds.
  • We open-sourced the fixes as Joyframe, a Kotlin Multiplatform library on Maven Central, and wrote a detailed how-to for other Kotlin developers.

What we learned

Sharing code across platforms was the easy part. The real work was making the game feel right on each one: smooth frames, fast response and controls that make sense for the device.

Average fps hides the frames players actually notice, and a phone can report 60 while the game is stuck. So we measure frame intervals in release builds on real devices.

In a real-time game, letting the server decide is simpler than trying to keep every device in perfect agreement.

Cross-platform purchases turned out to be mostly about identity. RevenueCat saved us from building a separate integration for every store. The work left for us was making sure an entitlement follows the player everywhere.

What's next for BoatBrawl

  • More arenas and boats, built with our in-house map editor
  • New premium boat packs
  • Ranked play and seasonal leaderboards
  • The Windows version on the Microsoft Store

Built With

Share this project:

Updates

posted an update —

Shipaton is over, but we didn't stop building

Since we submitted BoatBrawl: My colleague Ashwin built two brand-new 3D games on Joyframe, our open-source Kotlin Multiplatform game library, using Claude Code and about an hour per game: Island Panic (a 6-player couch brawler where your phone is the controller) and SkyFall (a skydiving race down a desert canyon) BoatBrawl now runs on Amazon Fire TV Stick

We wrote up how both happened, with gameplay clips and code: dev.to: https://dev.to/rehaancubes/two-3d-games-in-two-hours-and-our-boat-game-on-fire-tv-life-after-shipaton-with-kotlin-5811 Medium: https://medium.com/@rehaancubes/two-3d-games-in-two-hours-and-our-boat-game-on-fire-tv-life-after-shipaton-with-kotlin-6c06609bad00

Joyframe is free and open source (Apache-2.0), and trying it takes one Gradle line: https://github.com/rehaancubess/joyframe-kmp Feedback, issues and stars all very welcome

Log in or sign up for Devpost to join the conversation.

Submission history