Turning Your Local Game into an Online Experience with MongoDB Realm Sync
Rate this tutorial
Playing a game locally can be fun at times. But there is nothing more exciting than playing with or against the whole world. Using Realm Sync you can easily synchronize data between multiple instances and turn your local game into an online experience.
If you have not used local Realms before we recommend working through the previous tutorial first so you can easily follow along here when we build on them.
We will use Atlas as our backend and cloud-based database. Realm Sync on the other side enables sync between your local Realm database and Atlas, seamlessly stitching together the two components into an application layer for your game. To support these services, MongoDB Realm also provides from which we will be using the feature to register and login the user.
The local Realm is based on four building blocks:
PieceEntityalong with the
Vector3Entityrepresents our model which include the two properties that make up a chess piece: type and position.
In the previous tutorial we have also added functionality to persist changes in position to the Realm and react to changes in the database that have to be reflected in the model. This was done by implementing
PieceSpawneris responsible for spawning new
Pieceobjects when the game starts via
public void CreateNewBoard(Realm realm). Here we can see some of the important functions that we need when working with Realm:
Write: Starts a new write transaction which is necessary to change the state of the database.
Add: Adds a new
RealmObjectto the database that has not been there before.
RemoveAll: Removes all objects of a specified type from the database.
All of this comes together in the central part of the game that manages the flow of it:
GameStateopen the Realm using
Awakeand offers an option to move pieces via
public void MovePiece(Vector3 oldPosition, Vector3 newPosition)which also checks if a
Piecealready exists at the target location. Furthermore we subscribe for notifications to set up the initial board. One of the things we will be doing in this tutorial is to expand on this subscription mechanic to also react to changes that come in through Realm Sync.
The first thing we need to change to get the local Realm example ready for Sync is to . This is a mandatory requirement for Sync to make sure objects can be distinguished from each other. We will be using the field
Idhere. Note that you can add a
MapToattribute in case the name of the field in the
RealmObjectdiffers from the name set in Atlas. By default the primary key is named
_idin Atlas which would conflict with the .NET coding guidelines. By adding
[MapTo("_id")]we can address this fact.
The local Realm tutorial showed you how to create a persisted game locally. While you could play with someone else using the same game client, there was only ever one game running at a time since every game is accessing the same table in the database and therefore the same objects.
This would still be the same when using Realm Sync if we do not separate those games. Everyone accessing the game from wherever they are would see the same state. We need a way to create multiple games and identify which one we are playing. Realm Sync offers a feature that let's us achieve exactly this: .
A partition represents a subset of the documents in a synced cluster that are related in some way and have the same read/write permissions for a given user. Realm directly maps partitions to individual synced .realm files so each object in a synced realm has a corresponding document in the partition.
What does this mean for our game? If we use one partition per match we can make sure that only players using the same partition will actually play the same game. Furthermore, we can start as many games as we want. Using the same partition simply means using the same
partiton keywhen opening a synced Realm. Partition keys are restricted to the following types:
For our game we will use a string that we ask the user for when they start the game. We will do this by adding a new scene to the game which also acts as a welcome and loading scene.
Assets -> Create -> Sceneto create a new scene and name it
WelcomeScene. Double click it to activate it.
GameObject -> UIwe then add
Buttonto the new scene. The input will be our partition key. To make it easier to understand for the player we will call its placeholder
game id. The
Textobject can be set to
Your Game ID:and the button's text to
Start Game. Make sure to reposition them to your liking.
Add a script to the button called
Add Componentin the Inspector with the start button selected. Then select
scriptand type in its name.
StartGameButtonknows two other game objects: the
gameIdInputField(1) that we created above and a
loadingIndicator(2) that we will be creating in a moment. If offers one action that will be executed when the button is clicked:
First, we want to show a loading indicator (4) in case loading the game takes a moment. Next we grab the
InputFieldand save it using the . Saving data using the
PlayerPrefsis acceptable if it is user input that does not need to be saved safely and only has a simple structure since
PlayerPrefscan only take a limited set of data types:
Next, we need to create a Realm (5). Note that this is done asynchrounously using
await. There are a couple of components necessary for opening a synced Realm:
app: An instance of
App(6) represents your Realm App that you created in Atlas. Therefore we need to pass the
app idin here.
user: If a user has been logged in before, we can access them by using
app.CurrentUser(7). In case there has not been a successful login before this variable will be null (8) and we need to register a new user.
The actual values for
passwordare not really relevant for this example. In your game you would use more
Input Fieldobjects to ask the user for this data. Here we can just use
Guidto generate random values. Using
EmailPasswordAuth.RegisterUserAsyncoffered by the
Appclass we can then register the user (9) and finally log them in (10) using these credentials. Note that we need to await this asynchrounous call again.
When we are done with the login, all we need to do is to create a new
gameId(which acts as our partition key) and the
userand save it as the
RealmConfiguration.DefaultConfiguration. This will make sure whenever we open a new Realm, we will be using this
Finally we want to open the Realm and synchronize it to get it ready for the game. We can detect if this is the first start of the game simply by checking if a Realm file for the given coonfiguration already exists or not (11). If there is no such file we open a Realm using
Realm.GetInstanceAsync()(12) which automatically uses the
DefaultConfigurationthat we set before.
When this is done, we can load the main scene (13) using the
SceneManager. Note that the name of the main scene was extracted into a file called
Constantsin which we also added the app id and the key we use to save the
game idin the
PlayerPrefs. You can either add another class in your IDE or in Unity (using
Assets -> Create -> C# Script).
One more thing we need to do is adding the main scene in the build settings, otherwise the
SceneManagerwill not be able to find it. Go to
File -> Build Settings ...and click
Add Open Sceneswhile the
With these adjustments we are ready to synchronize data. Let's add the loading indicator to improve the user experience before we start and test our game.
As mentioned before we want to add a loading indicator while the game is starting up. Don't worry, we will keep it simple since it is not the focus of this tutorial. We will just be using a simple
Imagewhich can both be found in the same
UIsub menu we used above.
The make sure things are a bit more organised, embed both of them into another
GameObject -> Create Empty.
You can arrange and style the UI elements to your liking and when you're done just add a script to the
The script itself should look like this:
The loading indicator that we will be using for this example is just a simple square moving sideways to indicate progress. There are two fields (1) we are going to expose to the Unity Editor by using
SerializeFieldso that you can adjust these values while seing the indicator move.
maxMovementwill tell the indicator how far to move to the left and right from the original position.
speed- as the name indicates - will determine how fast the indicator moves. The initial movement direction (2) is set to left, with
Vector3.Rightbeing the options given here.
The movement itself will be calculated in
Update()which is run every frame. We basically just want to do one of two things:
- Move the loading indicator to the left until it reaches the left boundary, then swap the movement direction.
- Move the loading indicator to the right until it reaches the right boundary, then swap the movement direction.
Using the component of the
GameObjectwe can move it by calling
Translate. The movement consists of the direction (
Vector3.right), the speed (set via the Unity Editor) and
Time.deltaTimewhich represents the time since the last frame. The latter makes sure we see a smooth movement no matter what the frame time is. After moving the square we check (3) if we have reached the boundary and if so, set the position to this boundary (4). This is just to make sure the indicator does not visibly slip out of bounds in case we see a low frame rate. Finally the position is swapped (5).
The loading indicator will only be shown when the start button is clicked. The script above takes care of showing it. We need to disable it so that it does not show up before. This can be done by clicking the checkbox next to the name of the
LoadingIndicatorparent object in the Inspector.
The scripts we have written above are finished but still need to be connected to the UI so that it can act on it.
First, let's assign the action to the button. With the
StartGameButtonselected in the
Inspectorand scroll down to the
On Click ()area. Click the plus icon in the lower right to add a new on click action.
Next, drag and drop the
Hierarchyonto the new action. This tells Unity which
GameObjectto use to look for actions that can be executed (which are functions that we implement like
Finally, we can choose the action that should be assigned to the
On Click ()event by opening the drop down. Choose the
We also need to connect the input field and the loading indicator to the
StartGameButtonscript so that it can access those. This is done via drag&drop again as before.
Now that the loading indicator is added the game is finished and we can start and run it. Go ahead and try it!
You will notice the experience when using one local Unity instance with Sync is the same as it was in the local Realm version. To actually test multiple game instances you can open the project on another computer. An easier way to test multiple Unity instances is . After following the installation instruction you will find a new menu item
ParallelSyncwhich offers a
Clones Manageryou add and open a new clone by clicking
Add new cloneand
Open in New Editor.
Using both instances you can then test the game and Realm Sync.
Remember that you need to use the same
partition keyto join the same game with both instances.
In this tutorial we have learned how to turn a game with a local Realm into a multiplayer experience using MongoDB Realm Sync. Let's summarise what needed to be done:
- Create an Atlas account and a Realm App therein
- Enable Sync, an authentication method and development mode
- Make sure every
- Choose a partition strategy (in this case: use the
partition keyto identify the match)
- Open a Realm using the
SyncConfiguration(which incorporates the