Inspiration

I've always liked puzzle games where the rule is simple but the levels still make you think for a bit. So I started wondering what happens if you can't move the character at all and can only turn the world around her.

That became Muzu. She can't walk or jump, she just falls. You rotate the maze and she drops until she hits something. Getting her into the ring is the whole game.

What it does

Every level is a small board. You turn it a quarter turn left or right and Muzu falls. Each board has a known best solution, so you can always try to do it in fewer moves. You get stars, a bonus if you match the best, and a few extra points for being quick.

New stuff gets added as you go. Spikes first, then ramps, portals, keys, sticky slime, one-way gates, magnets, and flux, which flips gravity so she falls up. Each one gets a small lesson level before it shows up in the real puzzles.

There's a Shapes mode too, where the boards are cut into letters, hearts and lightning bolts. And a Weekly Season with three new boards every day and a leaderboard for the week.

You can drag the board, tap the turn buttons, use a slider or just tilt the phone. There are 11 skies, 8 sound worlds and a wardrobe for Muzu (caps, glasses, a beard and a wizard hat, which I'm a bit too proud of). It's in 18 languages and the main game works offline.

How I built it

It's one Kotlin Multiplatform project and all the UI is Compose Multiplatform. Roughly 90% of the Kotlin code sits in commonMain, around 54k lines. The iOS app is basically 122 lines of Swift that just show the shared UI.

Some things I liked about doing it this way:

  • The game logic (physics, the solver, scoring) is plain Kotlin. I compile the same code to JS for my admin panel, which makes the Weekly Season boards right in the browser.
  • There are no image files for the game art. The boards, Muzu, the skies, even the app icon are drawn in code.
  • Sounds are generated at runtime from a small JSON file, so Android and iPhone play exactly the same thing.
  • Firebase Auth and Firestore go through small REST clients I wrote with Ktor in common code, so I only had to write that once.
  • RevenueCat's KMP SDK handles Premium on both stores from shared code.

The platform-specific bits are small: Google and Apple sign-in, ads and consent, haptics, sharing, notifications and the review prompt.

Challenges I ran into

Performance took me a while. My first version redrew way too much on every frame while the board was turning. Splitting the board into a static layer and a moving layer cut the drawing cost by about 72%.

Difficulty was the other hard part. I wrote a solver that finds the best solution for each board and used it to order the levels. Funny thing I learned is that the irregular, messy layouts are what make a board hard.

Tilt was annoying too. A phone lying flat on a table barely reports gravity, so I had to mix in the gyroscope before it felt right.

Accomplishments that I'm proud of

Getting it out on both the App Store and Google Play during Shipaton, on my own, from one codebase. The same level plays the same on Android, iPhone, desktop and web, and the video shows all four running side by side.

What I learned

Compose Multiplatform turned out to be fine for a game. Drawing, animation and touch handling all live in shared code and behave the same on both phones. Keeping the platform code small is really what let one person look after two apps.

What's next

More Shapes boards, a new Weekly Season every week, and more stuff for Muzu to wear.

Built With

  • admob
  • android
  • coil
  • compose-multiplatform
  • firebase
  • firebase-authentication
  • firestore
  • gradle
  • ios
  • koin
  • kotlin
  • kotlin-coroutines
  • kotlin-js
  • kotlin-multiplatform
  • kotlinx-serialization
  • ktor
  • revenuecat
  • swift
  • swiftui
Share this project:

Updates

Submission history