Did you know that MongoDB has a Realm SDK for the Unity game development framework that makes working with game data effortless? It’s currently an alpha release, but you can already start using it to build persistence into your cross platform gaming projects.
A popular game template for the past few years has been in infinite runner style games. Games such as Temple Run and Subway Surfers have had many competitors, each with their own spin on the subject. If you’re unfamiliar with the infinite runner concept, the idea is that you have a player that can move horizontally to fixed positions. As the game progresses, obstacles and rewards enter the scene. The player must dodge or obtain depending on the object and this happens until the player collides with an obstacle. As time progresses, the game generally speeds up to make things more difficult.
While the game might sound complicated, there’s actually a lot of repetition.
In this tutorial, we’ll look at how to create our own infinite runner game with Unity and C#. We’ll look at important concepts such as object pooling and collision, as well as data persistence using the Realm SDK for Unity.
To get an idea of what we want to build, check out the following animated image:
As you can see in the above image, we have simple shapes as well as cake. The score increases as time increases or when cake is obtained. The level restarts when you collide with an obstacle and depending on what your score was, it could now be the new high score.
There are a few requirements, some of which will change once the Realm SDK for Unity becomes a stable release.
This tutorial might work with earlier versions of Unity. However, 2020.2.4f1 is the version that I’m using. As of right now, the Realm SDK for Unity is only available as a tarball through GitHub rather than through the Unity Asset Store. For now, you’ll have to dig through the releases on GitHub.
Even though there are a lot of visual components moving around on the screen, there’s not a lot happening behind the scenes. There are three core visual objects that make up this game example.
We have the player, the obstacles, and the rewards, which we’re going to interchangeably call cake. Each game object will have the same components, but different scripts. We’ll add the components here, but create the scripts later.
Within your project, create the three different game objects. To start, each game object will be empty.
Rather than working with all kinds of fancy graphics, create a 1x1 pixel image that is white. We’re going to use it for all of our game objects, just giving them a different color or size.
Each game object should have a Sprite Renderer, Rigidbody 2D, and a Box Collider 2D component attached. The Sprite Render can use the 1x1 pixel graphic or one of your choosing. For the Rigidbody 2D, make sure the Body Type is Kinematic on all game objects because we won’t be using things like gravity. Likewise, make sure the Is Trigger is enabled for each of the Box Collider 2D components.
We’ll be adding more as we go along, but for now, we have a starting point.
There are a million different ways to create a great game with Unity. However, for this game, we’re going to not rely on any particular visually rendered object for managing the game itself. Instead, we’re going to create a game object responsible for game management.
Add an empty game object to your scene titled GameController. While we won’t be doing anything with it now, we’ll be attaching scripts to it for managing the object pool and the score.
With the three core game objects (player, obstacle, reward) in the scene, we need to give each of them some game logic. Let’s start with the logic for the obstacle and reward since they are similar.
The idea behind the obstacle and reward is that they are constantly moving down from the top of the screen. As they become visible, the position along the x-axis is randomized. As they fall off the screen, the object is disabled and eventually reset.
Create an Obstacle.cs file with the following C# code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Obstacle : MonoBehaviour {
public float movementSpeed;
private float[] _fixedPositionX = new float[] { -8.0f, 0.0f, 8.0f };
void OnEnable() {
int randomPositionX = Random.Range(0, 3);
transform.position = new Vector3(_fixedPositionX[randomPositionX], 6.0f, 0);
}
void Update() {
transform.position += Vector3.down * movementSpeed * Time.deltaTime;
if(transform.position.y < -5.25) {
gameObject.SetActive(false);
}
}
}
In the above code, we have fixed position possibilities. When the game object is enabled, we randomly choose from one of the possible fixed positions and update the overall position of the game object.
For every frame of the game, the position of the game object falls down on the y-axis. If the object reaches a certain position, it is then disabled.
Similarly, create a Cake.cs file with the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Cake : MonoBehaviour {
public float movementSpeed;
private float[] _fixedPositionX = new float[] { -8.0f, 0.0f, 8.0f };
void OnEnable() {
int randomPositionX = Random.Range(0, 3);
transform.position = new Vector3(_fixedPositionX[randomPositionX], 6.0f, 0);
}
void Update() {
transform.position += Vector3.down * movementSpeed * Time.deltaTime;
if (transform.position.y < -5.25) {
gameObject.SetActive(false);
}
}
void OnTriggerEnter2D(Collider2D collider) {
if (collider.gameObject.tag == "Player") {
gameObject.SetActive(false);
}
}
}
The above code should look the same with the exception of the OnTriggerEnter2D
function. In the OnTriggerEnter2D
function, we have the following code:
void OnTriggerEnter2D(Collider2D collider) {
if (collider.gameObject.tag == "Player") {
gameObject.SetActive(false);
}
}
If the current reward game object collides with another game object and that other game object is tagged as being a “Player”, then the reward object is disabled. We’ll handle the score keeping of the consumed reward elsewhere.
Make sure to attach the Obstacle
and Cake
scripts to the appropriate game objects within your scene.
With the obstacles and rewards out of the way, let’s look at the logic for the player. Create a Player.cs file with the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Player : MonoBehaviour {
public float movementSpeed;
void Update() {
if(Input.GetKey(KeyCode.LeftArrow)) {
transform.position += Vector3.left * movementSpeed * Time.deltaTime;
} else if(Input.GetKey(KeyCode.RightArrow)) {
transform.position += Vector3.right * movementSpeed * Time.deltaTime;
}
}
void OnTriggerEnter2D(Collider2D collider) {
if(collider.gameObject.tag == "Obstacle") {
// Handle Score Here
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
} else if(collider.gameObject.tag == "Cake") {
// Handle Score Here
}
}
}
The Player.cs file will change in the future, but for now, we can move the player around based on the arrow keys on the keyboard. We are also looking at collisions with other objects. If the player object collides with an object tagged as being an obstacle, then the goal is to change the score and restart the scene. Otherwise, if the player object collides with an object tagged as being “Cake”, which is a reward, then the goal is to just change the score.
Make sure to attach the Player
script to the appropriate game object within your scene.
As it stands, when an obstacle falls off the screen, it becomes disabled. As a reward is collided with or as it falls off the screen, it becomes disabled. In an infinite runner, we need those obstacles and rewards to be constantly resetting to look infinite. While we could just destroy and instantiate as needed, that is a performance-heavy task. Instead, we should make use of an object pool.
The idea behind an object pool is that you instantiate objects when the game starts. The number you instantiate is up to you. Then, while the game is being played, objects are pulled from the pool if they are available and when they are done, they are added back to the pool. Remember the enabling and disabling of our objects in the obstacle and reward scripts? That has to do with pooling.
Ages ago, I had written a tutorial around object pooling, but we’ll explore it here as a refresher. Create an ObjectPool.cs file with the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public static ObjectPool SharedInstance;
private List<GameObject> pooledObstacles;
private List<GameObject> pooledCake;
public GameObject obstacleToPool;
public GameObject cakeToPool;
public int amountToPool;
void Awake() {
SharedInstance = this;
}
void Start() {
pooledObstacles = new List<GameObject>();
pooledCake = new List<GameObject>();
GameObject tmpObstacle;
GameObject tmpCake;
for(int i = 0; i < amountToPool; i++) {
tmpObstacle = Instantiate(obstacleToPool);
tmpObstacle.SetActive(false);
pooledObstacles.Add(tmpObstacle);
tmpCake = Instantiate(cakeToPool);
tmpCake.SetActive(false);
pooledCake.Add(tmpCake);
}
}
public GameObject GetPooledObstacle() {
for(int i = 0; i < amountToPool; i++) {
if(pooledObstacles[i].activeInHierarchy == false) {
return pooledObstacles[i];
}
}
return null;
}
public GameObject GetPooledCake() {
for(int i = 0; i < amountToPool; i++) {
if(pooledCake[i].activeInHierarchy == false) {
return pooledCake[i];
}
}
return null;
}
}
If the code looks a little familiar, a lot of it was taken from the Unity educational resources, particularly Introduction to Object Pooling.
The ObjectPool
class is meant to be a singleton instance, meaning that we want to use the same pool regardless of where we are and not accidentally create numerous pools. We start by initializing each pool, which in our example is a pool of obstacles and a pool of rewards. For each object in the pool, we initialize them as disabled. The instantiation of our objects will be done with prefabs, but we’ll get to that soon.
With the pool initialized, we can make use of the GetPooledObstacle
or GetPooledCake
methods to pull from the pool. Remember, items in the pool should be disabled. Otherwise, they are considered to be in use. We loop through our pools to find the first object that is disabled and if none exist, then we return null.
Alright, so we have object pooling logic and need to fill the pool. This is where the object prefabs come in.
As of right now, you should have an Obstacle game object and a Cake game object in your scene. These game objects should have various physics and collision-related components attached, as well as the logic scripts. Create a Prefabs directory within your Assets directory and then drag each of the two game objects into that directory. Doing this will convert them from a game object in the scene to a reusable prefab.
With the prefabs in your Prefabs directory, delete the obstacle and reward game objects from your scene. We’re going to add them to the scene via our object pooling script, not through the Unity UI.
You should have the ObjectPool
script completed. Make sure you attach this script to the GameController game object. Then, drag each of your prefabs into the public variables of that script in the inspector for the GameController game object.
Just like that, your prefabs will be pooled at the start of your game. However, just because we are pooling them doesn’t mean we are using them. We need to create another script to take objects from the pool.
Create a GameController.cs file and include the following C# code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameController : MonoBehaviour {
public float obstacleTimer = 2;
public float timeUntilObstacle = 1;
public float cakeTimer = 1;
public float timeUntilCake = 1;
void Update() {
timeUntilObstacle -= Time.deltaTime;
timeUntilCake -= Time.deltaTime;
if(timeUntilObstacle <= 0) {
GameObject obstacle = ObjectPool.SharedInstance.GetPooledObstacle();
if(obstacle != null) {
obstacle.SetActive(true);
}
timeUntilObstacle = obstacleTimer;
}
if(timeUntilCake <= 0) {
GameObject cake = ObjectPool.SharedInstance.GetPooledCake();
if(cake != null) {
cake.SetActive(true);
}
timeUntilCake = cakeTimer;
}
}
}
In the above code, we are making use of a few timers. We’re creating timers to determine how frequently an object should be taken from the object pool.
When the timer indicates we are ready to take from the pool, we use the GetPooledObstacle
or GetPooledCake
methods, set the object taken as enabled, and then reset the timer. Each instantiated prefab has the logic script attached, so once the object is enabled, it will start falling from the top of the screen.
To activate this script, make sure to attach it to the GameController game object within the scene.
If you ran the game as of right now, you’d be able to move your player around and collide with obstacles or rewards that continuously fall from the top of the screen. There’s no concept of score-keeping or data persistence in the game up until this point.
Including Realm in the game can be broken into two parts. For now, it is three parts due to needing to manually add the dependency to your project, but two parts will be evergreen.
From the Realm .NET releases, find the latest release that includes Unity. For this tutorial, I’m using the realm.unity.bundle-10.1.1.tgz file.
In Unity, click Window -> Package Manager and choose to Add package from tarball…, then find the Realm SDK that you had just downloaded.
It may take a few minutes to import the SDK, but once it’s done, we can start using it.
Before we start adding code, we need to be able to display our score information to the user. In your Unity scene, add three Text game objects: one for the high score, one for the current score, and one for the amount of cake or rewards obtained. We’ll be using these game objects soon.
Let’s create a PlayerStats.cs file and add the following C# code:
using Realms;
public class PlayerStats : RealmObject {
[PrimaryKey]
public string Username { get; set; }
public RealmInteger<int> Score { get; set; }
public PlayerStats() {}
public PlayerStats(string Username, int Score) {
this.Username = Username;
this.Score = Score;
}
}
The above code represents an object within our Realm data store. For our example, we want the high score for any given player to be in our Realm. While we won’t have multiple users in our example, the foundation is there.
To use the above RealmObject
, we’ll want to create another script. Create a Score.cs file and add the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Realms;
public class Score : MonoBehaviour {
private Realm _realm;
private PlayerStats _playerStats;
private int _cake;
public Text highScoreText;
public Text currentScoreText;
public Text cakeText;
void Start() {
_realm = Realm.GetInstance();
_playerStats = _realm.Find<PlayerStats>("nraboy");
if(_playerStats is null) {
_realm.Write(() => {
_playerStats = _realm.Add(new PlayerStats("nraboy", 0));
});
}
highScoreText.text = "HIGH SCORE: " + _playerStats.Score.ToString();
_cake = 0;
}
void OnDisable() {
_realm.Dispose();
}
void Update() {
currentScoreText.text = "SCORE: " + (Mathf.Floor(Time.timeSinceLevelLoad) + _cake).ToString();
cakeText.text = "CAKE: " + _cake;
}
public void CalculateHighScore() {
int snapshotScore = (int)Mathf.Floor(Time.timeSinceLevelLoad) + _cake;
if(_playerStats.Score < snapshotScore) {
_realm.Write(() => {
_playerStats.Score = snapshotScore;
});
}
}
public void AddCakeToScore() {
_cake++;
}
}
In the above code, when the Start
method is called, we get the Realm instance and do a find for a particular user. If the user doesn’t exist, we create a new one, at which point we can use our Realm like any other object in our application.
When we decide to call the CalculateHighScore
method, we do a check to see if the new score should be saved. In this example, we are using the rewards as a multiplier to the score.
If you’ve never used Realm before, the Realm SDK for Unity uses the same API as the .NET SDK. You can learn more about how to use it in the getting started guide. You can also swing by the community to get additional help.
So, we have the Score
class. This script should be attached to the GameController game object and each of the Text game objects should be dragged into the appropriate areas using the inspector.
We’re not done yet. Remember, our Player.cs file needed to update the score. Before we open our class, make sure to drag the GameController into the appropriate area of the Player game object using the Unity inspector.
Open the Player.cs file and add the following to the OnTriggerEnter2D
method:
void OnTriggerEnter2D(Collider2D collider) {
if(collider.gameObject.tag == "Obstacle") {
score.CalculateHighScore();
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
} else if(collider.gameObject.tag == "Cake") {
score.AddCakeToScore();
}
}
When running the game, not only will we have something playable, but the score should update and persist depending on if we’ve failed at the level or not.
The above image is a reminder of what we’ve built, minus the graphic for the cake.
You just saw how to create an infinite runner type game with Unity and C# that uses the MongoDB Realm SDK for Unity when it comes to data persistence. Like previously mentioned, the Realm SDK is currently an alpha release, so it isn’t a flawless experience and there are features missing. However, it works great for an example like we saw here.
If you’re interested in checking out this project, it can be found on GitHub. There’s also a video version of this tutorial, an on-demand live-stream, which can be found below.
As a fun fact, this infinite runner example wasn’t my first attempt at one. I built something similar a long time ago and it was quite fun.
This content first appeared on MongoDB.