TM★
← All 35 projects

Physics Survivor

Personal2025
TypeScriptbitECSVite

A hand-rolled JavaScript framework that makes survivor-game crowd physics fast without a GPU.

I wanted to build a survivor-like game that ran on nothing but JavaScript. Partly that's because I love JavaScript as a platform to iterate in. Hot module reloading means you make a change and see it live in the running game, which feels like using scriptable objects in Unity except much faster and without the build times. Partly it's distribution. A game on the web is a game anyone can play by clicking a link. And partly it was to prove a point, that you could do something like this on limited hardware, in a browser. I've been playing with 3D on the web since my early work on the geotechnical data viewer, and along the way I fell in love with react-three-fiber, which is just an elegant way to build 3D applications on the web that are performant and easy to compose. This was the natural next thing to try.

For those unfamiliar with the survivor-like genre, they're action games where hordes of enemies naively crash in toward the player while keeping space between each other, which creates a crowd-like feel. That crowd creates particular problems. All of those enemies are moving toward one point and need to repel each other, so you end up with a giant cluster all trying to repel each other at once, which is the worst-case scenario for almost any physics engine. In a lot of engines it creates jittering, because the solver has trouble converging, and a lot of overlapping colliders, including what's sometimes known as the collapsed core problem, where the center of the cluster gets denser and denser because a small number of entities are absorbing all the accumulated force from the large masses around them.

I made some early attempts to solve this using existing engines. React-three-fiber was the first one I tried. It has a Rapier integration, but it's built around Rapier 3D and didn't get me where I needed to go, so I tried making my own port of Rapier 2D, and that didn't get me the performance I needed either. I tried Box2D and Matter.js and a handful of lesser-known libraries, and none of them could do what I wanted, because of the brutal nature of this kind of physics. For anyone familiar with these games, the original Vampire Survivors was built in JavaScript on an engine called Phaser, which uses arcade-style physics with really simple representations of objects, and that simplicity is what makes it performant. The thing is, I didn't want to take this only as far as Phaser could go. I wanted to see what more could be done. So in 2025 I decided to take a crack at writing my own physics engine to solve the problem I'd been circling for years. It needed to solve two main problems, the core collapse and the jittering.

By then I'd been working with agentic generation long enough to know that on really open-ended problems it pays to set up a sandbox where you can try multiple things in parallel, assess which ones seem to be working best, work toward the best solutions, and expand again, in a process that's almost like human gradient descent. My goal was simple. I wanted 100,000 entities to chase the player around, resolve satisfyingly if not perfectly, do it at 60 frames per second, and do it without a GPU. I set up a testbed where every candidate physics system was a swappable module with its own auto-generated controls, a pause-and-step button, and telemetry so I could see what frame rates looked like over time, and I got started.

I started with a simple version where every enemy checks its distance to every other enemy and nudges away, which is fine with hundreds and dies with thousands. Then a traditional velocity-and-acceleration physics approach, which gets bouncy and jittery in a crowd. Then a family of ideas borrowed from fluid dynamics, where you stop treating enemies as individuals and treat the crowd like a fluid, painting a density map onto a grid and letting each enemy flow downhill away from the crowded cells. I tried variants of that with memory, so the flow field blended with the previous frame's to stop everything from twitching, and variants where enemies looked ahead in the direction they were moving and slowed down before they hit a dense patch. I had a notebook of even stranger ideas, like pressure caps from real fluid simulation and enemies reserving the space they were about to move into. And I tried peeling the crowd from the outside in, on the theory that if you make room at the edges first, the center can expand into it. None of it worked right. There was jittering, there was oscillation, and there was a whole lot of collapsed core. Everything was either too slow or didn't produce a simulation that actually felt correct.

Along the way I implemented pretty much every well-known optimization I could think of. An entity component system with data laid out for the CPU cache. A spatial hash so nothing checks against everything. A fixed physics timestep so changing the game speed can't cause tunneling. Two-phase resolution so walls always get the final word. I remember swapping the spatial hash's string keys for numbers, which alone cut frame time by 40 percent. But nothing could get me to 100,000 entities. I could get 10,000. I could get 20,000. I could get 30,000. And then finally one idea hit.

