Announcing: Senalumi
Senalumi is very simple real-time strategy game heavily inspired by Auralux. My goald was to create a game that you can pick up for 5-10mins and walk away from when you want. Low commitment but hopefully fun or nostalgic.
Here is a screenshot from Senalumi (or you can try it live - no download required):

For those unfamiliar with Auralux, the game Senalumi was based on, this is a video introducing Auralux:
I actually started this project years ago, during COVID-19, when everyone was working at home and my team was looking for a social activity. We played some multiplayer games and had fun and I recalled Auralux and thought it might be a fun game for us too. It’s a slow pace strategy game where we could pair up or go solo and compete. Unfortunately, I was busy and didn’t get it polished. Now that I’ve picked it up years later, the multiplayer dependencies are all out of date and have CVEs so I removed them for this release. I’d like to add multiplayer back sometime but I’m not sure when.
On the tech side, Senalumi is written in Typescript with Svelte to managed HTML and PixiJS to render the game. Most of the tech is pretty straightforward although I’ve experimented with a few ideas to make the implementation efficient.
Tech
Below are some software-engineering notes on this project. If you’re a non-technical person, this may be a little tough to follow. You’ve been warned ;)
Tech: Object Pooling
Usually when I’m building software I call new ThisThing() or new ThatThing() and pass that object around as needed (the (B) pattern in the snippet below). In most applications, the (B) pattern is fine. Because Senalumi manages and updates thousands of small objects, I was afraid this might put too much pressure on the browser garbage collector and cause pauses or dropped frames during gameplay. As a result, Senalumi makes heavy use of Object Pools. Many places in the code look like this:
// (A) In Senalumi, we use object pools
const satelitte = satellitePool.take().init(parameters);
// and we have to manually release the object when finished with it
satelittePool.release(satelitte);
// Not this: (B) "Standard" way to create objects:
const satellite = new Satellite(parameters)
// the GC will take care of cleaning up the memory
It’s not a huge change in the code but it does result in some memory efficiency. In the graphs below, the frequency of valleys indicate how often the GC is run and the slope of the hill represents how quickly memory usage is increasing until the peak when the GC runs again. The first graph is from non-pooled objects (new Thing()) and the second is from pooled objects. In the second graph, the peaks and valleys are more spread out and overall memory usage is lower.

Tech: Quadtree Performance
Most of the CPU time for this game is spent on (a) updating satellite position and velocity and (b) checking for collisions. I spent some time improving performance of the game and most of my time time was spent on (b).
As I learned with doom collisions, collision detection happens in two phases: broad and narrow.
In the broad phase, we are quickly finding objects that are candidates for collision or, to put it another way, we are eliminating objects we know cannot collide. It can be expensive to figure out if the objects actually collide so we want to eliminate as many candidates as possible in the broad phase.
DOOM uses a fixed size grid where each cell represents 128 units of world space. Any object in that space could collide with other objects. In Senalumi, most satellites will be grouped close to each other or close to a planet so I wanted something that would ignore big spaces without objects and that led me to quadtrees.
Quad trees, in theory, work by putting objects into a box. When the box gets too full, we take out all the objects, split the box into 4, and put the objects into the new boxes based on their location. Then repeat the process of inserting and splitting boxes as needed. That’s the theory. In practice, I’m using a more complicated implementation I based on a detailed StackOverflow discussion and I’ve combined that with object pooling.
With my quad tree working, I needed to decide when to split the boxes. To figure that out, I wrote a relatively simple performance test and tweaked the split parameter so I could achieve a high score on that test.
As a side note, after I optimized the quadtree I went back and tested a grid. The grid is actually faster when dealing with smaller numbers of satellites but only about 15% faster. However, as the number of satellites grew, the grid actually performed worse. Perhaps my implementation was sloppy but it’s a surprising result. I may take a look at this down the road.
For satellite updates (a), there is very little we can do. We need to change positions and velocity and that is going to mean hundreds and thousands of multiplications and additions per second. What we could do is to reduce the frequency or complexity of those updates. I’ve got some ideas and I suspect the existing performance test will help but nothing is done yet.
Tech: Dependency Upgrades
This project sat in a mostly-finished state for about 3 years and in the web world, that is plenty of time for dependencies to get out of date. It’s basically a meme. Fortunately, because I’m a big fan of having less dependencies, this project only really has 3 (the rest are trivial):
- Svelte: upgrading Svelte was amazing. In fact, I’m planning to spend some more time writing about that. I’ll admit I’m not always sold on Svelte 5, I fell in love with Svelte 3 syntax, but the migration from 3 to 5? Simply amazing.
- PixiJS: upgrading from 6 -> 8 was not so smooth. In fact, it’s still not finished. I’m not sure the viewport plugin I’m using here is fully compatible. I’m also having a hard time migrating my usage of
PIXI.Graphics()but that is kind of my own fault. I do plan to complete this upgrade because I think the particle system is a good fit for this use case but it’s been a little painful. - Colyseus: upgrading from 0.14 -> 0.16 has been confusing. The documentation isn’t very easy to follow and I think I accidentally depended on some internals of 0.14 to get multiplayer working. Oops! I’ll return to this upgrade at a later date but it has been more painful than PixiJS so far because PixiJS documentation is so much more polished.
Wrap
That’s about all I have to say. I may look to publish this game somewhere just for my own curiosity. For now, I’ve got to sort out those dependency updates…