Lessons learned building a game engine from scratch
This is an edited transcript of my LambdaConf 2025 talk. It has been tightened for reading while preserving the ideas and technical examples from the stage.
Why build a game engine?
I built a game engine—or, more accurately, I am building one. My career has been shaped by projects like this: projects slightly beyond what I already know how to do.
My day job is design engineering, which means wearing enough design and engineering hats to bring software ideas to life. I wanted a project at the intersection of those skills that would push me beyond my current limits. A game engine certainly qualifies. It combines pathfinding, projection, animation, cutscenes, sprites, and storytelling into something a player can enjoy.
Why Gleam?
I did not choose the language I use every day. I chose Gleam, partly because I love functional programming and partly because it fits a browser-based game engine unusually well.
Gleam compiles to JavaScript, so the client can reach browser APIs like Canvas and respond to keyboard and pointer events. It also compiles to Erlang and runs on the BEAM, giving the server a runtime built for high concurrency. I can use one strongly typed, immutable language on both sides while choosing the runtime that fits each job.
The developer experience helps, too. Gleam’s compiler errors explain what is wrong, suggest likely fixes, and show the valid options. Add pipelines and pattern matching, and it becomes a pleasant environment for working through a complicated model.
Lesson one: expect hidden complexity
At the start of a project, motivation is abundant. Then a supposedly simple problem reveals the machinery hiding under its surface. That is why the project is ambitious. You do not get the reward of ambitious work without being willing to face that complexity.
Consider the game loop, the first thing every engine must solve. At a high level it sounds trivial: hold some state, render it, update it, and repeat. A sprite animation increments its current frame, renders the next part of a sprite sheet, and loops back to the beginning. Simple.
In the browser, the right foundation isrequestAnimationFrame. It synchronizes work with the browser’s rendering, avoids long-term timer drift, and pauses when the tab is no longer active. But it also runs at the display’s refresh rate. A 120Hz monitor produces twice as many callbacks as the traditional 60Hz model used by many physics and animation systems.
Fixed updates, variable renders
Rendering should be as fast as the screen allows, but game-state updates need a fixed cadence. The loop compares the current timestamp with the last update. If at least one 60fps interval has elapsed, it updates the state; otherwise it renders without updating.
That still misses frames when more than one interval has elapsed. If the delta is 38 milliseconds, there is room for two updates. The update function therefore recurses, subtracting one fixed interval each time, until there is not enough time left for another update.
The leftover fraction matters. A few milliseconds seem harmless, but repeatedly discarding them can lose many frames every second. An accumulator carries that remainder into the next pass. Eventually those fractions add up to a complete update instead of disappearing.
There is one more failure mode. If a slow frame produces a huge delta, catching up can take so long that the next frame falls even farther behind. That feedback loop ends in a frozen game—the spiral of death. A maximum update count gives the loop a kill condition and lets it recover over subsequent frames.
That is the “simple” game loop: fixed updates, variable rendering, a recursive catch-up step, an accumulator for fractional time, and a cap to prevent runaway work. Ambitious projects are full of problems like this.
Lesson two: you do not have to solve it alone
The contrasting lesson is that complexity does not have to be tackled in isolation. Communities help because people are excited by the project—or because your excitement is contagious. The relationships created while asking for help can become one of the best outcomes of the work.
AI is also useful here, regardless of the larger debate around it. The hardest part of entering a new domain is often not knowing its language. Search works well when you know the terms. It works poorly when you do not know what to call the problem. AI can bridge that cold start: ask in the language you have, learn the vocabulary of the domain, and then bring your normal research skills back into play.
It also speeds up comparison and iteration. When designing a magic system, I can quickly explore fixed versus customizable class progression, attribute combinations, enemies, environments, and names. The output is not automatically correct. Refinement remains my job. But the distance from a vague idea to something I can evaluate becomes much shorter.
The point is the future you want
I went to school for industrial design. Teaching myself to code was my first ambitious project, and help from other people led to my first development job. I got the future I wanted because I was willing to move outside my comfort zone.
The point of sharing this is not to make the engine sound impressive. It is to make the work feel possible. If I can learn enough to build this, you can learn enough to build the project that matters to you.
My favorite secret about ambitious projects is that you do not have to succeed to be successful. I once made a single season of an Elm podcast. It did not make me a famous podcaster, but it did lead directly to a job and to friendships I still have.
Should you take on an ambitious project? Yes. If you are not already on the path to the future you want, you owe it to yourself to cross the gap of hard work and unknowns that might get you there.
I hope you enjoyed
There is a lot more coming...
If you want to get updates when I publish new guides, demos, and more just put your email in below.