Porting Seiklus to the browser
A 2003 GameMaker exploration platformer, five engine versions later.
Play it →
Why this game
Seiklus is the reason I write software. I was messing around with GameMaker in 2003 when I found it — a wordless exploration game where you wander, collect things, and nobody explains anything to you. It was the game for me. I remember thinking that if I ever got good enough, I'd want to build something like it.
Twenty-odd years later I decided the next best thing was to make sure it still runs. It was written for Windows, and the number of people who will download an executable to try a freeware game from 2003 is approximately zero. A URL is a much lower bar.
Five engine versions
Getting it to the browser meant walking it forward through GameMaker's entire history: GameMaker 5, then 6.1, then 8.1, then GameMaker Studio 1.4, which is the first version that can export HTML5. Every step broke something. The project that exists today is 100 GML scripts, 439 objects, and 51 rooms.
A lot of the game's feel had to be recovered by playing rather than by reading. Timings, momentum, how long a transition holds — the kind of thing that isn't written down anywhere and that you only notice is wrong because it feels wrong. I spent a lot of evenings walking the same few rooms back and forth, adjusting numbers.

What HTML5 takes away
GameMaker's HTML5 target isn't a full implementation of the engine. Several functions the game leaned on simply don't exist, and each absence forced a rewrite:
game_save() and game_load() are gone. Saving is now hand-rolled: the game serialises its own state into an INI-shaped blob and puts it in localStorage. The original three save slots collapsed into one, because three slots in a browser tab is a solution to a problem nobody has.
game_end() is gone too, which is obvious in hindsight — a web page can't quit. The quit screen returns you to the title instead.
Gems, and a bug that only bites moving objects
Collectibles have to survive a reload, so each one is keyed by the room it's in plus where it spawned:
"6_476_660" // room 6, spawned at x=476, y=660Which works perfectly until you remember that some gems hover. If the key is computed anywhere other than the very first lines of the Create event, the object has already drifted, the key comes out different on every load, and collected gems quietly reappear. The fix is unglamorous — capture start_x and start_y before anything else runs — but it took a while to see, because the bug only affects the subset of gems that move.
Loading a save, before the player exists
Every room has a manager object that sets up music, spawns the player, and marks the room visited. On a normal room transition that ordering is fine. Loading a save is not a normal room transition: the manager's Room Start event can run before the player object exists, so anything that reaches for the player's position throws. Every room manager now carries a fallback that spawns the player itself if nothing else has.
Talking to JavaScript from inside GameMaker
The mobile work needed the page to know when the player opened the in-game subscreen, so the touch controls could get out of the way. The export is obfuscated, so there's no function to call into. What does survive is this:
if (os_browser != browser_not_a_browser) {
url_open("javascript:window.myVariable=true");
}It is not elegant. It is a game engine setting a global variable by pretending to navigate to a URL. But it crosses the boundary reliably, and it keeps working after the exporter has renamed everything else.
Two days on audio
The whole port took about nine days. Two of them went to sound on iOS, and the commit titles tell the story on their own: working on mobile with no lag, but also no audio, then improve audio for mobile, then finally audio finally working for mobile!
iOS refuses to play anything until the user has interacted, so the page resumes the audio context on first touch and queues any music that got rejected, retrying it on the next interaction. Most sound effects are blocked outright on mobile and only a small allowlist gets through, because the alternative is a wall of overlapping noise.
The bug that actually cost the time was smaller and much more annoying. Fading music out means knowing which track is playing — but GameMaker clears an audio element's src before it calls pause(), so by the time the fade code runs, the thing it needs to identify the track is already gone. The fix is to record each element's source in a WeakMap when play() succeeds, and look it up again on the way out.
The part that will bite me later
What ships is the GameMaker HTML5 export, but not untouched. After every export a script splices the generated game code into a template that carries all the browser work — the touch controls, the audio hooks, the letterbox scaling that keeps the 4:3 canvas sharp on a phone.
Which means the build has a trapdoor in it: re-export from GameMaker without running the patch step and every bit of that silently disappears. It's written down in the repo in capital letters. I expect to rediscover it anyway.
Still on the list
It isn't finished. There's a running list of things I know are wrong and haven't got to yet — take the elevator up from the snowy area and it draws in front of the player instead of behind, a map piece is missing from the subscreen, and a thin line shows through the middle of a few textures. It still doesn't behave on iPad.
The one that nags at me most is chests. I fixed the equivalent problem for gems — key them by spawn position, capture it before anything moves — but I'm not convinced chests are saving reliably in every case, and the fix probably means autosaving on every room change rather than trusting the existing logic. It's the same class of bug wearing a different hat.
I work on it in bursts, when I feel like it. That's sort of the point of a project like this.
Credit
Seiklus was made by clysm in 2003 and released as freeware. It has never been for sale, and neither is this — it's a passion project, built purely so the game stays playable. All of the art, music, and design are theirs. The only thing here that's mine is the porting work.
