Announcing: Senalumi


Senalumi is very simple real-time strategy game heavily inspired by Auralux. The idea is to have a very simple game that you can play 5-10mins at a time and walk away from. Low commitment but also hopefully fun or nostalgic.

Here is a screenshot from Senalumi (or you can try it live - no download required):

Senalumi mid-game with 7 opponents

For those unfamiliar with Auralux, this is what the game looks like:

Play

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 during the game. As a result, 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. 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.

Memory graph from creating new objects instead of pooling Memory graph from pooled objects

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. As I was checking performance of the game, I spent most of my time 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 satelittes 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 complicate implementation I based on a detailed StackOverflow discussion and I’ve combined that with object pooling.

Now that I had a 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 only very little we can do. We need to change positions and velocity and that is going to mean hundreds of thousand multiplications and additions many times 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 Upgradas

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):

  1. 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.
  2. 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 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.
  3. 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…

More articles tagged 'Announcements':