State Vector March 24


Another month, another update. So far I manage to keep to my schedule, which is encouraging. This months update adds quite a bit of mechanical depth, with drive and money no longer being the only “resources” to manage. In addition to your motivation, your spacecraft, EMU and body all can deteriorate to various conditions. These are not difficult to recover from… at first. The worse things get, the more difficult and expensive they are to recover from.

A “push”-mechanic has also been added that lets you reroll your dice pool in specific circumstances. If you’re clever and lucky, you might get away with it without any cost, though most of the time it will cost you some drive. The whole mechanic is designed to allow getting out of situations where you cannot possibly succeed anymore and will likely encounter a game over before you can reroll your pool naturally, or to help with awfully rolled pools after resting (I just so happened to roll 5 ones the other day. It’s a 1 in 100’000 chance, which is not actually that unlikely. If the game is successful enough some day, it’s something that will very likely happen to quite a few players). But why am I talking about things that you can just try out for yourself? Let’s talk about other things instead!

Let’s get unnecessarily technical

For some time I wanted to talk a bit about the technical guts that drive the game. Hopefully somebody else will find it interesting, too. As mentioned before, I’m coming at this with a backender background. I do have some marginal experience with how code in games is usually structured. Most engines enforce an Entity-component system by default. LibGDX, happily, does not. LibGDX enforces precious few things in fact, and could be considered almost irresponsibly unopinionated. Which is bad and offers you plenty of opportunities to shoot yourself in the foot if you don’t know your way around code so well, but if code is literally your daily bred, you know that such frameworks can become great to work with once you impose your own opinion on them. Which I did, copiously.

The thing is, ECS is a really, really good structure for what most games are - collections of real-time, input-driven state machines. And since most states they hold are having immediate effects on what’s being shown on the screen, the separation between backend and frontend can get a bit fuzzy. Turnbased games, however, are not that. Or at least, don’t need to be.

So the first opinion I hoisted on libGDX was strong backend-frontend separation. I.e. I’m using libGDX as a data-driven frontend for a backend that is completely independent from it. My frontend and backend are two different projects. They’re not strongly decoupled, though. While the backend can be built all on its own, the frontend pulls in the backend API and uses it directly. It seemed unnecessarily tedious and inefficient to let the two talk by some protocol. I’m not planning on any multiplayer, after all. One of the (many) reasons I’m doing this is because I don’t want to constantly hassle around with networks as I do in my day job, so that would completely defeat the point.

In any case, the way this works, on the high-level, is fairly simple. There’s the backend, which consists of data repositories (think database, but much, much simpler) and a service layer providing methods to access them and do some other stuff. But the frontend doesn’t need to claw its required data together from those repositories. Rather, the backend builds a context object - a data structure containing all the data that is necessary for the current “scene” in the game - and most of the time, that’s all the frontend needs. The frontend then generates the UI for the given context dynamically. Later it will do the same for sound and graphics, so the backend won’t have to bother with any of those. LibGDX does provide structures for managing all of that, after all, no need to re-invent the wheel.

That context is immutable. That means, it cannot be changed. The frontend just receives it, and does what it needs to do to relay the necessary information and input opportunities to the player. Once the player does something that actually affects the state in an important way, the frontend invokes the service layer in the backend. All the important “business logic” to actually evaluate the results of the action are there. The service then updates relevant data in the repository, builds a new context, and hands it to the frontend. The whole thing looks somewhat like this:

The system also has the advantage that to save the game, about all I need to do is to dump the repository into a file, and Bob’s your Uncle. Ok, there’s an entire data transfer layer with the usual gazillion serialisers and deserialisers in there, so not quite that simple, but in essence, that’s it. Very simple to decouple and shove into its own thread once I decide it’s taking too long. The entire repository can very easily make an in-memory copy of itself, so writing to the repository while another thread is doing the more timeconsuming writing to disk stuff of the copied data is not a problem.

Taking a deeper look

This is of course only a very high-level overview. The real magic happens when the context is built. Because again, all the repositories contain is data, but of course there needs to be something that evaluates that data and decides what should happen next. I can’t really do that practically when processing a player input. The player input and its evaluation changes the data in the repository in appropriate ways by effects of the action being executed, true. But those effects might have other effects, or various elements of the new state might interact together in new ways.

