Unit of Work Pattern in Unity

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Infallible Code
Views: 9,993
Rating: undefined out of 5
Keywords: unity unit of work, unit of work unity, unit of work in unity, programming in unity, programming for unity, unity programming tutorial, unity programming tutorials, unity design patterns, design patterns unity, design patterns in unity, unity coding, coding for unity, game programming, unity game programming, game programming for unity, game development, game dev, unity, unity3d, unity 3d, infallible code, charles amat
Id: 5D4qHl3SeoA
Channel Id: undefined
Length: 11min 50sec (710 seconds)
Published: Fri Dec 17 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.