May is the first month that I didn’t release an update to Masteroid since I first released it to the public a year ago. I thought I’d write a little bit about how the month unfolded and why I didn’t release an update. This might be relatable or insightful to other developers!

First of all, this has been a pretty horrible month for me. I’ve had some health issues in both my immediate and extended family. Work also blew up and I gave over 200 hours of my awake time to my full time career. I also really let some people down that I care about and that took an emotional toll on me.

An unrelated anecdote: a person I know is the director of a fairly large team and has a lot of responsibility. I have heard that she also is a barista at Starbucks. It’s surprising she’d take on another job given that she has a high-pressure career. Apparently she finds the barista work relaxing and enjoyable.

I realized that Masteroid is like this for me. Often the decisions in both my home and work life have no clear answer and real consequences that affect a lot of people. Working on Masteroid is black and white. The progress is measurable. The answers are usually clear. And I’m pretty good at it. I’ve built something that’s fun to play and I’m proud of it. The biggest success of Masteroid is that every month it has continued to be fun and relaxing to work on.

But that being said, May got overwhelming. I didn’t want to do anything outside of work and Masteroid itself became stressful. Here’s why…

Problem 1

In some of the very first days that I worked on Masteroid, I had a clear goal for some core game tech. I wanted game entities to be very data-driven. Instead of having a game entity for TertiusScoutShip and TertiusFrigateShip and AetherkindDestroyerShip… I have a single Ship entity whose visual appearance, collision, behavior and everything else are driven by a set of data.

I hand-authored the data for the first ships, weapons, factions and sectors in JSON in the first few days of the project. The idea was I’d get it working with hand-written data and eventually build tools to create the JSON in a more friendly, visual format. This was a great approach and has worked very well in general. However, I made a pretty grave error early on.

Masteroid has data models for Ships, Weapons, Factions, Sectors, GameSettings, PlayerSave and more. There is a very, very important difference in these models that I didn’t realize for months. I even had a conversation with a fellow dev and still didn’t appreciate the impact. Some of those models are essentially read only. Some of those models are read/write. I mixed the two and that has caused many problems.

Player data is read/write. The Player model is updated constantly when the player gains/loses experience, cash, resources, health, etc. That model is regularly saved to disk to store the player’s progress. The game modifies Player data constantly.

Ship data is generally read-only. A Ship model is essentially a template that describes the health, speed, energy, collision, engines and everything else about a ship. The Ship model owns a list of Turrets. This allows the game to pass a model to a ship and the runtime game entity gets all the right settings, turrets, engines, etc. Where I screwed up is that the Turret has a Weapon property. This was a major error. Everything else is read-only. But the weapon in a turret is read/write. A ship of type A always has the same properties but is given a unique weapon loadout for every spawned NPC. And the player customizes their loadout at will.

This means that all ships of type A can’t share the same model reference. If they did then Ship A changing the weapon in the Turret Model would also affect Ship B because they’re sharing a reference. Each ship needs unique loadouts. So, read-only and read/write data is mixed together.

As a result, I have to clone the template reference and then mount weapons in a unique copy of the model. Early on I had a lot of bugs with reference sharing where changing a weapon on one ship would accidentally change the weapon on multiple ships. This is a terrible pattern and has been a consistently kludgy issue I’ve had to work around from a month in. Ship, Weapon and similar templates should have been kept strictly read-only.

Problem 2

I built some really cool content-authoring tools for creating Sectors. The visual editor allows me to choose colors, visually edit sector design and save the whole thing out as JSON. I also built a sweet ship editor tool where you can load a sprite, lay out engines, turrets, collision etc.

Sectors don’t need a lot of balancing. Factions control roughly the same number of sectors at roughly the same levels and cargo payouts. So you can advance about the same with either faction. This means Sectors can be built in the tool without constantly referencing other Sector data.

But Ships (and weapons) must be balanced. Their resource storage, speed, energy, health, etc all need to be carefully balanced both against other ships and against the full range of weapons you can install. Players should generally not be able to mount the most powerful weapon in the game in the weakest, fastest ship and create completely unbalanced “minmax” builds. The best way to do this is to keep all this data in a spreadsheet, which I do. I have all kinds of performance indexes so as I create and tweak a ship (even before designing the art for it) I can tell how good it is relative to other ships, what it should cost, etc. I have calculations to give me a rough idea of how fast the ship can earn money mining, how well it performs in combat, etc. I use this to try to balance player progression so it’s an enjoyable curve from beginning to end.

I also have a couple of custom JavaScript methods written in the Google Sheet. It creates the JSON output for the ship on each row so I can balance all of the ships and, theoretically, have that data updated in game in minutes. But there’s a problem.

You can’t easily store collision points, turret attach points, and engine attach points in a spreadsheet. Sure, I could create multiple sheets with joins and roll that all up into a JSON object. But how do you get that collision data into the sheet? Manually enter each collision point? Some large ships have complex polygons. Small ships are essentially a four-point rectangle. Same goes for turrets and engines.

So. The visual-authoring tool I created is fantastic for visually laying out collision polygon, engines and turrets. But it’s terrible for balancing stats. The Google Sheet is great for balancing stats but is terrible for collision, turrets and engines.

Figuring out how to mesh these disparate sources of data for the same object has been a barrier to having quality, balanced content in the game!

Solution 1

This month I set out to fix the second problem. My idea was that I would define a standard layout for every class of ship (there are 10 classes or levels of ship in the game). So all Scout ships would share engine, turret and collision layout but have unique art and properties.

