DEV Community

Cover image for We Built an Arcade for Our Own Game Engine
Olu Desire
Olu Desire

Posted on

We Built an Arcade for Our Own Game Engine

Or: how I spent three months using the platform we built at Evolved Tech, noticing what was broken, and refusing to stop


Kehinde Owolabi founded Evolved Tech and built the core: epic.js, our 2D game engine for the browser. Not Unity. Not Godot. Something small and honest — one file, no npm, no build step. Drop it into an HTML page, write some JavaScript, and you have a game running.

We called the project Limn Engine. Kehinde owns the engine core. It worked.

My name is Desire. I'm the co-founder of Evolved Tech. My job was everything around the engine — I built the editor where people actually make games with it, and the arcade where those games live once they're made.

Here's the situation we found ourselves in: the engine existed. Games could be made. But there was nowhere to put them. No gallery. No way to share. No way for anyone to see what you'd built.

Kehinde built the engine. I built the editor and the roads.

So I started building the arcade. Then I kept building — not because anyone asked, but because every time I used our own platform, something felt missing.

This is the story of how our engine became a full arcade with profiles, leaderboards, comments, replies, follows, weekly winners, an inbox that took two days to stop loading forever, and one screenshot that will haunt Kehinde for the rest of his football-watching life.

If you've ever added one feature and ended up with fifteen, pull up a chair.


Image

Part 1 — The Arcade

Picture a game gallery. Grid of cards. Each card shows one game someone made.

That's the arcade. The problem: how do you show a game without a screenshot?

Games aren't images, they're JavaScript. There's no browser on the server to render one, and recording video on every publish is way too heavy for a platform where games ship in 30 seconds.

So I did the obvious thing: I draw the thumbnail from the game's own data.

Every game saves a small config — the positions and colours of its objects. A dress-up game has five coloured rectangles (hat, head, shirt, pants, shoes). A platformer has a player square and a goal. A shooter has a ship and an enemy.

At load time, the arcade reads that config and draws the shapes into an SVG. No screenshot needed. No server rendering. Just math.

I expected it to look terrible. It actually looks... fine. A bit abstract. Like a Mondrian painting, if Mondrian was into 2D side-scrollers.

I'll take it.


Image

Part 2 — The Leaderboard (Which Nearly Broke Me)

Here's how my brain works. Watch closely.

"I should rank the games."

Okay, by what? Plays? Too easy to game — refresh five times, you're #1. Score? Every user writes their own game code, so there's no shared scoring format. Likes? Fine. Simple. One like per person per game.

"But likes should reset weekly, or the same game stays #1 forever."

Okay. Monday, 00:00 UTC. Every week. Automatic.

"But how do people know the leaderboard changed?"

Now there's a Weekly Leaderboard card in the inbox — top 5 games, like counts, comment counts. Like a game on Tuesday, the creator's inbox updates Wednesday.

"But that's kind of empty if nobody likes anything."

Then the card just doesn't render — which, in the platform's early days, meant it almost never did. Because I was the only user, and I wasn't going to like my own games.

...Reader, I liked my own games. I'm not proud of it. The leaderboard showed.


Image

Part 3 — Comments with Replies (The prompt() Mistake)

Comments were easy. I built them in an afternoon.

Replies were not.

My first version used a prompt() dialog — the built-in browser popup. Ugly, unstylable, covers the whole screen on mobile, looks like it's from 1998. I shipped it anyway because it worked.

Then I tried it on my tablet.

Tapped "Reply." Grey box appeared. Typed. Hit OK. Box vanished.

Nothing happened.

Turns out the page had already reloaded from an earlier action, and the reply was posting against a stale game ID. The fix took twenty minutes. The rewrite of the UI took four hours.

The new version: an inline reply form under every comment — small text box, indented, blue left border. Reply appears underneath, slightly smaller, slightly dimmer, clearly nested. Two levels deep, max — root comments and replies. No more, because three levels of nested comments on a phone screen is a war crime.

Now it looks like a real comment section. Like Reddit, if Reddit had five users and all of them were me.

Screenshot above: that's Kehinde in one of our test comment threads, confidently typing "Arsenal will win the league this season" like a man who has never once watched Arsenal play in May. Sir, we are testing a **reply feature, not writing your eulogy. I kept the screenshot in the codebase for QA purposes. I kept it in this article for accountability purposes. History will remember this comment. History will also remember what happened next.


Part 4 — The Username Problem

Worth telling carefully, because it's the kind of bug that teaches you something.

Until recently, every game shipped with two pieces of creator info: author_id (a Supabase UUID) and author_name — the creator's email prefix.