The usual approach in game design to make all this stuff happen is an event messaging system. Which, again, is a great architecture for real-time or asynchronous applications. Orbital Margins is neither, so I can go with something that is quite a bit easier to test and debug: A pipeline!

Now, what the hell is that? In all honesty, the term “pipeline” is somewhat misleading. There’s an old joke that “there’s only two hard things in software engineering: Cache invalidation, and naming things!”. It’s hilarious because it sounds silly, but you encounter the truth of it almost every day if you work in software development, and this is but one example of many. Because a “pipeline” serves transportation, so you’d expect a software pipeline to be predominantly about getting something from A to B. This pipeline’s most important job, however, is putting something together, so “assembly line” would really be a more proper term. But, somebody at some point designed a system for which pipeline was a good term, and then somebody realised that you can use the same basic pattern to perform serial, decoupled operations on data, and merrily continued to call it a pipeline, because it’s still the same basic structure, so that’s the terminology we’ve ended up with. But I digress, a lot.

So, there is in fact an assembly line between the backend and the frontend that puts the whole context together. This whole thing has many aspects of what we call “middleware”: It doesn’t really belong to either the frontend or the backend, being kind of its own thing that could be easily wrapped out into yet another project. Or into scripts. If modding support should ever become a requested feature for the game, this entire layer could just simply be opened up, as this is where the actual magic happens that makes this whole thing work as a game.

The line has two fundamentally separated stages: One in which the data in the repositories is still actively being changed, and one in which the repositories are frozen, and the context is manipulated directly to add things that simply don’t need storing for long.

The really neat thing of this entire architecture is that I can now implement crucial game logic independent from all the rest, and encapsulate it tightly in decoupled modules. To give you a better understanding of what’s going on here, one type of crucial game logic that is located here is producing all the challenges. Let’s say you’re in space, and now you docked to a station. The generator module for the challenge that lets you rest at anchor in the vicinity of a station looks at the new context and goes “oh, the ship is docked to a station, guess my challenge is no longer needed” and removes it. Meanwhile, the module for resting at a station, will look at the context a bit later and be like “oh, the player is docked to a station, which means that my challenge should exist in the context, but it doesn’t, so let’s fix that”.

Needless to say, the conditions for whether a challenge in a certain context should exist or not, or should exist in its current form, are usually a bit more complex than that. And that’s the nice thing about this data driven assembly line: Every piece is independent of the other, can be tested independently, and if something does not work as intended, it is often very clear where you have to go to look. Compare that to an event-driven system, where debugging can often get very confusing very fast, because there’s no rigid structure to what things are getting done where in what order.

On the other hand, it is somewhat less efficient than an event-driven system that changes mutable objects. Quite obviously, this system would do very poorly where almost any input at any time can change the relevant context of the game (even something so common as moving your mouse), or worse, where the mere passage of time will do that without the player even having to do anything. So, better don’t try to design an action game based on this architecture. But for turn-based games, I find it just so much more comprehensible and maintainable than an ECS where state-affecting behaviour is built directly into objects that update their state unguarded when prompted by events whose order you don’t control. The work efficiency I gain by the greatly reduced overhead when hunting for bugs or implementing new features is well worth the couple of CPU cycles I waste on it whenever the player does something important for the state, which is not that often (every couple of seconds or potentially even minutes, as opposed to 60 times per second for a real-time, framelocked game).

Obviously you could implement this pattern in an ECS like for example Unity, where you keep your ECS components strictly frontend-related. But a more open framework like libGDX just makes it so much easier and inviting, which is why I prefer it. LibGDX also provided a very good basis to implement a UI architecture that allows for a lot better separation of concerns than I’m used to from most game engines that are (unsurprisingly) not built in a way that emphasises UI over everything else. But that is another story, that I might get into some other time. For now, if you found this in any way interesting and would like to return a favour, the best thing you can do is get the game, play it for half an hour, and give me some feedback. It’s even slowly starting to get fun. I think.

Get Orbital Margins

Leave a comment

Log in with itch.io to leave a comment.