Improving performance of the atom simulation has been like pulling teeth: slow, painful, and not well suited to being featured in short videos paired with a couple hundred explanatory words.
So let's talk about something a tiny bit more fun: geometry! And a light serving of mathematics!
I know I've got you hooked now, so let's set the stage.
The Integer Grid
The atom simulation divides the entire game world into a discrete "integer grid" - and every atom occupies one place in that grid.
And so while most games check for collisions between lines and various shapes1, in this game an awful lot of logic boils down to "atom raycasting", aka "draw an imaginary line and see which atoms it hits":
- Can this enemy see that player? Sure, if only transparent atoms are in the way.
- Where should this mine spell lodge in the ground? At the first solid atom it hits.
- Can this atom move to there? Only if there are only empty atoms in the way.
And some logic ends up asking about "points in an area" - for example, do the atoms in a rectangle in front of an enemy make up a slope or a wall?
This is all fine! Checking an individual grid position for an atom is quick, and checking a line (or shape) of points is also fast.
But... sometimes we have to mix the two.
Take explosions: raycast from the center of a circle to each point in that circle to apply force to atoms, but also reduce the force for each "fixed in place" atom (e.g. stone) hit:
Spells that need to spawn or damage atoms around their impact point need similar logic.
So can we make it faster?
Raycasting in a Circle
Raycasting to all atoms in a circle seems so easy:
- Ask your barely-competent-but-ever-present mathematical consultant2 how to find integer points in a circle. 3
- Raycast to each of those points, just like normal.
Unfortunately that ends up checking a lot of points many times:
Checking the same points over and over is slow! Ideally we'd visit each point in the circle exactly once.
Okay, no problem! We're all smart people here.
Obviously the raycasts to the points on the circumference of the circle are going to hit the points in the circle (..right?) so we can dust off our high school trigonometry and draw lines at various angles to hit all atoms on the circumference (..right?):
One step forward, and two steps back: we're doubling up on points less, but now there also are some points we're not visiting at all.
We could reduce the difference in angles between lines so we do more raycasts, but then we'll visit more points again, and the correct angle-difference would depend on the radius of the circle.
Hmm.
What if we calculate points on the circle's circumference4 and raycast to those directly?
We're missing fewer points, but we still visit points close to the circle's center multiple times.
This approach just wasn't going to work. 5
Circular Raycast Tree
I eventually realized that if I wanted to visit each point inside the circle exactly once, I could work backwards from the list of points to "build the ray casts":
- Generate all the points in a circle centered at the origin.
- Sort the points by distance from the origin.
- In order of increasing distance from the origin, connect each point to the parent point that's closest to the origin.
By traversing the resulting tree from the center point outwards, we can finally satisfy our two criteria:
- we can be guaranteed to hit all points exactly once, and
- we can also stop going in a given direction if we hit something.
It is faster6 than the "cast many separate rays" approaches I explored above - at least for largish circles7 - though slightly less accurate:
A small inaccuracy like that shouldn't be noticeable during the game. (..right?)
Playable web build
Here's a hacked up build with the tree raycast logic hooked up to the cursor position (use mouse wheel to change its size), so you can play with it:
I still need to actually integrate this new approach into explosions, mind, so you won't notice any difference there.
PS: Yes, there's a bug with atoms getting stuck floating in the air sometimes. It's a performance optimization gone awry! Just pretend like you didn't see it.
E.g. Does the line between the enemy's eyes and the player have any shapes between it? Is the sphere that represents the player's feet intersecting with any shapes that make up the level? Etc
OpenAI's GPT4o - or Anthropic's Claude when I'm in a big spender mood.
The approach: check every integer point in a square whose sides are sized to twice the circle radius r
, and if the point is less than r
distance away from the center, it's in the circle! I could have worked that one out myself.
Using for example Bresenham's circle algorithm.
Though perhaps if you're a real mathematician, you can devise a clever analytical way to make this work? Let me know.
As long as you build the raycast tree structure once and reuse it over and over.
For small circles, the overhead of traversing the tree data structure is more than just visiting the same points multiple times.
It is probably possible to further optimize the tree data structure as a cache-friendly array of points to visit, but that is complicated by having to support bailing out of a given ray at any point so I haven't done that yet.