A Bodeville Guide to Unity Game Architecture, Part 1: Configuration Data

Bo Boghosian
5 min readJul 27, 2023

--

At Bodeville we started a new project less than three weeks ago. In that time, I’ve built a nearly complete game loop with at least ten different screens, a tool for importing and exporting game tuning data, a fully hierarchical game state tree, and some of the more specific parts of our game, including a basic dollmaker and a prototyped time-based simulation mechanic.

Over the next month I’ll write posts about a few systems I built in the first weeks of development. The components I’ve built are generally small, simple, but effective. Today’s post is about game configuration data.

Game configuration data management

Our new game is a simulation that is tentatively called Grift. Simulations usually have a lot of tabular data that game designers are continuously tweaking and balancing. Even before I had written a line of code, Alexia already had several spreadsheets of game configuration data in Google Sheets.

Clearly, this game would need a tool for managing this. I’d typically reuse a tool for this but our first game, Chief Emoji Officer, was a simple text adventure that didn’t require configuration data. So I started from scratch.

A blank project can be intimidating. I often spin my wheels for a bit trying to design the perfect system in my head. This usually leads me nowhere. Then I reach a point where I force myself to start writing some code, any code. I knew I’d need to define some kind of plain C# data type for the dollmaker accessories, so I started writing it. After a few iterations the end result is here:

public class AccessoryGmt {
private readonly string id;
private readonly int aesthetic;
private readonly int costSoftCurrency;
private readonly int costHardCurrency;
private readonly string type;

public AccessoryGmt(string id, int aesthetic, int costSoftCurrency, int costHardCurrency, string type) {
this.id = id;
this.aesthetic = aesthetic;
this.costSoftCurrency = costSoftCurrency;
this.costHardCurrency = costHardCurrency;
this.type = type;
}

public string Id => id;
public int Aesthetic => aesthetic;
public int CostSoftCurrency => costSoftCurrency;
public int CostHardCurrency => costHardCurrency;
public string Type => type;
}

Next, I started building the Unity GUI to display these Accessory data types, and quickly realized that I would need a way to define the multiple different GMTs available.

A bit of nomenclature — GMT is short for “game master template”. I use this name out of habit since I’ve used it in the past. I’ve also seen these systems called “game tuning data”, “configuration data”, or simply “game data”. Call it what you will.

Anyway, I started writing a definition interface so the Unity tool knew about the different types of GMT it would have to load. The definition tells the tool which type it is, where to load the data from, and how to parse the TSV into an object instance. Here it is, along with a specific implementation for the dollmaker accessories.

public interface GmtTypeDefinition {
Type Type { get; }
string DisplayName { get; }
string TsvFilePath { get; }
object FromTsv(string[] fields);
}

public class AccessoryTypeDefinition : GmtTypeDefinition {

public Type Type => typeof(AccessoryGmt);
public string TsvFilePath => "Assets/Gmt/AccessoryData.tsv";
public String DisplayName => "Accessory";

public object FromTsv(string[] fields) {
return new AccessoryGmt(fields[0], int.Parse(fields[1]),
fields[2].Trim().Length == 0 ? 0 : int.Parse(fields[2]),
fields[3].Trim().Length == 0 ? 0 : int.Parse(fields[3]),
fields[4]);
}
}

Experienced programmers might cast a wary eye at the FromTsv method. It’s parsing a line of TSV in a totally unstructured way, hoping that columns 1, 2, and 3 are indeed integers. There’s no validation of the column headers. If the column order changes, this code would indeed start to fail.

It’s hard to argue with that logic. This method is undoubtedly flaky. On the other hand, is it sufficient for what I need right now? Absolutely. It unblocks me and it’s something that I’ll most likely improve later on.

The editor tool

Now onto the Unity Editor tool. It’s simple as can be. There’s a dropdown for the type, then the data table itself, and buttons to import and export.

The GMT data is not editable in the tool — how lame! But this is a very intentional design decision. Microsoft Excel and Google Sheets are already amazing tools for editing tabular data, and no matter what I built in the Unity tool, it would never match the capability of those programs.

Instead, the tool offers two buttons — export to clipboard and import from clipboard

We use TSV because it’s the native format for copying and pasting from Google Sheets. After a single click of the “Export TSV” button we can paste from the clipboard directly into Sheets. It doesn’t get easier.

This simplicity also saves boatloads of time. I could have spent days adding an edit feature to the Unity tool, but it would never be as powerful as a regular spreadsheet application.

Now in the tool, you can see where those type definitions from earlier come into play.

private static List<GmtTypeDefinition> types = new() {
new AccessoryTypeDefinition(),
new ScammeeTypeDefinition(),
new StringTypeDefinition()
};

For each of the defined data types, the tool creates a menu option. Then, for the selected type, the tool loads the associated TSV file, shows the data in a table, then lets you import TSV or export TSV. It’s not rocket science, but it’s quite effective for our needs.

if (GUILayout.Button("Export TSV to clipboard")) {
if (File.Exists(gmtDefinition.TsvFilePath)) {
StreamReader reader = new StreamReader(gmtDefinition.TsvFilePath);
EditorGUIUtility.systemCopyBuffer = reader.ReadToEnd();
reader.Close();
} else {
// If the file doesn't exist just copy the headers.
EditorGUIUtility.systemCopyBuffer = string.Join('\t', gmtDefinition.Type
.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
.Select(info => info.Name));
}
}

if (GUILayout.Button("Import TSV from clipboard")) {
var writer = new StreamWriter(gmtDefinition.TsvFilePath);
writer.Write(EditorGUIUtility.systemCopyBuffer);
writer.Close();
}

Loading the data at runtime

The final part is loading these data at runtime. Here’s an abridged version with just the method signature, but the implementation is straightforward.

public class GmtLoader {

public List<T> LoadAll<T>(GmtTypeDefinition definition) {
// Read data from the file located at GmtTypeDefinition.TsvFilePath
// then create C# objects using GmtTypeDefinition.FromTsv(string)
}
}

Just a half day of code, and this tool was up and running. These were the first commits I added to the repo and I have the git history to prove it!

% git log --pretty=oneline
ff6a922903349e99eee55c3e1d5f96aa25c8d912 gmt tool 2
421e9fe0072c28d993e11ff67b35086bcf4ce16f gmt tool start
65606505f43f9f7df36c9b030e82afb396499637 empty project

To faster beginnings

I’m usually so excited to start a new game that I punt on writing tools. This instance was the exception to that rule, and I’m glad I did it this way. Within a week, Alexia and I had already defined three different GMT data types and filled three tables of data.

That’s it for this week. Next time I will cover a game state system. This is a way to cleanly structure the backbone of a project in order to maximize development speed and (hopefully) minimize an entire class of lifecycle bugs.

--

--

Bo Boghosian
Bo Boghosian

Written by Bo Boghosian

Co-founder, Bodeville. Chief Emoji Officer available for steam wishlist! http://tinyurl.com/chiefemoji

No responses yet