Sending and Requesting Data from MongoDB in a Unity Game
Rate this tutorial
Are you working on a game in Unity and finding yourself needing to make use of a database in the cloud? Storing your data locally works for a lot of games, but there are many gaming scenarios where you'd need to leverage an external database. Maybe you need to submit your high score for a leaderboard, or maybe you need to save your player stats and inventory so you can play on numerous devices. There are too many reasons to list as to why a remote database might make sense for your game.
If you've been keeping up with the content publishing on the MongoDB Developer Hub and our Twitch channel, you'll know that I'm working on a game development series with Adrienne Tacke. This series is centered around creating a 2D multiplayer game with Unity that uses MongoDB as part of the online component. Up until now, we haven't actually had the game communicate with MongoDB.
In this tutorial, we're going to see how to make HTTP requests from a Unity game to a back end that communicates with MongoDB. The back end was already developed in a tutorial titled, Creating a User Profile Store for a Game With Node.js and MongoDB. We're now going to leverage it in our game.
To get an idea where we're at in the tutorial series, take a look at the animated image below:
To take this to the next level, it makes sense to send data to MongoDB when the player crosses the finish line. For example, we can send how many steps were taken by the player in order to reach the finish line, or how many times the player collided with something, or even what place the player ranked in upon completion. The data being sent doesn't truly matter as of now.
The assumption is that you've been following along with the tutorial series and are jumping in where we left off. If not, some of the steps that refer to our project may not make sense, but the concepts can be applied in your own game. The tutorials in this series so far are:
Because Unity, as of now, doesn't have an official MongoDB driver, sending and receiving MongoDB data from Unity isn't handled for you. We're going to have to worry about marshalling and unmarshalling our data as well as making the request. In other words, we're going to need to manipulate our data manually to and from JSON and C# classes.
To make this possible, we're going to need to start with a class that represents our data model in MongoDB.
Within your project's Assets/Scripts directory, create a PlayerData.cs file with the following code:
1 using UnityEngine; 2 3 public class PlayerData 4 { 5 public string plummie_tag; 6 public int collisions; 7 public int steps; 8 }
Notice that this class does not extend the
MonoBehavior
class. This is because we do not plan to attach this script as a component on a game object. The public
-defined properties in the PlayerData
class represent each of our database fields. In the above example, we only have a select few, but you could add everything from our user profile store if you wanted to.It is important to use the
public
identifier for anything that will have relevance to the database.We need to make a few more changes to the
PlayerData
class. Add the following functions to the class:1 using UnityEngine; 2 3 public class PlayerData 4 { 5 // 'public' variables here ... 6 7 public string Stringify() 8 { 9 return JsonUtility.ToJson(this); 10 } 11 12 public static PlayerData Parse(string json) 13 { 14 return JsonUtility.FromJson<PlayerData>(json); 15 } 16 }
Notice the function names are kind of like what you'd find in JavaScript if you are a JavaScript developer. Unity expects us to send string data in our requests rather than objects. The good news is that Unity also provides a helper
JsonUtility
class that will convert objects to strings and strings to objects.The
Stringify
function will take all public
variables in the class and convert them to a JSON string. The fields in the JSON object will match the names of the variables in the class. The Parse
function will take a JSON string and convert it back into an object that can be used within C#.With a class available to represent our data model, we can now send data to MongoDB as well as retrieve it. Unity provides a UnityWebRequest class for making HTTP requests within a game. This will be used to communicate with a back end designed with a particular programming language.
We're going to spend the rest of our time in the project's Assets/Scripts/Player.cs file. This script is attached to our player as a component and was created in the tutorial titled, Getting Started with Unity for Creating a 2D Game. In your own game, it doesn't really matter which game object script you use.
Open the Assets/Scripts/Player.cs file and make sure it looks similar to the following:
1 using UnityEngine; 2 using System.Text; 3 using UnityEngine.Networking; 4 using System.Collections; 5 6 public class Player : MonoBehaviour 7 { 8 public float speed = 1.5f; 9 10 private Rigidbody2D _rigidBody2D; 11 private Vector2 _movement; 12 13 void Start() 14 { 15 _rigidBody2D = GetComponent<Rigidbody2D>(); 16 } 17 18 void Update() 19 { 20 // Mouse and keyboard input logic here ... 21 } 22 23 void FixedUpdate() { 24 // Physics related updates here ... 25 } 26 }
I've stripped out a bunch of code from the previous tutorial as it doesn't affect anything we're planning on doing. The previous code was very heavily related to moving the player around on the screen and should be left in for the real game, but is overlooked in this example, at least for now.
Two things to notice that are important are the imports:
1 using System.Text; 2 using UnityEngine.Networking;
The above two imports are important for the networking features of Unity. Without them, we wouldn't be able to properly make GET and POST requests.
Before we make a request, let's get our
PlayerData
class included. Make the following changes to the Assets/Scripts/Player.cs code:1 using UnityEngine; 2 using System.Text; 3 using UnityEngine.Networking; 4 using System.Collections; 5 6 public class Player : MonoBehaviour 7 { 8 public float speed = 1.5f; 9 10 private Rigidbody2D _rigidBody2D; 11 private Vector2 _movement; 12 private PlayerData _playerData; 13 14 void Start() 15 { 16 _rigidBody2D = GetComponent<Rigidbody2D>(); 17 _playerData = new PlayerData(); 18 _playerData.plummie_tag = "nraboy"; 19 } 20 21 void Update() { } 22 23 void FixedUpdate() { } 24 25 void OnCollisionEnter2D(Collision2D collision) 26 { 27 _playerData.collisions++; 28 } 29 }
In the above code, notice that we are creating a new
PlayerData
object and assigning the plummie_tag
field a value. We're also making use of an OnCollisionEnter2D
function to see if our game object collides with anything. Since our function is very vanilla, collisions can be with walls, objects, etc., and nothing in particular. The collisions will increase the collisions
counter.So, we have data to work with, data that we need to send to MongoDB. To do this, we need to create some
IEnumerator
functions and make use of coroutine calls within Unity. This will allow us to do asynchronous activities such as make web requests.Within the Assets/Scripts/Player.cs file, add the following
IEnumerator
function:1 IEnumerator Download(string id, System.Action<PlayerData> callback = null) 2 { 3 using (UnityWebRequest request = UnityWebRequest.Get("http://localhost:3000/plummies/" + id)) 4 { 5 yield return request.SendWebRequest(); 6 7 if (request.isNetworkError || request.isHttpError) 8 { 9 Debug.Log(request.error); 10 if (callback != null) 11 { 12 callback.Invoke(null); 13 } 14 } 15 else 16 { 17 if (callback != null) 18 { 19 callback.Invoke(PlayerData.Parse(request.downloadHandler.text)); 20 } 21 } 22 } 23 }
The
Download
function will be responsible for retrieving data from our database to be brought into the Unity game. It is expecting an id
which we'll use a plummie_id
for and a callback
so we can work with the response outside of the function. The response should be PlayerData
which is that of the data model we just made.After sending the request, we check to see if there were errors or if it succeeded. If the request succeeded, we can convert the JSON string into an object and invoke the callback so that the parent can work with the result.
Sending data with a payload, like that in a POST request, is a bit different. Take the following function:
1 IEnumerator Upload(string profile, System.Action<bool> callback = null) 2 { 3 using (UnityWebRequest request = new UnityWebRequest("http://localhost:3000/plummies", "POST")) 4 { 5 request.SetRequestHeader("Content-Type", "application/json"); 6 byte[] bodyRaw = Encoding.UTF8.GetBytes(profile); 7 request.uploadHandler = new UploadHandlerRaw(bodyRaw); 8 request.downloadHandler = new DownloadHandlerBuffer(); 9 yield return request.SendWebRequest(); 10 11 if (request.isNetworkError || request.isHttpError) 12 { 13 Debug.Log(request.error); 14 if(callback != null) 15 { 16 callback.Invoke(false); 17 } 18 } 19 else 20 { 21 if(callback != null) 22 { 23 callback.Invoke(request.downloadHandler.text != "{}"); 24 } 25 } 26 } 27 }
In the
Upload
function, we are expecting a JSON string of our profile. This profile was defined in the PlayerData
class and it is the same data we received in the Download
function.The difference between these two functions is that the POST is sending a payload. For this to work, the JSON string needs to be converted to
byte[]
and the upload and download handlers need to be defined. Once this is done, it is business as usual.It is up to you what you want to return back to the parent. Because we are creating data, I thought it'd be fine to just return
true
if successful and false
if not. To demonstrate this, if there are no errors, the response is compared against an empty object string. If an empty object comes back, then false. Otherwise, true. This probably isn't the best way to respond after a creation, but that is up to the creator (you, the developer) to decide.The functions are created. Now, we need to use them.
Let's make a change to the
Start
function:1 void Start() 2 { 3 _rigidBody2D = GetComponent<Rigidbody2D>(); 4 _playerData = new PlayerData(); 5 _playerData.plummie_tag = "nraboy"; 6 StartCoroutine(Download(_playerData.plummie_tag, result => { 7 Debug.Log(result); 8 })); 9 }
When the script runs—or in our, example when the game runs—and the player enters the scene, the
StartCoroutine
method is executed. We are providing the plummie_tag
as our lookup value and we are printing out the results that come back.We might want the
Upload
function to behave a little differently. Instead of making the request immediately, maybe we want to make the request when the player crosses the finish line. For this, maybe we add some logic to the FixedUpdate
method instead:1 void FixedUpdate() 2 { 3 // Movement logic here ... 4 5 if(_rigidBody2D.position.x > 24.0f) { 6 StartCoroutine(Upload(_playerData.Stringify(), result => { 7 Debug.Log(result); 8 })); 9 } 10 }
In the above code, we check to see if the player position is beyond a certain value in the x-axis. If this is true, we execute the
Upload
function and print the results.The above example isn't without issues though. As of now, if we cross the finish line, we're going to experience many requests as our code will continuously execute. We can correct this by adding a boolean variable into the mix.
At the top of your Assets/Scripts/Player.cs file with the rest of your variable declarations, add the following:
1 private bool _isGameOver;
The idea is that when the
_isGameOver
variable is true, we shouldn't be executing certain logic such as the web requests. We are going to initialize the variable as false in the Start
method like so:1 void Start() 2 { 3 // Previous code here ... 4 _isGameOver = false; 5 }
With the variable initialized, we can make use of it prior to sending an HTTP request after crossing the finish line. To do this, we'd make a slight adjustment to the code like so:
1 void FixedUpdate() 2 { 3 // Movement logic here ... 4 5 if(_rigidBody2D.position.x > 24.0f && _isGameOver == false) { 6 StartCoroutine(Upload(_playerData.Stringify(), result => { 7 Debug.Log(result); 8 })); 9 _isGameOver = true; 10 } 11 }
After the player crosses the finish line, the HTTP code is executed and the game is marked as game over for the player, preventing further requests.
You just saw how to use the
UnityWebRequest
class in Unity to make HTTP requests from a game to a remote web server that communicates with MongoDB. This is valuable for any game that needs to either store game information remotely or retrieve it.There are plenty of other ways to make use of the
UnityWebRequest
class, even in our own player script, but the examples we used should be a great starting point.This tutorial series is part of a series streamed on Twitch. To see these streams live as they happen, follow the Twitch channel and tune in.