Version control for Video Game Development using Dolt

9 min read

Dolt is the world's first SQL database that you can branch and merge, push and pull, fork and clone just like a git repository. A lot of people, when they hear about this, say "that's really cool, but what would I do with it?" The answer of course is lots of things! But today I want to focus on one use case where we think Dolt really shines: video game development.

game development GUI

We are already working with a few different customers building their games on top of Dolt, and we think this is a perfect use case for a version controlled database. The image above is a GUI one of our customers built using Dolt to assist in developing their RPG, which you can read more about here.

If you haven't thought much about the role of version control in video game development for things other than the source code, this post is for you.

Game development on a team

Modern game development is a team effort, just like producing any media. For the source code of the game, developers collaborate using version control, usually Git these days.

But the work of game development encompasses much more than writing code. In fact, usually a far bigger task is what's called asset creation: creating levels, characters, stories, dialog, quests, items, enemies, and so on. Programmers have source control to collaborate. What do the level designers, character modelers, and the rest of the people who collaborate on game production have?

file versions

Version control can help with the management of large binary files like artwork involved in game development, but the binary nature of the files makes the features of version control much less useful -- for example, there's usually no way to sanely merge two revisions of an image file together, the way there is with source code. Unity even recommends telling game artists to lock files they're working on to prevent the problem of someone else making edits at the same time.

gluon UI

But there's one class of game asset that is especially amenable to version control, but which many game developers don't even consider an asset. And Dolt can be especially useful managing it.

Game configuration parameters as an asset

Most games go through a process of tuning, where the developers experiment with different parameters to find the ones that give the best gameplay. There are lots of ways to encode this parameter or configuration information into a game, and many ways to version control it. Let's examine a few of them.

For all of these methods we'll consider the example of an RPG where enemies have stats and sometimes drop items when slain.

RPG enemies

The first and most obvious way to encode this information is to embed these values into the source code itself. So you could have something like this:

enemy = slime(4 /* HP */, 1 /* XP */);
enemy.addReward("Potion", 0.2);

This isn't terrible, and version control lets you experiment with different values on different branches. A slightly better version might be to use constants for these values instead so you can change them in one place:

#define SLIME_HP 4
#define SLIME_XP 4
#define SLIME_POTION_DROP 0.2

enemy = slime(SLIME_HP, SLIME_XP);
enemy.addReward("Potion", SLIME_POTION_DROP);

You could imagine defining a single file where all such parameters live, all managed by source control. Not a bad solution.

However, it has one pretty big drawback: because it's stored as text constants in source control, it's not easy to manipulate with other programs. To make it tractable to change lots of parameters during experimentation, you really want to store them in some sort of structured from, not just plain text like source code.

For example, imagine the scenario where your play testers are having too hard a time on a particular level, so you decide that all the enemies need to drop healing items twice as often. For this to be feasible to experiment with, you really want to move these values out of the source code and into structured data. For example, here's a way to represent it with JSON:

{
    "enemies": [
        {
            "name": "slime",
            "HP": 4,
            "XP": 1,
            "drops": [
                {"potion": 0.2}
            ],
        },
        {
            "name": "metal slime",
            "HP": 4,
            "XP": 1024,
            "drops": [
                {"elixir": 0.5}
            ],
        }
    ]
}

Now it's possible for me to write a little program to examine all these values and update whatever I want, in bulk. I can check this file into source control and still get all the benefits of collaboration, including running experiments on branches. That's a lot better than the embedded source code solution, but we can do even better.

Tabular game data

As several of our customers have realized, it's very convenient to represent game parameters as SQL tables, which you can then manipulate with SQL statements. For example, here's how you might represent the data about enemy attributes and their item drops.

CREATE TABLE `enemies` (
    `enemy_id` INT PRIMARY KEY NOT NULL,
    `name` VARCHAR(1024) NOT NULL,
    `hp` INT NOT NULL,
    `xp` INT NOT NULL
);

CREATE TABLE `reward_drops` (
    `enemy_id` INT NOT NULL,
    `item_id` INT NOT NULL,
    `chance` INT NOT NULL,
    KEY (`enemy_id`)
);

