Inspiration

On summer nights in my hometown, moths circle the street lamps endlessly, drawn to the light but never quite reaching it. I wanted a game that captured that feeling in a single gesture: something you can play for thirty seconds while waiting for the bus, but that keeps pulling you back like the lamp pulls the moth.

What it does

Polilla ("moth" in Spanish) is a one-tap arcade game. Your moth circles a glowing street lamp on two orbits, and tapping anywhere switches between the inner and outer one. You collect sparks and dodge the bats that cross your path.

  • The moth speeds up with every spark.
  • Every 10 sparks it reverses direction, just when you were getting comfortable.
  • Caught by a bat? You can watch an optional video once per flight to keep flying with your score intact.
  • Four achievements and a global leaderboard through Google Play Games.

How I built it

Polilla started as a quick HTML prototype to check that the core loop was fun. Once it was, I rebuilt it from scratch with Kotlin Multiplatform and Compose Multiplatform, keeping a strict separation:

  • A pure Kotlin game engine in commonMain, with no UI dependencies. Everything is measured in "lamp units", so the engine doesn't know or care about screen sizes. It's covered by unit tests.
  • Rendering with Compose DrawScope, driven by a withFrameNanos game loop. No game engine, no assets: the moth, the bats and the lamp glow are all drawn in code.
  • Shared business logic, platform-specific services. Achievements, ad frequency rules and analytics live in common code as small, testable classes. Each platform only implements thin interfaces: Play Games Services, AdMob and Firebase on Android, with Game Center, Google Mobile Ads and Firebase ready on the iOS side.
  • RevenueCat tracks ad revenue by wrapping AdMob's loading calls with loadAndTrack, with GDPR consent handled through Google's User Messaging Platform.

Challenges I ran into

  • Ads that don't ruin a one-tap game. In a game where you tap constantly, banners mean accidental clicks, so there are none. Interstitials follow strict rules written as tested common code: at most one every three deaths, never in the first minutes of play, never right after a rewarded video.
  • Measuring whether ads drive players away. I wanted analytics to answer one specific question: do players quit right after an interstitial? A full-screen ad covers the Compose view and looks exactly like the app going to the background, so the tracker had to learn to tell the two apart.
  • Shipping, not just building. Consent flows, a privacy policy, Play Games credentials, store declarations and signing keys took as much effort as the game itself.

What I learned

How far you can go with Compose as a game renderer, and how much a clean split between shared logic and platform services pays off: adding ads, achievements and analytics never touched the game engine.

What's next

Releasing on the App Store with Game Center, tuning difficulty with real analytics data, and new night-time hazards for the moth.

Built With

  • admob
  • android
  • compose-multiplatform
  • firebase-analytics
  • github
  • google-play-games-services
  • google-ump
  • gradle
  • kotlin
  • kotlin-multiplatform
  • revenuecat
Share this project:

Updates

Submission history