Fun with WebSockets - The server layer

For our WebSockets example, we're going to use Unity as the main logic behind our game, but also to simply show what's happening in our game - like this panel of lights, although ours won't be as pretty.

In previous blogs, we've covered building a WebSocket connection layer using PHP's Ratchet, and building a client for it using HTML and JavaScript. Now we're going to cover building the server in Unity.

As with the rest of the parts of this example, the code's designed for demoing the concept on a blog, and for game jams. This means there'll be security holes, errors, and probably a tonne of bad practice. Although, to be fair, that's the same as the rest of these blogs anyway, and most examples online in general. This is only really one step above Stack Overflow.

This blog then, will walk through how we communicate with the connection layer from Unity over WebSockets and how we can show something on-screen and pass any changes back to the connection layer.

A little help

We're going to use a couple of little helpers to get our code going. One to handle the actual WebSocket connections, and one to handle converting to and from JSON, as it's a great way to pass data around without too much hassle.

The first piece of code is from github.com/sta/websocket-sharp and, like WebSocket() in JavaScript, will allow us to handle connecting, and to cope with interpreting messages.

The second is one i really recommend as I end up using it in pretty much all of my C# projects and is from github.com/JamesNK/Newtonsoft.Json and gives us extra methods like JsonConvert.SerializeObject() and JsonConvert.DeserializeObject<>().

Either download or build your own *.dll files from these two repositories and drop them anywhere in your Assets directory in your Unity project, or take them from the github link to this test project at the bottom of the page.

WebSocketServer

The way I'm going to be using the WebSocket code is to run it in a Coroutine that never ends. This is because if the thread it's running in ends, the connection will end to.

Running code in a Coroutine introduces it's own issues with Unity, as you lose access to a lot of functionality like creating random numbers. To get around this, we're going to split our code into two classes. One will take care of WebSocket communication, and the other with managing how the clients will move.

Back to the Coroutine then, the following shows how we connect to the server, and the loop we use to keep the connection alive. This passes any movement data for the clients back to the connection layer every 1/20 of a second, or every 50ms. Play around with this to see what works well for you.

public ClientManager clients;
private WebSocket webSocket;

public void Start()
{
    StartCoroutine(ConnectToWebSocket());
}
    
private IEnumerator ConnectToWebSocket()
{
    webSocket = new WebSocket("ws://localhost:100");
    webSocket.OnMessage += (sender, e) => InterpretMessage(e.Data);

    webSocket.Connect();
        
    webSocket.Send(JsonConvert.SerializeObject(
        new Dictionary<string, object> { { "connect", "server" } }
    ));

    while (true) {
        webSocket.Send(JsonConvert.SerializeObject(
            new Dictionary<string, object> { { "data", new Dictionary<string, object> { { "clients", clients.GetClients() } } } }
        ));

        yield return new WaitForSeconds(0.05f);
    }
}

The above code is assuming your connection layer is local, but if it's hosted in AWS already then remember to change the url above.

After connecting to the connection layer, we register this application as a server, and then start sending back updates. Any incoming messages are handled by the following code:

private void InterpretMessage(string data)
{
    Debug.Log(data);

    Dictionary<string, object> message = JsonConvert.DeserializeObject<Dictionary<string, object>>(data);

    int clientId = Convert.ToInt32(message["client_id"].ToString());
    clients.AddClient(clientId);

    if (message.ContainsKey("client_data")) {
        Dictionary<string, object> clientData = JsonConvert.DeserializeObject<Dictionary<string, object>>(message["client_data"].ToString());
        clients.UpdateClient(clientId, float.Parse(clientData["x"].ToString()), float.Parse(clientData["y"].ToString()));
    }
}

As we're only expecting to input telling us where clients would like to move to, this only parses client data that it's expecting to contain an x and y coordinate.

All updates in this method are done via the ClientManager so we can separate our code nicely and not have any issues with Coroutines.

ClientManager

The ClientManager is not only the store for our clients, but also handles moving them around the map towards their targets. To accomplish this, we store positional data for where they are now, and where they want to be.

The first part of this code creates the methods that we used above for updating the client lists from the WebSocketServer:

public Vector2? AddClient(int clientId)
{
    if (!clientPos.ContainsKey(clientId)) {

        clientPos.Add(clientId, new Vector2(randomPos.x, randomPos.y));
        clientTargets.Add(clientId, new Vector2(randomPos.x - 0.5f, randomPos.y - 0.5f));

        return clientPos[clientId];
    }

    return null;
}

public void UpdateClient(int clientId, float x, float y)
{
    if (clientTargets.ContainsKey(clientId)) {
        clientTargets[clientId] = new Vector2(x - 0.5f, y - 0.5f);
    }
}

public List<Dictionary<string, object>> GetClients()
{
    List <Dictionary<string, object>> clientList = new List<Dictionary<string, object>>();

    foreach (KeyValuePair<int, Vector2> kvp in clientPos) {
        clientList.Add(new Dictionary<string, object> { { "id", kvp.Key }, { "x", kvp.Value.x }, { "y", kvp.Value.y } });
    }

    return clientList;
}

We can use these to add a new client, update a client's position, and get a list of clients object in a format that we'll pass straight back to the connection layer, and from there will go on to the clients themselves.

We manipulate their positions slightly when we add them simply because of how we've laid this test Unity project out.

The last piece of important code we're going to cover is how we actually move the clients around our map:

private void Update ()
{
    randomPos = new Vector2(Random.value, Random.value);

    foreach (KeyValuePair<int, Vector2> kvp in clientTargets) {
        int clientId = kvp.Key;
        if (!clientObj.ContainsKey(clientId)) {
            clientObj.Add(clientId, Instantiate(clientPrefab));
            clientObj[clientId].transform.parent = map.transform;
            clientObj[clientId].transform.position = new Vector3((kvp.Value.x * map.transform.localScale.x) * -1f, 0, kvp.Value.y * map.transform.localScale.z);
        }

        clientObj[clientId].transform.position = Vector3.MoveTowards(clientObj[clientId].transform.position, new Vector3((clientTargets[clientId].x * map.transform.localScale.x) * -1f, 0, clientTargets[clientId].y * map.transform.localScale.z), 0.2f);

        clientPos[clientId] = new Vector2(((clientObj[clientId].transform.position.x / map.transform.localScale.x) * -1f) + 0.5f, (clientObj[clientId].transform.position.z / map.transform.localScale.z) + 0.5f);
    }
}

This uses the Update()method to re-calculate the positions for all players. What this means is that the single point of truth for where a client is and how fast their moving, is the Unity code. This, if we took this code further, would reduce the possibilities of hacking our clients, as they don't have the ability to control how quickly a player is moving, only where they intend to.

The weird randomPosvariable at the top of this method is simply a hack to get around random values not working with any code called from within a Coroutine, so we instead generate a new random number every frame.

Putting it all together

If your build a connection layer, a couple of clients, and a server then you can end up with a setup similar to something below. The window on the left is Unity, and the two on the right are Google Chrome:

The server in Unity with 2 HTML / JavaScript clients running. It may be simple but hopefully it's a good-enough base to work with.

That's all there is to these demos. The code for this can be found at github.com/dittto/sockets-server.