So if your email was desiregeorge434@gmail.com, every game you published said "by desiregeorge434." Not your name. Not a handle. Your raw email prefix, sitting there, looking at you.

So I added usernames. The rules:

  • 3–20 characters
  • Lowercase letters, numbers, underscores only
  • Unique across the platform
  • Locked forever once set

That last rule matters. If usernames could change, every /u/?u=desire link would break the moment Desire became @desirethegreat. Games, followers, old shares — all dead links.

So: one shot. Pick carefully. Live with it.

There's a second field — display name — which can change. You can be @desire forever while your display name bounces between "Desire" and "Desire the Magnificent" on a whim.

Two names. One permanent, one fluid. I only know this is the correct design because I got it wrong first.


Part 5 — The Two-Day Bug (Three Lines of Code)

My favourite story from this whole project.

For a week, every game showed its author's email prefix. Every profile page showed the actual username. The two systems never spoke to each other.

Games stored author_id / author_name. Profiles stored user_id / username. The arcade rendered games by reading author_name directly — it had no idea a profile system even existed.

I stared at this for two days. Rewrote the arcade query. Blamed Supabase. Blamed my browser cache. Blamed the phase of the moon.

Then it clicked: I needed a lookup.

const profile = profileMap[game.author_id];
if (profile && profile.username) return '@' + profile.username;
if (profile && profile.display_name) return profile.display_name;
if (game.author_name) return game.author_name.split('@')[0];
Enter fullscreen mode Exit fullscreen mode

Three lines. Two days. This is what programming is. I've made peace with it.


Image

Part 6 — The Inbox That Wouldn't Load

The inbox should load in two seconds. On my tablet, it loaded in never.

Here was the culprit:

try {
  const user = await supabase.auth.getUser();
  const docs = await supabase.from('user_documents').select('*');
  const updates = await supabase.from('engine_updates').select('*');
  // ...and four more fetches
} catch (err) {
  console.log('Error');
}
Enter fullscreen mode Exit fullscreen mode

Every fetch inside one try/catch. If any single one hangs, the whole chain waits forever — and my data was clocking in at roughly 2 bars and a prayer that week, so they hung constantly. (Nigerian networks are fine, by the way. It's not the network. It's a man who refuses to buy more data and instead debugs for six hours as a substitute.)

The fix: every fetch gets its own try/catch and a hard timeout.

const result = await withTimeout(
  supabase.from('...').select('*'),
  4000
);
Enter fullscreen mode Exit fullscreen mode

Four seconds, then move on. If a fetch fails, that one card just doesn't render — everything else still does.

The old inbox took 40 seconds to load, when it loaded at all. The new one takes 2. Twenty times faster, from forty lines of defensive code.

Lesson: fast isn't about speed. It's about knowing when to give up.


Part 7 — The Buttons Nobody Could See

My first on-screen button was rgba(255,255,255,0.18) — white at 18% opacity. Basically a ghost. Invisible on light backgrounds, barely-there on dark ones, a mistake on all of them.

Players would tap the screen, something would happen, and they'd have no idea why.

New buttons:

Button Color Purpose
Move Dark navy #1a1a2e + white text Solid, unambiguous
Jump Bright blue #2563eb Clearly an action
Fire Bright red #dc2626 Clearly an action

Plus labels on top: JUMP FIRE. No more guessing.

The runner template has zero buttons — tap anywhere to jump. "The screen is the button." Beautiful, and free.


Part 8 — Game Over → Play Again

Before: you lose. The screen freezes. Your score just sits there, mocking you. You refresh the page. You lose your score too.

After: you lose, and a blue ▶ PLAY AGAIN button appears center screen. Tap it — score zeroes, timer resets, you're back in, one tap away.

Fifteen minutes of work. Turned every template from "play once and close the tab" into "play forever." This is the feature that separates a tech demo from an actual game.


Part 9 — The Features That Were Missing

Every feature below came from actually using my own platform and hitting a wall. Not a roadmap. Not user requests — I had no users yet.

Feature Reason
The arcade Because nobody could see the games
Thumbnails Because blank cards look broken
Likes Because I wanted a signal people were playing
Leaderboard Because "top games" beats "all games"
Weekly reset Because the leaderboard got stale fast
Comments Because I wanted to know what people thought
Replies Because one-way comments felt empty
Usernames Because email prefixes should never be public
Profiles Because usernames needed a home
Follows Because "creator" is a real concept
Inbox Because notifications make a platform feel alive
Touch buttons Because tablets exist
Play Again Because the game isn't over until you say so

That's the real lesson: you don't design a platform, you use one until the gaps become obvious — then you build them, one weekend at a time, because you're the only one who cares they're missing.

