4 Hugues Ross - Blog: Halberd: Let's Talk Architecture
Hugues Ross

5/29/18

Halberd: Let's Talk Architecture

I knew this day would come...

...a dark, fateful day...

...when I actually have to care about code architecture


Halberd's development is chugging along, and now that I've gotten a few systems together my codebase is starting to get a little bit cluttered. That means it's time to tidy it up.

Why the Wait?

Personally, I believe that a lot of independent devs overthink game architecture. There's a certain school of thought that dictates that your code should be a perfect, well-oiled machine that's designed to handle anything you throw at it. That's all well and good in theory, but I've always had my doubts that most indies starting a "from scratch" project actually need more than a quickly-assembled Rube Goldberg machine, held together with twine and masking tape.

There are two primary reasons behind this notion of mine:
  1. Your customer does not care about code quality - They really don't. You could have the most beautiful, well-structured code, and a terrible game that no-one wants. On the other hand, your code could be a tire fire and no-one would notice if you managed to keep the game bug-free. As such, polish and quality should be put where it matters the most.
  2. Good architecture is an investment - And of course, investments have an upfront cost. Designing and building solid game architecture takes time, and implementing new code to work with it can also take longer in some circumstances. In return, you make your code more readable and maintainable in the long-term. However, games are often short-term projects and smaller titles are unlikely to fully reap the benefits, while still paying the cost.
Ultimately, I think a lot of novice devs see various patterns being touted as "best practices", and don't consider whether they'll actually benefit from them in the end. Nowadays, my usual approach to architecture is "as little as possible, until it is necessary".

Why now?

Now that I've written 4 paragraphs about why I usually avoid over-engineering like the plague, let's talk about why I've chosen to begin restructuring my code.

Unlike an internal engine built to develop one or two games, Halberd is intended to be an all-in-one tool used by others. That alone isn't necessarily an argument for needing architecture, but it does highlight one important point: This project will require long-term development and support. Along with large teams, long-term projects are among the truly valid use-cases for sound architectural design in my opinion.

If this were the only factor, I wouldn't be too worried yet.The other issue is that my code is starting to get rather complex, another good sign that some straightening out is necessary. More specifically, I'm dealing with:
  • Language interop
  • Data that must be edited and tested non-destructively at runtime
  • Modal input/display (due to switching between PIE and editing)
  • An engine that links into two different front-ends, and must be able to immediately start in any state (menus, combat, map, etc)
Technically, I have all of that already. Problem is, the level of complexity is turning it into a mess. So, it's time to refactor.

What's the Plan?

When you have a clear need for architecture, the next step is to plan it all out. I've done that already, and I'm going to step you through my process in hopes that it'll be helpful.

First of all, we have to consider our requirements. To begin with, we know that the editor and game are going to be Vala and C, respectively. We also know that our exported game shouldn't include any Vala code, giving us the following high-level relationships:
That has already existed since the beginning of the project, so the next step is to look a bit deeper. Specifically, let's look at how the asset editors are laid out, since my impression is that this is the messiest part. Right now, your average editor looks a little like this:
First off, let's get rid of the doubled-up viewport. As a GTK widget, the viewport shouldn't be owned by anything on the C side, but is so that they can check for things like size changes. If we add an event to DFGame for handling window size changes (which is a good idea to begin with), we can at least remove one of the references.

The other obvious problem here is that the C side is clearly doing two things, editing and testing. As a result, there are multiple copies of each child. The simple solution is to split off any test-related data into its own struct, ensuring a proper separation of responsibilities:
So, what about this new "Game Context" that just popped up? Right now, I'm in a position where I haven't had to think too much about how to put the different game elements together. Since I'm rapidly approaching the point where I will need to do that, let's talk about structuring the game state. Right now things are split:
So, what can we do about this? Clearly, the data here needs to be known to the two "states", but we need a way to manage them and make some data (such as the player) available across all of them. We also need to be able to handle things one at a time (We shouldn't be able to run around the map during a fight, after all) without losing information about other parts of the game. To me, this demands a certain structure:
I think a stack makes a lot of sense for this sort of interaction, because there's a lot of "layering" that can happen in an RPG.

Imagine you're walking through the woods, when a bunch of bandits appear to tell the party that they have a bounty on their heads. The bandits attack, then halfway through the fight some allies join in to help even the odds.

This feels pretty typical by the standards of scripted battles, and it fits this structure perfectly: Map, with a Cutscene within it, which initiates an Encounter, during which another Cutscene plays. And of course, most of these states clearly return to the previous state when they conclude. That's perfect territory for a stack-based solution. Since a stack needs something to manage it anyway, we can also make the manager handle our 'global' information, such as the player's stats and inventory.

Back to the editor, this means that we can use our test context to take the edited data in memory, convert it into a game state, and pass it to the game context to push it onto the stack. If we make an event for when the stack empties, we can also return to the edit mode automatically when something like an encounter in test mode finishes. I can also reuse this feature later to know when the final game needs to exit.

Ultimately, we get something that looks like this at a higher level:

Pretty close! There's a little bit more to figure out, but I think I've covered the important parts here.

So that's all?

Nope, not by a long shot. Iteration is key in software development, so I'm sure to return to the drawing board sooner or later. In the meantime though, this design will help make my code easier to maintain. This little diversion has slowed me down for a bit, but I think it'll be worth it regardless. Since I'm still making pretty good time right now, I can also afford to spend time on these sorts of things. As the Roadmap page shows, I'm getting pretty close to my first milestone!
Post a Comment