Persistence in Unity Using Realm
Rate this tutorial
When creating a game with Unity, we often reach the point where we need to save data that we need at a later point in time. This could be something simple, like a table of high scores, or a lot more complex, like the state of the game that got paused and now needs to be resumed exactly the way the user left it when they quit it earlier. Maybe you have tried this before using
PlayerPrefsbut your data was too complex to save it in there. Or you have tried SQL only to find it to be very complicated and cumbersome to use.
Realm can help you achieve this easily and quickly with just some minor adjustments to your code.
To make it easy to find your way around this example, here are some notes to get you started:
Pieceshave to be activated first, which happens by making them clickable as well.
Piecesare not initially added to the
Boardbut instead will be spawned by the
PieceSpawner. You can find them in the
Prefabsfolder in the
The important part to look for here is the
Piecescript which detects clicks on this
Piece(3) and offers a color change via
Deselect()(2) to visualize if a
Pieceis active or not.
We use two events to actually track the click on a
Piece(1) or a
InputListenerwaits for those events to be invoked and will then notify other parts of our game about those updates. Pieces need to be selected when clicked (1) and deselected if another one was clicked (2).
Pieceis selected will send a message (3) to the
GameStateto update the position of this
The actual movement as well as controlling the spawning and destroying of pieces is done by the
GameState, in which all the above information eventually comes together to update
Piecepositions and possibly destroy other
Pieceobjects. Whenever we move a
Piece(1), we not only update its position (2) but also need to check if there is a
Piecein that position already (3) and if so, destroy it (4).
In addition to updating the game while it is running, the
GameStateoffers two more functionalities:
- set up the initial board (5)
- reset the board to its initial state (6)
Go ahead and try it out yourself if you like. You can play around with the board and pieces and reset if you want to start all over again.
To make sure the example is not overly complex and easy to follow, there are no rules implemented. You can move the pieces however you want. Also, the game is purely local for now and will be expanded using our Sync component in a later article to be playable online with others.
In the following section, I will explain how to make sure that the current game state gets saved and the players can resume the game at any state.
The first thing we need to do is to import the Realm framework into Unity. The easiest way to do this is by using NPM.
You'll find it via
Package Manager→ cogwheel in the top right corner →
Advanced Project Settings:
Scoped Registries, you can add the
NPMas a source for libraries. The final step is to tell the project which dependencies to actually integrate into the project. This is done in the
manifest.jsonfile which is located in the
Packagesfolder of your project.
Here you need to add the following line to the
manifest.jsonshould look something like this:
When you switch back to Unity, it will reload the dependencies. If you then open the
Package Manageragain, you should see
Realmas a new entry in the list on the left:
We can now start using Realm in our Unity project.
Before we actually start adding Realm to our code, we need to think about how we want to achieve this and how the UI and database will interact with each other.
There are basically two options we can choose from: top-down or bottom-up.
The top-down approach would be to have the UI drive the changes. The
Piecewould know about its database object and whenever a
Pieceis moved, it would also update the database with its new position.
The preferred approach would be bottom-up, though. Changes will be applied to the Realm and it will then take care of whatever implications this has on the UI by sending notifications.
Let's first look into the initial setup of the board.
The first thing we want to do is to define a Realm representation of our piece since we cannot save the
MonoBehaviourdirectly in Realm. Classes that are supposed to be saved in Realm need to subclass
RealmObject. The class
PieceEntitywill represent such an object. Note that we cannot just duplicate the types from
Piecesince , like
Add the following scripts to the project:
Even though we cannot save the
PieceType(1) and the position (2) directly in the Realm, we can still expose them using backing variables (3) to make working with this class easier while still fulfilling the requirements for saving data in Realm.
Additionally, we provide a convenience constructor (4) for setting those two properties. A default constructor (6) also has to be provided for every
RealmObject. Since we are not going to use it here, though, we can set it to
Note that one of these backing variables is a
RealmObjectitself, or rather a subclass of it:
EmbeddedObject(7). By extracting the position to a separate class
PieceEntityis more readable. Another plus is that we can use the
EmbeddedObjectto represent a 1:1 relationship. Every
PieceEntitycan only have one
Vector3Entityand even more importantly, every
Vector3Entitycan only belong to one
PieceEntitybecause there can only ever be one
Pieceon any given
Vector3Entity, like the
PieceEntity, has some convenience functionality like a constructor that takes a
ToVector3()function (9) and the private, mandatory default constructor (10) like
Looking back at the
PieceEntity, you will notice one more function:
OnPropertyChanged(5). Realm sends notifications for changes to fields saved in the database. Since we expose those fields using
Position, we need to make sure those notifications are passed on. This is achieved by calling
The next step is to add some way to actually add
Realm. The current database state will always represent the current state of the board. When we create a new
PieceEntity—for example, when setting up the board—the
GameObjectfor it (
Piece) will be created. If a
Piecegets moved, the
PieceEntitywill be updated by the
GameStatewhich then leads to the
GameObjectbeing updated using above mentioned notifications.
First, we will need to set up the board. To achieve this using the bottom-up approach, we adjust the
The important change here is
CreateNewBoard. Instead of spawning the
Pieces, we now add
PieceEntityobjects to the Realm. When we look at the changes in
GameState, we will see how this actually creates a
Here we just wipe the database (1) and then add new
PieceEntityobjects (2). Note that this is wrapped by a
realm.writeblock. Whenever we want to change the database, we need to enclose it in a write transaction. This makes sure that no other piece of code can change the database at the same time since transactions block each other.
The last step to create a new board is to update the
GameStateto make use of the new
PieceEntitythat we just created.
We'll go through these changes step by step. First we also need to import Realm here as well:
Then we add a private field to save our
Realminstance to avoid creating it over and over again. We also create another private field to save the collection of pieces that are on the board and a notification token which we need for above mentioned notifications:
Awake, we do need to get access to the
Realm. This is achieved by opening an instance of it (1) and then asking it for all
PieceEntityobjects currently saved using
realm.All(2) and assigning them to our
Note that collections are live objects. This has two positive implications: Every access to the object reference always returns an updated representation of said object. Because of this, every subsequent change to the object will be visible any time the object is accessed again. We also get notifications for those changes if we subscribed to them. This can be done by calling
SubscribeForNotificationson a collection (3).
Apart from an error object that we need to check (4), we also receive the
sender(the updated collection itself) with every notification. For every new collection of objects, an initial notification is sent that does not include any
changesbut gives us the opportunity to do some initial setup work (5).
In case we resume a game, we'll already see
PieceEntityobjects in the database even for the initial notification. We need to spawn one
PieceEntityto represent it (6). We make use of the
PieceSpawnerto achieve this. In case the database does not have any objects yet, we need to create the board from scratch (7). Here we use the
CreateNewBoardfunction we added earlier to the
On top of the initial notification, we also expect to receive a notification every time a
PieceEntityis inserted into the Realm. This is where we continue the
CreateNewBoardfunctionality we started in the
PieceSpawnerby adding new objects to the database. After those changes happen, we end up with
changes(8) inside the notifications. Now we need to iterate over all new
PieceEntityobjects in the
sender(which represents the
pieceEntitiescollection) and add a
Piecefor each new
PieceEntityto the board.
Apart from inserting new pieces when the board gets set up, we also need to take care of movement and pieces attacking each other. This will be explained in the next section.
Whenever we receive a click on a
Squareand therefore call
GameState, we need to update the
PieceEntityinstead of directly moving the corresponding
GameObject. The movement of the
Piecewill then happen via the
PropertyChangednotifications as we saw earlier.
Before actually moving the
PieceEntity, we do need to check if there is already a
PieceEntityat the desired position and if so, destroy it. To find a
newPositionand also to find the
PieceEntitythat needs to be moved from
newPosition, we can use queries on the
By querying the collection (calling
Filter), we can look for one or multiple
RealmObjects with specific characteristics. In this case, we're interested in the
RealmObjectthat represents the
Piecewe are looking for. Note that when using a
Filterwe can only filter using the Realm properties saved in the database, not the exposed properties (
PieceType) exposed for convenience by the
If there is an
attackedPieceat the target position, we need to delete the corresponding
GameObject(1). After the
attackedPieceis updated, we can then also update the
Like the initial setup of the board, this has to be called within a write transaction to make sure no other code is changing the database at the same time.
This is all we had to do to update and persist the position. Go ahead and start the game. Stop and start it again and you should now see the state being persisted.
The final step will be to also update our
ResetGamebutton to update (or rather, wipe) the
Realm. At the moment, it does not update the state in the database and just recreates the
Resetting works similar to what we do in
Awakein case there were no entries in the database—for example, when starting the game for the first time.
We can reuse the
CreateNewBoardfunctionality here since it includes wiping the database before actually re-creating it:
With this change, our game is finished and fully functional using a local
Realmto save the game's state.
In this tutorial, we have seen that saving your game and resuming it later can be easily achieved by using
The steps we needed to take:
Realmvia NPM as a dependency.
Realmin any class that wants to use it by calling
- Create a new
Realm.GetInstance()to get access to the database.
- Define entites by subclassing
RealmObject(or any of its subclasses):
- Fields need to be public and primitive values or lists.
- A default constructor is mandatory.
- A convenience constructor and additional functions can be defined.
- Write to a
realm.Write()to avoid data corruption.
- CRUD operations (need to use a
Createa new object.
Updatecan be achieved by simply
With this, you should be ready to use Realm in your games.
News & Announcements
Introducing Flexible Sync (Preview) – The Next Iteration of Realm Sync
Oct 19, 2022