INSERT INTO enemies VALUES (1, "slime", 4, 1);
INSERT INTO reward_drops VALUES (1, 2, 0.2);

Defining game parameters this way makes bulk editing and experimentation a breeze. If I want to make my enemies drop items more often, I can really easily try that out.

UPDATE reward_drops set chance = least(chance * 1.2, 1);

Just like that, I've updated every enemy in the game to be more generous. No need to write a custom program to find and edit every necessary parameter in a source control file. This is the strength of SQL.

If I want to increase the challenge by making every enemy stronger, but only in the early game, that's simple too:

UPDATE enemies SET hp = hp + 5 WHERE hp < 20;

SQL has been around for a long time, and its lasting success is due to how easy it makes it to query and manipulate large amounts of structured data. It can be a great choice to store configuration information, not just for game development, but in lots of different domains. It's definitey a lot easier to work with than a 40,000-line JSON or YAML file.

Dolt for versioning and experimentation

In the above examples, if you ran these UPDATE statements on a traditional SQL database, you would be making changes to the only copy of the data you had. Whatever values were there before are gone forever after the UPDATE statement runs.

Dolt is different: every step of the history of your database is saved so you can roll back to a previous revision if you need to, and you can create branches of the database to run experiments on. Let's look at the above workflow with Dolt.

For this example I've created the database with the initial version of the data, before any updates.

% dolt log
 commit dklitc793rnb9k2qri78pm7kecdgdhbl (HEAD -> main)
 Author: Zach Musgrave <zach@dolthub.com>
 Date:  Tue Feb 07 20:28:09 -0800 2023

     Initial enemy and reward tables

 commit al008irgf9rshroi09vcc73o27d71r9v
 Author: Zach Musgrave <zach@dolthub.com>
 Date:  Tue Feb 07 20:27:27 -0800 2023

     Initialize data repository
     

Now let's create a branch to apply our experimental game data update on. Dolt's command line copies git exactly, so all the commands you know for git work the same.

% dolt checkout -b more-rewards
Switched to branch 'more-rewards'
% dolt sql -q "UPDATE reward_drops set chance = least(chance * 1.2, 1);"
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Now I can examine what my change did and commit it if I'm happy with it.

% dolt diff
diff --dolt a/reward_drops b/reward_drops
--- a/reward_drops @ mn32q8fd6fv4i65qlfl0nhic398k6mm9
+++ b/reward_drops @ djtob7dvkfq3gu4spvcnrh6kj52lpamr
+---+----------+---------+------------+
|   | enemy_id | item_id | chance     |
+---+----------+---------+------------+
| - | 1        | 2       | 0.2        |
| + | 1        | 2       | 0.24000001 |
+---+----------+---------+------------+
% dolt commit -am "20% higher drop rate"
 commit 2kgh5gpfv6uig9kcg04ahj5i535803m9 (HEAD -> more-rewards)
 Author: Zach Musgrave <zach@dolthub.com>
 Date:  Tue Feb 07 20:32:51 -0800 2023

     20% higher drop rate

Now let's make the alternate change, making harder enemies, on a different branch using the same workflow.

% dolt checkout main
Switched to branch 'main'
% dolt checkout -b tougher-enemies
Switched to branch 'tougher-enemies'
% dolt sql -q "UPDATE enemies SET hp = hp + 5 WHERE hp < 20;"
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
% dolt diff
diff --dolt a/enemies b/enemies
--- a/enemies @ vpcddm0r6sbmrd807kc76pah4tdacj9d
+++ b/enemies @ 0icsdq7a2bsb8d2lismujplgfca1gadh
+---+----------+-------+----+----+
|   | enemy_id | name  | hp | xp |
+---+----------+-------+----+----+
| < | 1        | slime | 4  | 1  |
| > | 1        | slime | 9  | 1  |
+---+----------+-------+----+----+
% dolt commit -am "+5 HP to all weak enemies"
 commit nt3djf1rjih7sa65gnmu4nkqpsn011jc (HEAD -> tougher-enemies)
 Author: Zach Musgrave <zach@dolthub.com>
 Date:  Tue Feb 07 20:36:25 -0800 2023

     +5 HP to all weak enemies

