MongoDB Developer
Realm
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
Realmchevron-right

Persistence in Unity Using Realm

Dominic Frei14 min read • Published Oct 14, 2021 • Updated Sep 07, 2022
UnityRealmSDKC#
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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 PlayerPrefs but 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.
The goal of this article is to show you how to add Realm to your Unity game and make sure your data is persisted. The Realm Unity SDK is part of our Realm .NET SDK. The documentation for the Realm .NET SDK will help you get started easily.
The first part of this tutorial will describe the example itself. If you are already familiar with Unity or really just want to see Realm in action, you can also skip it and jump straight to the second part.

Example game

We will be using a simple 3D chess game for demonstration purposes. Creating this game itself will not be part of this tutorial. However, this section will provide you with an overview so that you can follow along and add Realm to the game. This example can be found in our Unity examples repository.
The final implementation of the game including the usage of Realm is also part of the example repository.
To make it easy to find your way around this example, here are some notes to get you started:
The interesting part in the MainScene to look at is the Board which is made up of Squares and Pieces. The Squares are just slightly scaled and colored default Cube objects which we utilize to visualize the Board but also detect clicks for moving Pieces by using its already attached Box Collider component.
The Pieces have to be activated first, which happens by making them clickable as well. Pieces are not initially added to the Board but instead will be spawned by the PieceSpawner. You can find them in the Prefabs folder in the Project hierarchy.
The important part to look for here is the Piece script which detects clicks on this Piece (3) and offers a color change via Select() (1) and Deselect() (2) to visualize if a Piece is active or not.
We use two events to actually track the click on a Piece (1) or a Square (2):
The InputListener waits 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).
Clicking a Square while a Piece is selected will send a message (3) to the GameState to update the position of this Piece.
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 Piece positions and possibly destroy other Piece objects. Whenever we move a Piece (1), we not only update its position (2) but also need to check if there is a Piece in that position already (3) and if so, destroy it (4).
In addition to updating the game while it is running, the GameState offers 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.

Adding Realm to your project

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 WindowsPackage Manager → cogwheel in the top right corner → Advanced Project Settings:
Within the Scoped Registries, you can add the Name, URL, and Scope as follows:
This adds NPM as 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.json file which is located in the Packages folder of your project.
Here you need to add the following line to the dependencies:
Replace <version-number> with the most recent Realm version found in https://github.com/realm/realm-dotnet/releases and you're all set.
The final manifest.json should look something like this:
When you switch back to Unity, it will reload the dependencies. If you then open the Package Manager again, you should see Realm as a new entry in the list on the left:
We can now start using Realm in our Unity project.

Top-down or bottom-up?

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 Piece would know about its database object and whenever a Piece is 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.

Setting up the board

The first thing we want to do is to define a Realm representation of our piece since we cannot save the MonoBehaviour directly in Realm. Classes that are supposed to be saved in Realm need to subclass RealmObject. The class PieceEntity will represent such an object. Note that we cannot just duplicate the types from Piece since not all of them can be saved in Realm, like Vector3 and enum.
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 private.
Note that one of these backing variables is a RealmObject itself, or rather a subclass of it: EmbeddedObject (7). By extracting the position to a separate class Vector3Entity the PieceEntity is more readable. Another plus is that we can use the EmbeddedObject to represent a 1:1 relationship. Every PieceEntity can only have one Vector3Entity and even more importantly, every Vector3Entity can only belong to one PieceEntity because there can only ever be one Piece on any given Square.
The Vector3Entity, like the PieceEntity, has some convenience functionality like a constructor that takes a Vector3 (8), the ToVector3() function (9) and the private, mandatory default constructor (10) like PieceEntity.
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 PieceType and Position, we need to make sure those notifications are passed on. This is achieved by calling RaisePropertyChanged(nameof(Position)); whenever PositionEntity changes.
The next step is to add some way to actually add Pieces to the 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 GameObject for it (Piece) will be created. If a Piece gets moved, the PieceEntity will be updated by the GameState which then leads to the Piece's GameObject being 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 PieceSpawner as follows:
The important change here is CreateNewBoard. Instead of spawning the Pieces, we now add PieceEntity objects to the Realm. When we look at the changes in GameState, we will see how this actually creates a Piece per PieceEntity.
Here we just wipe the database (1) and then add new PieceEntity objects (2). Note that this is wrapped by a realm.write block. 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 GameState to make use of the new PieceSpawner and the PieceEntity that 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 Realm instance 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:
In 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 PieceEntity objects currently saved using realm.All (2) and assigning them to our pieceEntities field:
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 SubscribeForNotifications on a collection (3).
Apart from an error object that we need to check (4), we also receive the changes and 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 changes but gives us the opportunity to do some initial setup work (5).
In case we resume a game, we'll already see PieceEntity objects in the database even for the initial notification. We need to spawn one Piece per PieceEntity to represent it (6). We make use of the SpawnPiece function in PieceSpawner to 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 CreateNewBoard function we added earlier to the PieceSpawner.
On top of the initial notification, we also expect to receive a notification every time a PieceEntity is inserted into the Realm. This is where we continue the CreateNewBoard functionality we started in the PieceSpawner by 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 PieceEntity objects in the sender (which represents the pieceEntities collection) and add a Piece for each new PieceEntity to 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.

Updating the position of a PieceEntity

Whenever we receive a click on a Square and therefore call MovePiece in GameState, we need to update the PieceEntity instead of directly moving the corresponding GameObject. The movement of the Piece will then happen via the PropertyChanged notifications as we saw earlier.
Before actually moving the PieceEntity, we do need to check if there is already a PieceEntity at the desired position and if so, destroy it. To find a PieceEntity at the newPosition and also to find the PieceEntity that needs to be moved from oldPosition to newPosition, we can use queries on the pieceEntities collection (3).
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 RealmObject that represents the Piece we are looking for. Note that when using a Filter we can only filter using the Realm properties saved in the database, not the exposed properties (Position and PieceType) exposed for convenience by the PieceEntity.
If there is an attackedPiece at the target position, we need to delete the corresponding PieceEntity for this GameObject (1). After the attackedPiece is updated, we can then also update the movedPiece (2).
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.

Resetting the board

The final step will be to also update our ResetGame button to update (or rather, wipe) the Realm. At the moment, it does not update the state in the database and just recreates the GameObjects.
Resetting works similar to what we do in Awake in case there were no entries in the database—for example, when starting the game for the first time.
We can reuse the CreateNewBoard functionality 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 Realm to save the game's state.

Recap and conclusion

In this tutorial, we have seen that saving your game and resuming it later can be easily achieved by using Realm.
The steps we needed to take:
  • Add Realm via NPM as a dependency.
  • Import Realm in any class that wants to use it by calling using Realms;.
  • Create a new Realm instance via 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 using realm.Write() to avoid data corruption.
  • CRUD operations (need to use a write transaction):
    • Use realm.Add() to Create a new object.
    • Use realm.Remove() to Delete an object.
    • Read and Update can be achieved by simply getting and setting the public fields.
With this, you should be ready to use Realm in your games.
If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB and Realm.

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Article

Creating a Multiplayer Drawing Game with Phaser and MongoDB


Feb 03, 2023 | 15 min read
Code Example

Building Splash Screen Natively, Android 12, Kotlin


May 12, 2022 | 3 min read
Article

Realm Meetup - Realm JavaScript for React Native Applications


Mar 21, 2023 | 32 min read
Code Example

Build Offline-First Mobile Apps by Caching API Results in Realm


Mar 06, 2023 | 11 min read
Table of Contents