That's not "features nobody asked for." That's the job.


Part 10 — What I Actually Learned

  1. You are your first user. Every confusing button and broken flow you notice before anyone else does is real. Fix it — that's not scope creep, that's the product.
  2. Users don't care about your architecture. They care that the button is visible, the game loads fast, and the leaderboard updates. Your state management is invisible to them, on purpose.
  3. Timeouts beat retries. Four seconds then move on is a design decision. Waiting forever is a bug wearing a trench coat.
  4. Small UX wins beat big features. A PLAY AGAIN button beats a new template. A readable button beats a pretty one.
  5. Usernames must be separate from display names. One person, two identities, two lifespans. Respect both.
  6. "Loading..." past 5 seconds isn't loading. It's broken. Fix it.
  7. You will spend two days on three lines of code, and it will feel like three years, and then you'll commit it, feel like a genius, open your email, and find 47 new messages. That's fine. That's the job too.

Part 11 — The Loop

Nothing is ever done. But right now, the loop works:

Make a game → Publish it → Share the link → Watch people play
→ Get likes → Rank on the leaderboard → Check the inbox → Make another
Enter fullscreen mode Exit fullscreen mode

That's the thing that has to work before anything else matters. And it does.


Part 12 — What's Still Coming

Short-term (this month)

Feature Why
Tags & categories Filter by puzzle, runner, shooter, dress-up — instead of scrolling everything
Featured banner Highlight one game a week on the arcade home page
Search by tag Pairs with existing search — find all "runner" games at once
Real thumbnails Render an actual screenshot on publish instead of abstract rectangles
Mobile multi-touch Move and shoot at once — an engine-level fix, not just a platform one

Medium-term (1–2 months)

Feature Why
Profile badges "Won the week of Sep 8" — give people bragging rights
Per-category leaderboards Top platformer, top runner, top puzzle — rewards variety
Notifications "Kofi replied to your comment" — instead of checking the inbox manually
Private sharing Publish without listing in the arcade, for WIPs and friends-only games
Follow feed A timeline of games from creators you follow

Long-term (not sure yet)

Feature Why I'm hesitating
Collaborative editing Two people, one game, live. Cool in theory, nightmare in practice
Moderation tools Right now I delete comments manually via the Supabase dashboard
Monetization Sounds nice, adds friction to a platform that currently has none
Native mobile app The web app works fine on tablets — this is a big lift for a small platform
Open-sourcing it I want to. I need to clean up the 4-second timeouts first

How I decide what's next: not by writing a roadmap and following it religiously — by opening my own arcade and getting annoyed at something that should exist and doesn't. Every item in Part 9 came from that process. Every item above will too.

The one thing I'm sure of: the loop works. Everything else is decoration.


What's New — At a Glance

Feature Description
🎮 Arcade gallery Browse every published game as a card with an auto-generated SVG thumbnail
🏆 Weekly leaderboard Top 5 games ranked by likes, resets every Monday at 00:00 UTC
💬 Comments + replies Two-level nested comments with inline reply forms (no more prompt())
🧑 Usernames + profiles Permanent unique handles, editable display names, public /u/?u= profile pages
👥 Follows Follow creators, see their games from one place
📥 Inbox Weekly leaderboard cards, notifications — loads in 2s flat, timeout-protected
🕹️ Touch controls Redesigned high-contrast buttons for mobile play, plus tap-anywhere runners
🔁 Play Again One-tap restart on every game template — no more refreshing the page

Try It

If you make a game, send me the link. I'll play it. I'll probably like it. I might even comment.


Get Involved / Get Help

Resource Link
💬 Discord discord.gg/kVpSmYWXr
🧑‍💻 GitHub github.com/terracodes004
📧 Email evolvedtech004@gmail.com

Bug reports, feature ideas, "why is my button rgba(255,255,255,0.18)" — all welcome.


Credits

Limn Engine — built by Kehinde Owolabi, founder of Evolved Tech. He built the core engine: one file, no dependencies, no build tools. Every game on this platform runs on his code.

The editor, arcade, leaderboard, profiles, comments, inbox, buttons, and everything in Part 12 — me, Desire, co-founder at Evolved Tech. Built one weekend at a time, on a tablet, while muttering about timeouts — because I kept using our own platform and kept finding things that bothered me.

If the games run, that's Kehinde. If the buttons work, that's me. If something's broken, it's probably both of us, and we'll fix it together — right after Kehinde finishes explaining, again, why this is finally Arsenal's year.


Draw your game into existence — with a canvas and a dream. 🎨🎮

#javascript #gamedev #webdev #showdev

Top comments (0)