If I'm happy with both these changes, I can go ahead and merge them back to main and see the result.

% dolt checkout main
Switched to branch 'main'
% dolt branch
* main
  more-rewards
  tougher-enemies
% dolt merge more-rewards
Updating dklitc793rnb9k2qri78pm7kecdgdhbl..2kgh5gpfv6uig9kcg04ahj5i535803m9
Fast-forward
% dolt merge tougher-enemies
Updating 2kgh5gpfv6uig9kcg04ahj5i535803m9..nt3djf1rjih7sa65gnmu4nkqpsn011jc
enemies | 1 *
1 tables changed, 0 rows added(+), 1 rows modified(*), 0 rows deleted(-)
% dolt diff HEAD~2
diff --dolt a/enemies b/enemies
--- a/enemies @ vpcddm0r6sbmrd807kc76pah4tdacj9d
+++ b/enemies @ 0icsdq7a2bsb8d2lismujplgfca1gadh
+---+----------+-------+----+----+
|   | enemy_id | name  | hp | xp |
+---+----------+-------+----+----+
| < | 1        | slime | 4  | 1  |
| > | 1        | slime | 9  | 1  |
+---+----------+-------+----+----+
diff --dolt a/reward_drops b/reward_drops
--- a/reward_drops @ mn32q8fd6fv4i65qlfl0nhic398k6mm9
+++ b/reward_drops @ djtob7dvkfq3gu4spvcnrh6kj52lpamr
+---+----------+---------+------------+
|   | enemy_id | item_id | chance     |
+---+----------+---------+------------+
| - | 1        | 2       | 0.2        |
| + | 1        | 2       | 0.24000001 |
+---+----------+---------+------------+

Again, Dolt works just like Git, including for things like clone, push and pull to remotes. So a team can all dolt clone the same configuration dump, make their changes on branches, and use PR workflows to get them merged back into main.

Dolt also ships with a built-in MySQL compatible server, so any database GUI that can connect to MySQL should work out of the box with Dolt as well. This is great for teammates that aren't as comfortable with SQL and just want to do data entry like with a spreadsheet.

Deploying configuration changes

With any external configuration management setup, at some point you have to get changes into the game itself. There are two general approaches here, either of which might be better in certain circumstances.

software lifecycle

The first is straightforward: export your constants from the config into source code and then build it into the binary. In this workflow, you treat the external config store (the database or the config file) as the master and dump it into a format that can be consumed by the game binary. This could be as simple as dumping to CSV and then writing a little glue code for the game binary to use when building.

% dolt dump -r csv
Successfully exported data.
% ls
doltdump
% ls doltdump
enemies.csv  reward_drops.csv
% cat doltdump/enemies.csv
enemy_id,name,hp,xp
1,slime,9,1

This could be called the "traditional" method of game release, where the end product runs entirely on the player's hardware. To get your players a new version of the game for a bug fix or other update, you build and ship them a new release.

The main drawback with this approach is working with publishers or digital storefront owners, who often impose strict requirements for their certification process and may even charge fees for the service. These processes make it impractical to ship game updates as quickly as the development team can make them, especially for smaller companies.

game submission

The second method involves loading game configuration dynamically at runtime, rather than building it into the binary. This can be especially attractive for games that have an online component, as it's a more nimble alternative to the client patch and release cycle that lets you iterate much more rapidly. With this method, you ship your game binary with some default fallback config values, then make RPCs to your game servers to get the most up to date ones at runtime. This approach makes a ton of sense in the context of online multiplayer, where players already expect to connect to game servers anyway.

The online method allows some really interesting possibilities for tuning your game using the live players as testers. For example, you could A/B test different config values for two different experimental groups of players, then examine the results to find which configuration gives more balanced gameplay results. Using Dolt, this would mean serving different branches of the data to the different experimental player groups.

Conclusion

We're really excited about the game development use case for Dolt and think it will continue to grow in popularity. Several customers are already building games on top of Dolt and have discovered that it makes their development process much easier to manage.

Does this sound interesting? Have questions about version controlling your game's configuration data? Come talk to our engineering team on Discord. We're always happy to talk to new customers.

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.