Just like the other approaches, I would move everything first and then resolve the overlaps. But I finally saw what was happening during the resolve. If you unpack the collisions in random order, about half of them resolve back toward the player, so as one entity gets pushed out, a later one has a tendency to push it right back in, and the whole cluster cascades inward. It's compounded by geometry. The farther you get from the player, the larger the perimeter of each shell in the cluster, which means more entities on the outside pushing in on fewer entities on the inside, and that cascades all the way through the entire mass. That was what was causing the collapsed core. The key was to resolve the collisions starting close to the player and work your way out. The ones in the middle grab the space they need first, that space keeps propagating outward, and by the time you're resolving the last ones they have plenty of room behind them. The result was far more stable. I call it onion peeling. That was the breakthrough that got me almost all the way there. I added one small tweak, a little extra resolution away from the player, by weighting each collision's resolution with a small vector pointing away from the player. Together they surpassed everything I'd managed in JavaScript. On a Mac M1 with no GPU involved, it handles 100,000 entities, and I could drop them in 10,000 at a time and watch the pile resolve into a clean crowd within three frames.

So I started building something on it. I took the physics engine and wrapped a game around it. A simple loop, a character controller with dashing, waves of enemies with different behaviors, chargers and healers and spawners and shooters, weapons with real motion, a Backpack Battles-style grid inventory, stamina and mana that feel like genuinely different resources, a lava arena with drifting rock islands. Shields were the hardest thing in the whole project. I wanted characters to carry physical barriers that push things around, acting as colliders but also as physics bodies in their own right, which means nested, parented colliders that all react with each other and constantly fight over which one gets to drive the other. I pulled in a lot of inspiration from Unity's TopDown Engine along the way, because I love how it's structured and how it thinks about things, colliders in particular, and I wanted those ergonomics in JavaScript. I even played around with procedural chiptune music, generated live with no audio files. It was terrible and never worked well, but it was interesting.

My favorite part was the motion authoring tools. I kept finding that my little daggers and swords would spin in a circle or poke out, and it just didn't feel satisfying. It wasn't juicy enough. I wanted it to feel more painterly. So the first idea was that rather than having things move in a clean circle or a straight line, I could draw the motion paths, and that looked pretty good. But I also wanted to manipulate the object's transform as it traveled the path, changing its scale and rotation along the way. So I'd have a Bezier curve for the translation, and then a separate set of curves defining scale and rotation as the weapon moved across it. This is where it got clever. It was hard to envision where a point on the scale or rotation curve lined up with a point on the motion curve, so I gave the Bezier line a rainbow color gradient, and put colored regions behind the scale and rotation curves that followed the same gradient. That made it easy to see which segment of each of the three lines corresponded to the others, and it made for buttery smooth transform authoring. The editors run in their own browser tab and broadcast every change to the running game, so a weapon rebuilds its animation on the very next swing. That work sparked a new love, in agentic coding, of building custom, bespoke tools for authoring and editing, something I now add to almost every project because they make things so much easier for me to think about, play with, and reason about. You can see the habit on this very site, in the lab.

As with so many great projects, it still hasn't quite become a game. Life got in the way. There's no full gameplay loop yet, just an engine that is totally satisfying to poke at and some simple demos built around things like status ailments that turned out to be very fun. Along the way I even pulled the reusable core out into its own engine package, with the physics as a swappable contract and onion peeling as the batteries-included default, because I didn't know exactly what the game wanted to be and I have ideas I'd like to try on top of it.

The key realization along the way was just how much time it takes to build the assets and do the authoring and editorial work a good game needs. I had solved some of the problems, but it left me with a hanging question. How would I get the art I wanted? What would the game look like, and what would it feel like? Then the 2026 farm season picked up and I had to step away, and that's where things stand to this day. During the season I've had some time to work on my agentic animation pipeline, which I'm hoping will solve some of the asset problem. As the 2026 farm season winds down, you might see my first game as early as 2027.

Say hi.

Got a problem that looks like this one? I want it.
Got one so new nobody's even scoped it? I want that one more.