The Plan So Far
Work Continues Apace-ish
This will be the first of a series of dev-log-like posts which aren't targeted at any particular project (yet) but will detail my experiments and learning exercises in trying to flesh things out.
Ponder is still very early and under wraps, so today, let's talk about Flagship and the as-of-yet-unannounced Project Sanctuary.
Project Flagship
I mentioned before that Flagship is a game, but not much more than that, so let's go over it.
I haven't completely nailed down the full concept yet, and my "design document" is more a series of thoughts and scribbles than it is a coherent document, but in short, I wanted to make a colony builder sort of game al a RimWorld. That is, heavy focus on the individuals and how they interact with the world around them. And by "world" I mean the Flagship you as the player build for them, which is where the Starbound-y elements come into play. Rather than just a simple clone of other games in its genre, I wanted to make the "colony" itself mobile. While this isn't a novel thing, I had a particular approach with a sort of hybrid between FTL and Starbound I wanted to try.
Of course, more on this later when I have more concrete things to share.
Project Sanctuary
What is Sanctuary? As of now, I actually have two ideas for a game and I'm on the fence about which one to do first. Both ideas, though, would need the same "simulation" work done either way, so that's happening first regardless and the actual decision is future me's problem.
That said, here's the jist: you build a Sanctuary (well isn't that surprising) for various alien races. The general inspiration came from Stellaris' "Rouge Servitor" civic or the general concept of a "Paperclip Maximizer": basically, an artificial intelligence was given the task to build a utopia, but the AI's idea of getting there is to effectively take absolute control of anything and everything to ensure it's as "utopia" as possible. Not violently, mind you, as I wanted to keep the whole thing light-hearted and chill (imagine Wheatley and/or GLaDOS trying to run a city), but the whole premise is these alien races don't do any work of any kind and your goal is simply to take care of them.
In order to create actual gameplay out of that, though, I had some inspiration from building systems from the likes of Planet Zoo/Coaster (and Roller Coaster Tycoon) plus some Spore. The main focus being taking care of your residents so more of them move in, which makes taking care of them harder, so more move in, etc. but with a focus on both creativity and some puzzle-ish elements.
Don't want to share too much for now, though. More on this project later if I decide I want to go in that direction.
SimLynx
It Simulates, It Links Lynx
So, in either case, I need a strong simulation engine. As I mentioned last week, I want these games to be data driven, so the core will be a separate module which handles the actual game simulation logic. But here's the kicker: I decided given how much open-source I end up consuming, I will be giving back by making this core simulation engine itself free-and-open-source. Anything game specific will of course remain proprietary, but anything agnostic to these games will be open for everyone to use and contribute to.
This module, I am calling SimLynx.
My hope with this is that other potential game developers can benefit from the core I am developing, leading to more better games, but also that outside contributors can help improve it and the games built upon it. It's a win-win, yeah?
I'll be setting up a GitHub repository for it once the foundations are in place, so, without further ado, let's start on it.
Diving In
I'm just going to pretend I didn't see the "no diving" sign.
I already talked a bit last week about my decision making process for language and engine of choice (or lack thereof), but here's the recap: I'll be working in a modern version of C# .NET (8 or 9 at the time of writing) and building SymLynx to be engine-agnostic. What I'm going to colloquially refer to as the "view engine" will be game-engine specific, and I'll probably be releasing a separate Godot-focused view engine module alongside SimLynx when it's ready.
In building with .NET, there are two main paradigms: web and games. While it does get used for other stuff (desktop apps?), those two are probably by far the most common. Thing is, though, a lot of the .NET game development concepts are pretty stuck in the past (mostly because the vast majority of it is Unity focused and Unity is), so a lot of what I'm going to be doing is bending these web concepts in weird ways. .NET in general has a lot going for it and can do some really strange things that other languages cannot (looking at you, LINQ), so I will probably find myself abusing it in many different ways until I find a good solution.
Design Concepts
"Powered by our innovative new patent-pending proprietary groundbreaking advanced technology."
For SimLynx, I wanted to create something that was radically different than the "gold standard" of how games, for the longest time, have been designed, because, in my opinion, this design style is hitting its limits when it comes to simulation games. Developers have to focus on micro-optimizations and squeezing every last little bit of speed out of their code for pretty minimal per-frame gain; there's got to be a better way to do this, right? Plus, SimLynx might end up getting used for non-game projects, too, (and I already have at least one idea) so the improved simulation performance is valuable anyway.
So, what does that entail?
Reactivity
For the vast majority of games, just about everything runs on a so-called "game loop". While some engines actually have multiple "loops" (i.e. one for sim, one for rendering, etc.), they all follow the same basic principle: every so often (usually every frame or other fractional-second "tick"), the engine kicks off an iteration of the game's loop and that loop, one by one, executes all of the game logic in some determined order. For simple games, this is often fine, and the "game loop" does actually have its advantages: it's easy to think about, it's deterministic, and it gives a sense of "time" since the loop usually runs at periodic and often known interval. However, once you start trying to do heavy simulation work, you find that all the entities of your simulation start having to as questions to one another in order to figure things out. Things like: Are we touching? Are you close by? Are you in the same room? What room am I even in? That gets messy, fast: imagine a room of even just two dozen people, but everyone is blindfolded and you're only allowed to navigate by asking questions to others around you. Yikes.
"So, time for a cheaper approach."
Honestly, a good example of a solid reactivity system is Roblox: almost everything in how objects interact with other objects is through a series of "events". There are events for objects being touched by other objects, clicked by players, moved, etc. but also a generic "changed" event which can describe any generic change to any of its properties. Roblox actually doesn't expose a traditional game loop: scripts running code have to rely on either events or its own internal sense of time. Granted, it has its own shortcomings, but most of them can be worked around with crafty applications of what the engine provides. It's even reasonably performant, too, despite being in Lua. Godot, as well, as what it calls "signals", but these are much lesser used since Godot does expect you to lean on the more traditional "game loop" behavior much more, but they're not too bad either (though they do have their shortcomings). Honestly, Godot and Roblox are pretty similar in function.
What can we learn from this? Well, it is clear we need some sort of similar "event" system that allows entities to communicate to an arbitrary and unknown number of subscribers when something happened, and, ideally, we would also want some sort of "changed" event that generically notifies when something happens. While events are easy (and native to C#), the latter is more tricky. Roblox and Godot actually cheat a little in this regard; the objects you interact with are actually thin wrappers around an underlying object but take care of the change tracking for you. In their case, that is actually the only option since they're really just C objects with little interfaces to interact with them being passed to scripts. In my case, though, I'm not doing that, so I have to get a little crafty to avoid sacrificing performance unnecessarily. That said, .NET is capable of some rather strange things like "Proxy" objects or even doing bizarre things like building entire types at runtime, so there's definitely some potential there.
There's even an entire library dedicated to handling of reactive streams of data: ReactiveX (i.e. System.Reactive
).
Context
In web development, there is a concept of a "request context". This is a special narrow scope of the application that allows parts of the app to be aware of the specific incoming request while still remaining agnostic to any particular request. That is, these parts can know that they are part of a request and do things with, but not care about some or all aspects of that request in order to get its job done. This is useful for things like database transactions, response manipulation, etc. but I see the potential in this context for something greater.
Regarding Flagship, let's imagine a hypothetical tool in the hand of a hypothetical crew member on a hypothetical ship. The tool needs to "know" about the hand that's holding it (and the crew member that it belongs to) so it can position itself in the world and know when it's being used. It does not, though, need to necessarily care about what kind of crew member is holding it or what else they may be up to or is afflicting them; all it cares about is that it is being held in a given spot. The crew member, too, needs to know what ship its on (so it knows where it can walk and what it can do), the ship needs to know what potential fleet it is part of, and that fleet what "map" the whole state is saved to.
Traditionally, this meant giving said tool a reference back to its holder, and the crew a reference to the ship, the ship its fleet, etc. Let's say, though, this tool is some sort of "fleet radio", so it needs to be able to get other ships in the fleet to communicate with them. Now we gotta do something like: this.GetHolder().GetShip().GetFleet()
. Not ideal, but not terrible. What it does mean, though, is that the developer has to make special care to keep those references updated correctly: if the crew sets the tool down, a special action has to be taken to clear the "holder" reference, and that's a potential for human error and bugs. But wait! If it's a radio, can't it receive messages even when not being held? So it does need to know what ship its on somehow, right? Well, the radio tool needs to keep a reference to the ship, but what happens if a crew member picks it up and goes to another ship with it? What if the ship changes fleets? Now we have to explicitly check each crew member for each of their tools that have references needing updating and make sure each reference gets its update. Ew.
What I would like is to be able to contextually provide things like the current fleet or ship to everything within it, allowing for everything to have this implicit access to it without keeping references that have to be updated explicitly. And, when these items move between contexts, the contexts just smoothly update to their new relevant values. No fuss. In honesty, the part of the tool that makes the whole "radio to fleet" bit work doesn't really care who, if anyone, is holding it, it just needs to know the fleet it's in and maybe the ship it's on.
Polymorphism
In object-oriented programming, we have a concept known as "Polymorphism," that is, the ability of any particular object to be treated as another object implicitly because it somehow derives from or matches the shape of that object. Usually this takes the form of "is a" relationships, i.e. a Dog
and a Cat
are both separate things, but both are an Animal
and I can refer to them as such implicitly because they are. This is actually very useful in game design as it allows for things to derive from some base "thing" (or a few levels of base thing) and different systems in the game can treat them as their less-specific versions since they may not need to know the full details of such. For example, a hypothetical building selection tool doesn't care what kind of building something is, only that it is one.
Except, this "is a" relationship is actually not ideal for extensible games. Very often, what we want to check for is not what something is, but what it can do, since that's more accurate to what we actually need to do. For example, take our selection tool again: what it really needs to know is not that something is a building, but that it is somehow selectable. In traditional OOP, this is done with an interface to indicate an object can be treated as something else as it matches the required shape. This, though, is not extensible: interfaces are static and rigid by design. Instead, we could create sub-objects of our object which represent it's capabilities: we call these "Components". In-fact, this is the root of what I often call the "component model" that Unity and others implement, as well as a Entity-Component-System model wherein the "objects" are just thin containers around a collection of components. Unfortunately, though, by doing it this way, the original value of the "is a" relationship is lost: objects cannot be buildings, they are simply amorphous things that, among other things, can act like one. This makes our desire to maintain type safety difficult.
An alternative approach is that of Roblox or Godot: objects (called "Instances" or "Nodes", respectively) are more narrow in nature. Instead of a building being selectable, it has a child object which is a "selection target" of some kind and that is picked up by the selection tool, which then references the building to which it belongs. This, I think, gives us the best of both worlds, since we both get to keep the "is a" relationship (the object is a building or is a selection target), but we also gain a has a or belongs to relationship (the building has a selection target which belongs to that building). This, too, makes context (as mentioned above) easier since there's an obvious tree structure to everything that context can be propagated through. As a bonus, these "child objects" can themselves by polymorphic and potentially have their own sub-objects, and so on, but this can get complicated quickly.
Unfortunately, while Roblox and Godot have great systems for their own use case, they fall too far short for what SimLynx aims to provide. Namely, serialization is difficult to do, type-safety is difficult to keep, and asynchronous code is all but impossible, so design work on a similar system is needed.
Parallelization
The biggest and most obvious boon for performance is to do things in parallel. By design of the "game loop", everything happens one-after-another in a sequence. While this is very easy to understand (and doing it any other way requires significant thought on the developer's part), what it means is the entire game's logic has to run to completion, top-to-bottom, every frame. This, quite honestly, sucks, since simulation games can be immensely complex. Modern processor design has shifted, too. Lately, more and more processors are simply putting more cores on the chip, since per-core speeds have mostly plateaued, but it's that very per-core speed we actually care about for games! More cores are completely useless to games if the entire logic loop has to run in sequence: this means only ever one core gets to do the work since only one thing is ever done at one time.
So, how do we approach this?
Well, the most obvious would be to allow the game's logic to branch out and run side-by-side instead of one-after-another, but doing so naively can quickly create problems. For one, some parts of the game logic may depend on others and letting them run in parallel means they become non-deterministic: the developer loses control over what order things execute in. Plus, the sense of "time" gets completely thrown off: while the game loop may run at a consistent speed, when in the loop code runs now becomes practically random.
We can solve a lot of these problems by leaning on the reactive events since, by nature, events can only been responded to after they have been fired. As for timing, we have a few options: we could take the simple approach of letting systems continue to run in series, but their per-entity work is parallelized, which alone would be a huge boost, but we could also allow entire systems to run in parallel if they could somehow be built into a graph that indicates when to trigger them, assuming they even need to respond to "time" at all. However, too much parallelization can very quickly start to hurt performance as the time and effort to sync everything back up. Plus, not everyone has 64 cores to play with: some, like myself, still have 8 and massive parallelization is moot.
Okay, what if "time" was an event instead? With the context, I talked about having the objects organized into a tree-like structure, so, perhaps we could utilize that tree to propagate some sort of "time update" event? If propagation was some sort of work queue, this would naturally limit parallelization to be similar to core count, since that's the most it can ever be anyway. I am unsure there, and I need to dig deeper into that thought at a later time as it sounds like it's going to be a significantly more complex topic.
Up Next
This has gone on long enough
Next week, I hope to plan out some more concrete examples of what I want to achieve specifically with Flagship, and draft out more for-real how those examples are going to be implemented using SimLynx. That, of course, means drafting out SimLynx, too. I hope that next week's will be less wall-of-text-y and more code-y and picture-y, too, which means the site itself might need some work.
Also, I know I said "weekly cadence" last week and I'm off by two days: I think Mondays makes more sense for now so that I have the weekend to compile and write up my thoughts. I'll probably stick to that instead so long as I am doing this in my spare time.
Until next time.