One of the core building blocks of making games is data.
On its own, data is nebulous and unconnected.
But when you structure it into something that tells you the relationship between it and
what it represents, you give that data meaning.
Here’s an example of some structured data in Unity.
This class represents an item with a name, a description, and a price.
This one represents a shop with a name, an amount of gold, and a supply of products.
And this one represents a player with an amount of gold and an inventory.
Both the shop and player classes have a list of items, but I’ve used different variable
names to better describe what each list actually represents.
This is how we assign meaning to our data, by organizing it all into distinct classes
which are known as data structures.
Once you’ve defined your data structures you can assign even more meaning through the
use of objects.
Objects add behaviour to your data structures and make them easier to understand and work
with.
Here’s an example of an object that encapsulates the usage of items.
We can use it to perform common operations, like checking whether or not the player can
afford a particular item or gathering all of the items that are cheaper or more expensive
than a particular amount.
Another use case for objects is persistence as is the case for the repository pattern.
The repository pattern is a design pattern that’s used to decouple your game code (or
business logic) from the data access layers in your application.
In other words, it completely separates the source of your data from the usage of your
data.
The way it works is that you define objects, called repositories, that are responsible
for creating, reading, updating, and deleting data from an arbitrary source.
These are known as CRUD operations and the source could be a text file, a database, or
even an API that’s hosted on the web.
To access this source, repositories use an abstraction which is generally called a data
context.
The data context is what decides where the data comes from, and you can implement more
than one to access multiple streams of data.
Let’s take a look at an example of the repository pattern in Unity.
But first, if you’re interested in content like this, then sign up for our monthly Level
2 Game Dev newsletter.
Level 2 is all about helping you develop the skills needed to take your game dev hobby
or career to the next level.
Once a month, we’ll send you an email with curated content that’s designed to help
push you on your game dev journey and help you keep your finger on the pulse of what’s
going on in the industry.
If that sounds good to you, visit the link in the description to sign up now!
Now, as with all design patterns, there are many ways to implement the repository pattern.
And there are even frameworks that do all of the heavy lifting for you (which I don’t
necessarily recommend).
But for the sake of simplicity, here’s a simple implementation that I created for this
video.
We’ll start with the GameData class.
In order to contextualize our data, we need to encapsulate which data structures we want
to persist, which is exactly what this class represents.
For now, we’re just gonna store a list of players and a list of shops.
Simple as that.
Next, we need a context for that data.
The DataContext class keeps a record of the game’s data.
It’s abstract because we don’t know exactly where the data is coming from or how to save
new data that gets generated by the user.
That’s why there are two abstract methods that need to be implemented: Load and Save.
To make things easier, DataContext has one implemented method called Set that we can
use to grab a subset of the game data based on a type, using C# generics.
For this video I’ve implemented a single context which is able to grab and store data
in a JSON file.
Here’s what it looks like.
It basically uses Unity’s built-in JsonUtility to serialize and deserialize data to and from
a json file which you can define in the editor.
Finally we have the base Repository class that each of our individual repositories will
derive from.
In a nutshell, it implements all of the CRUD methods I mentioned earlier using an instance
of DataContext that could change depending on where we want a particular repository to
source it’s data from.
It uses C# generics as well to make it easier to implement a repository for each data structure
in our model, as we can see here with our two implementations: the Shops and Players
classes.
So that’s the nuts and bolts of my implementation of the repository pattern.
Let’s switch over to Unity and see it in action.
In the scene, I’ve got a UI for the shop that’s being driven by the ShopWindow class.
We’ll take a look at that in a moment.
When I press play we can see the UI gets populated with items.
Both the shop and player’s gold is displayed, as well.
I can purchase items by pressing the buy button.
Doing so will update the UI to show items being removed from the shop, the shop gaining
gold, and the player losing the gold that was spent.
Now let’s stop and restart the scene.
Look at that: all of my purchases have persisted thanks to our repositories.
Let’s take a look at the ShopWindow class to see how it’s all wired up.
More specifically, let’s look at the mechanics of the Buy method.
Buy gets called when we press the buy button, and it’s where all of our persistence operations
take place.
First it grabs an instance of the player and the shop using a set of IDs.
For this demonstration, both IDs are set via some public fields.
But in a real implementation, they’d probably be injectected when the player interacts with
a shopkeeper NPC.
The player and shop instances are each retrieved from their respective repositories, which
are wired up to use the Json data context.
Next it does a couple of checks to make sure that the item exists in the shop and the player
has enough gold.
And then it subtracts the price of the item from the player’s gold before finally sending
an updated version of the player to the repository and saving the transaction.
The same is done for the shop.
Although the code might look a little different in production, this is pretty much how you’d
use the repository pattern in your own code.
It makes working with data much easier.
Plus it’s flexible enough to support changes to your data sources and can scale to support
as many data structures as you need to persist.
However there are a couple of problems that we still need to address.
Let’s imagine that this code was a little more complicated.
Maybe purchasing an item means we need to update a quest objective.
Or collecting a certain amount of items triggers an achievement.
Anything that could happen in response to the player gaining a new item.
It could happen out of band as a response to an event, or in-line right here in the
Buy method.
But what would happen if, in the middle of everything, an exception was thrown
and, as a result of this exception, the shop repository never got saved?
Well the answer is simple: your data would be out of sync and there would be no way to
recover.
This is an issue of data concurrency.
Data concurrency is the ability to allow multiple parts of your application to affect multiple
transactions within a single database.
Simply put, data concurrency allows many different parts of your application to access the same
data all at the same time.
To facilitate this, you’d need a way to queue up or log changes to your data in memory
before writing all of those changes to the actual source.
Which, in our case, is a JSON file.
However the repository pattern wasn’t intended to solve this issue on it’s own.
So we’ll need something else to help us out.
But before we look at that, there’s one other problem that we need to solve.
If we peek into the Save method of our base Repository class and then follow it all the
way down to our JSON implementation of the DataContext class, we can see that it’s
responsible for writing data directly to the source.
Every time it gets called it has to open up a data stream and push new data into the actual
JSON file.
If you’ve worked with data access before, you’ll know that opening streams like this
is very expensive.
Now of course the time it takes to open a JSON file that lives on my SSD is pretty negligible,
but imagine if our data source lived in the cloud and could only be accessed via a slow
API call.
If I wasn’t careful, this would become a performance bottleneck very quickly.
So how do we solve these two problems: that of data concurrency and performance bottlenecks
caused by data access?
Luckily, both of these problems occur commonly enough that an entire pattern was created
to solve them.
That pattern is called “Unit of Work”.
A Unit of Work maintains a list of objects affected by a business transaction and coordinates
the writing out of changes and the resolution of concurrency problems.
The idea is that the Unit of Work keeps track of every change that happens and writes them
all at the same time when you tell it to.
The shop example is a little trivial because it only makes changes to two objects and only
calls the Save method twice, but I think it’ll serve to illustrate the point.
First, we’ll create a class called UnitOfWork.
UnitOfWork will be responsible for coordinating all of our persistence logic so it’ll need
a reference to the DataContext and all of the repositories.
And since we’ll need access to those repositories, we’ll expose them through a couple of public
properties.
Finally, let’s add a Save method that delegates to the data context.
And that’s it.
Now we can put our UnitOfWork to work in the ShopWindow class.
We’ll start adding a reference.
Then we’ll replace each instance of a repository with a pass through call to the UnitOfWork.
And instead of calling save on each repository, we’ll simply call save on the UnitOfWork.
Finish with a little clean up and voila!
That’s all there is to it.
Our problems are solved and the code still works with very minimal changes
Persistence presents an interesting challenge for game developers.
Your data must maintain its integrity so your players have the experience that you designed.
And jankiness during gameplay as your code attempts to open and close the same data source
over and over again will only make your players turn away.
Thankfully, the repository pattern with the unit of work presents tried and tested solutions
for both of these problems.
But this isn’t the only approach to saving data for your games.
Let me know what you think about these patterns or if you save data in a completely different
way in the comment section below.
I’d love to hear all about it.
If you enjoyed this content, be sure to sign for the Level 2 Game Dev Newsletter.
And of course, like this video and subscribe to Infallible Code.
Thanks for watching.
As always, I’ll catch you in the next video.