This way, new ships could be quickly created with unique visuals and stats but a common layout. The layout data would be defined in the in-game tool. The stats data would be defined in a sheet. The only downside is that the layouts would be a little more generic but there was still a lot of latitude to make ships unique artistically.

I set out to accomplish this. It required major refactoring of models. I needed to split collision, turrets and engines into a Layout model and give Ships a way to reference their Layout. As I was working on this I realized I could also fix Problem 1. I could separate the idea of Templates (read only) from the idea of Models (read/write). I also started doing some major cleanup of the Data Service, which has some dark and scum-filled corners. While I was doing this it also made sense to clean up a lot of the data utilities for things like Serializing and Deserializing JSON. Next to these were also the utilities used for tons of math calculations.

I was on a roll. Code quality was going up across the board. I moved the new Templates, Models, Utilities, Data Service and more into their own shared project. I wrote beautiful, clean code that changed how all of these things worked. I finished up with a nice set of shared utilities in a tidy class library.

Template references were shared because they were no longer allowed to be changed. Runtime-specific stuff would have to be stored in a separate data structure from the now-immutable Templates.

At this point the game was obviously completely broken. The data all needed to be split and transformed into the new Templates and Models. Tons of stuff in game needed to be refactored and fixed. New ways had to be devised to equip weapons to turrets. I had good ideas but warning bells were going off. Every change found four more things that could be improved and cleaned up.

Fifteen hours in I knew I had gone down a bad road. All my ideas were good. But I had touched every single part of the game and game data. I was double-digit hours away from even having something that would build. Let alone rebalancing and testing everything.

Skipping the May release was an obvious answer. But with that decision immediately came the stress and pressure of a “real” project. Now I have all of this work to be done to make my game work again and I’m stressed about getting back to a releasable state.

I took a step back.

Revisiting the Core Philosophy

A very core principal of Masteroid is that I “finish” the game every month. I can walk away any month. The game is in a playable, finished state that is worth what gamers paid for it (in my opinion).

I have no obligation to investors. I have no debt. I have delivered a project that I believe is worth the asking price right now. Masteroid is low stress because it’s a finished project that I can CHOOSE to make a little better each month. That freedom has been THE number one thing that has kept this game fun for me to work on, and continually improving the gamer experience!

So, I had started May with the pure developer desire to improve some technical debt. But I had actually broken lots more, brought on stress at the worst possible time, and lost sight of the goal. What was I trying to do anyway? Everything in the game worked before I started ripping things apart.

Solution 2

I committed the mess I had started for posterity. Then I backed up to the commit right before starting the major refactor (hooray for good commit discipline), pulled a new branch, and revisited the true essence of the problem:

“I want to efficiently add quality content to the game”

Sure there’s a problem with how models work. I’m not proud of all the cloning and stuff but it does work just fine. The biggest problem is that it’s hard to quickly balance and add unique ships. I realized that, while hacky, I could probably write a small tool to merge the data from two sources easily.

Part of the problem is that every ship, weapon, faction etc are saved as their own file. This is all wrapped up into a gamedata.dat file that ships with the game. I reworked the save files so that ships, weapons, factions and sectors are each clumped into their own file containing all the templates. I also reworked the ship editor Data Service to handle these four major data sources. In the process I took the best (and least destructive) ideas from the refactor and re-applied them to the data service and models to at least iterate on their quality a bit.

I wrote a tiny merge tool that takes the collision, turret and engine data from the game tool, and ship stats from the Google Sheets, and merges it into a game-ready file.

It took me 15 minutes to write that tool.

Finally, I also built in a way to retire unbalanced ships and weapons without breaking old player saved games. This is important because I need to test and experiment with new weapons and ships but also be able to strip out bad/unbalanced ideas without players losing their saved game. The new system keeps track of the cash value of retired items. If an item no longer valid in game, it refunds the player the cash value of that item and then removes it from their saved game.

Summary

At 10:30pm on May 31st I had the game fully back to working status. The game had 36 new weapons, 4 new ships, and gracefully handled retiring weapons or ships for balancing reasons. It also had some cool little improvements to stations and some other stuff irrelevant to the context of this post. Technically, I had just enough time to squeeze a May release under the bar and plenty of new content to be release-worthy.

Unfortunately I realized that the game needed significantly more playtesting and balancing of the new weapons and ships. Releasing the game in its end-of-May state would have offered players a progression experience that might ruin the game for them entirely.

Even though I had come full circle into a position where releasing a May build was possible…I decided not to do it. The only reason I release monthly is to keep me focused and keep people interested in the game. I still finished a unit of work but releasing it might amass more technical debt and deliver a bad experience. The deadlines are self-imposed and arbitrary.

Ultimately I decided to write out this whole saga because I think this is a metaphor for software development in general. It’s so easy to fall into a trap of building something because it seems “better” or “the right way” to do it. And in the process completely lose sight of the original goal. I used to be pedantic about code. I wanted everything to be “right”. The older I get the more I realize that there is no “right”. Right changes over time. And what will be right next year is unknowable today. The best thing you can do is stay focused on the problem and build a solution today that fits that problem. If you can make that solution extensible and modular that’s a bonus.

The best developer is the developer that can build it just big enough to support tomorrow’s ideas but meet today’s goal.

This is likely the 20th time I’ve learned this in my career and it’s unlikely to be the last.

Categories: GameDevSoftware

Leave a Reply

Your email address will not be published. Required fields are marked *