Connecting a Unity Game to the Game Server

How to connect to your game server to communicate actions that affect asset properties

Previously in the tutorial series, we created a game server that received an 'action' from a dummy javascript client. In this tutorial, we will continue from where we left off having created an Ethereum compatible web3 account in a Unity project, and use this to connect to our server.

Requirements

  • Basic knowledge of game development in Unity.

  • A development machine with the latest version of Unity installed

  • Having followed and completed two previous tutorials, Web3 Wallets in Unity, and Basic Game/Server Authentication, or downloaded the example project from the repositories for those tutorials.

Setup

Either create a new Unity project or continue from where you left off with the Web3 Wallets in Unity tutorial (this is not necessary as we will not be using the Web3 wallet just yet).

Make sure the Game Server is running.

Now, create a new empty GameObject called ServerManager in the scene.

Create a new C# script called ServerManager, and add it as a component of the ServerManager GameObject.

Connecting Unity to the server

We will use the UnityWebRequest POST functionality to send a message to the server.

Let's modify the example from the Unity documentation to make a test connection to the server. Now would also be a good time to add all of the dependencies that we will need:

// ServerManager.cs
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;
using UnityEngine;
using System.IO;
using System.Security.Cryptography;

public class ServerManager : MonoBehaviour
{
    public void Start()
    {
        StartCoroutine(Upload());
    }


    IEnumerator Upload()
    {
        using (UnityWebRequest www = UnityWebRequest.Post(
            "http://localhost:3000/evolve/", 
            "{ \"this data\": 1, \"will fail\": 2 }", 
            "application/json")
        )
        {
            yield return www.SendWebRequest();
            if (www.result != UnityWebRequest.Result.Success)
            {
                print(www.error);
            }
            else
            {
                var data = www.downloadHandler.text;
                print(data);
            }
        }
    }
}

Save the scene. You can hit the play button if you like, but unless you added some error handling to the server built in the previous tutorial, the server will crash. This is because we are sending junk data.

Encrypting a message correctly

In the previous tutorial, we created RSA keys for our game server. We can embed the public key string into the code of the Unity binary very easily, but parsing this text format correctly, in order to use the default RSA Encryption classes of .NET requires a bit more work.

Fortunately, there is an active open source community in the cryptography space, and we can use the Bouncy Castle package for C# (MIT license) to do much of the heavy lifting. Install Bouncy Castle in your Unity project via NuGet, alternatively you can download the package directly from the NuGet gallery (this tutorial uses release 2.2.1), then unzip the package (rename the extension fo the package file to .zip if necessary), and drag and drop the lib/netstandard2.0/BouncyCastle-Cryptography.dll file to the Unity Assets folder.

a Let's now modify our ServerManager class to add two utility functions to convert byte arrays to hex strings and vice-versa (with later versions of .NET, these functions are not required, but as we are using Standard 2.1, we must implement them). Let's also add a string with the server public key, the data we will send to the server, and the Bouncy Castle dependencies that we will be using:

// ServerManager.cs
// ... existing imports
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;

public class ServerManager : MonoBehaviour
{
    private string SERVER_PUBLIC_KEY = @"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArTtVgx+9+sQTt3qyTeGX
<snip many lines>
xwIDAQAB
-----END PUBLIC KEY-----";
    private string USER_EMAIL = "user@server.com";
    private string SALT = "livingassets";
    private string ACTION_CODE = "XYZ500";

    // utility function to convert a byte array to a hex string
    public static string ByteArrayToString(byte[] ba)
    {
        return BitConverter.ToString(ba).Replace("-","");
    }

    // Utility function to convert a hex string to a byte array
    public static byte[] StringToByteArray(String hex)
    {
        int NumberChars = hex.Length;
        byte[] bytes = new byte[NumberChars / 2];
        for (int i = 0; i < NumberChars; i += 2)
            bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
        return bytes;
    }
    
    // ... existing code

Now we can create a single new function which will parse the public key, import its parameters into an RSACryptoServiceProvider instance, and encrypt some given data.

// ServerManager.cs
  
    // private function of ServerManager class
    string EncryptData(string toEncrypt) 
    {
        // Use Bouncy Castle libraries to parse the public key
        PemReader pr = new PemReader(new StringReader(SERVER_PUBLIC_KEY));
        AsymmetricKeyParameter publicKey = (AsymmetricKeyParameter)pr.ReadObject();
        RSAParameters rsaParams = DotNetUtilities.ToRSAParameters((RsaKeyParameters)publicKey);

        // Create new RSA provider and import key parameters
        RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();
        RSA.ImportParameters(rsaParams);

        // package our data and encrypt it
        byte[] dataToEncrypt = new UTF8Encoding().GetBytes(toEncrypt);
        byte[] encryptedData = RSA.Encrypt(dataToEncrypt, false);
        string result = ByteArrayToString(encryptedData);
        return result;
    }

This function will return our data as a hex string, in the exact format that our server is expecting. So all we need to do is to assemble the JSON string with the encrypted data, and POST it to the server.

Modify the Start function to the following:

void Start() {
    // hash the email and salt
    byte[] userBytes = new UTF8Encoding().GetBytes(USER_EMAIL + SALT);
    var hashedUserBytes = new MD5CryptoServiceProvider().ComputeHash(userBytes);
    string hashedUser = ByteArrayToString(hashedUserBytes).ToLower();
    
    // assemble the JSON string to encrypt, in the correct format
    var messageJson = $@"{{""user"":""{hashedUser}"", ""action"":""{ACTION_CODE}""}}";
    // encrypt
    var encryptedMessage = EncryptData(messageJson);
    // assemble the final JSON to send to the server
    var finalJSON = $@"{{""message"":""{encryptedMessage}""}}";
    
    StartCoroutine(Upload(finalJSON));
}

You'll notice that our Upload coroutine is now accepting a parameter, which is the JSON string. So let's update the coroutine to accept that string and send it in the POST request.

IEnumerator Upload(string json)
{
    using (UnityWebRequest www = UnityWebRequest.Post(
        "http://localhost:3000/evolve/", 
        json, // THIS LINE IS CHANGED TO ACCEPT THE PARAMETER
        "application/json")
    )
// ... existing code

Now hit play! (Make sure the server is running!)

If all goes well, check the Unity console to see confirmation from the server that the asset has been modified:

Congratulations! You now have connected your game to the server, and can send actions that will trigger a connection to the Living Assets API.

Last updated

freeverse.io