Using SocketIO and Unity 3D To Build Multi-User Experiences

socket io

Like many homes, my kids and I enjoy playing video games together. At this stage of life, my kids especially playing games like Minecraft or Roblox. As I continue to explore the Unity ecosystem, I decided to look into using NodeJs to implement a playground for my kids in a Unity game. In this post, I’ll outline the major ideas I used to create my prototype. In the future, hope this prototype evolves into a multiplayer tank shooter game.

This blog post will assume a working knowledge of Unity. To start things off, I installed the following tools from the Unity Asset store. If you’re interested in getting started with Unity, check out their learning materials here.

https://assetstore.unity.com/packages/tools/network/socket-io-for-unity-21721(by Fabio Panettieri)
https://assetstore.unity.com/packages/tools/input-management/json-net-for-unity-11347
(from Parent Element LLC)

The SocketIO asset includes a sample scene outlining the common setup.

If you’re interested in inspecting a finished sample, please visit the following GitHub link.
https://github.com/michaelprosario/tanks

I did test this SocketIO in an Android mobile environment too. Unfortunately, it didn’t work well. These patterns seem to work fine with Unity desktop experiences. There may be additional edits I need to do to get it working.

While learning this pairing of Unity 3D and SocketIO, I found the following video series very helpful too. Thank you Adam Carnagey!

To build our prototype, let’s kick things off by building our server. This server will leverage NodeJs. In your server working directory, create a package.json like the following:

{
  "name": "tank-server",
  "version": "1.0.0",
  "description": "",
  "main": "tank-server.js",
  "scripts": {
    "start": "nodemon tank-server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "socket.io": "^2.3.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.2"
  }
}

Next, let’s implement our tank-server.js. This code will instantiate a server using web sockets at port 4567.

https://github.com/michaelprosario/tanks/blob/master/TankServer/tank-server.js

var io = require('socket.io')({
    transports: ['websocket'],
});

io.attach(4567);

var players = {};

io.on('connection', function (socket) {

    // Message received when player starts
    // Share with other clients
    socket.on("PlayerStartedCommand", (data) => {
        players[data.PlayerId] = data;
        socket.broadcast.emit('PlayerStartedEvent', data);
    })

    // Message received when player moves
    // Share with other clients
    socket.on("PlayerMovedCommand", (data) => {
        players[data.PlayerId] = data;
        socket.broadcast.emit('PlayerMovedEvent', data);
    })

    // Query handler to find other players and their data
    socket.on("GetPlayersQuery", (data) => {
        var list = [];
        for (var key in players) {
            if (players.hasOwnProperty(key)) {
                list.push(players[key]);
            }
        }

        var GetPlayersResponse = {};
        GetPlayersResponse.Players = list;
        socket.emit('GetPlayersResponse', GetPlayersResponse);
    })
})

Implementing the Unity client controller

In your Unity solution, you will go through a process of creating a scene and defining your environment. In this section, we’ll outline the major parts of implementing the socket io prefab and the controller script.

  1. Like any game experience, we start with creating a new unity project, adding a scene, making a terrain, and player objects.

  2. Install the Unity SocketIO package from the asset store. https://assetstore.unity.com/packages/tools/network/socket-io-for-unity-21721

  3. After the package installs, inspect the README.txt and sample scene to get a feel of how you layout your scene and configure the references to your server. I recommend that you layout your scene in a similar fashion.

  4. For my solution, I started by crafting a few command and query objects to communicate state changes of my players to the server. We’ll also review response objects to receive data from the server.

using Newtonsoft.Json;

namespace TankGame
{
    public class BaseServerCommand : TankGame.IServerCommand
    {
        public BaseServerCommand()
        {
        }
        public string ToJsonString()
        {
            return JsonConvert.SerializeObject(this);
        }

        public string CommandName => GetType().Name.ToString();
    }
}
using Newtonsoft.Json;

namespace TankGame
{
  public class PlayerStartedCommand : BaseServerCommand
  {
    public PlayerStartedCommand()
    {
    }

    public string PlayerId { get; set; }
    public Position Position { get; set; }
    public Rotation Rotation { get; set; }
  }
}
using Newtonsoft.Json;

namespace TankGame
{
  public class PlayerMovedCommand : BaseServerCommand
  {
    public PlayerMovedCommand()
    {
    }

    public string PlayerId { get; set; }
    public Position Position { get; set; }
    public Rotation Rotation { get; set; }
  }
}
using System.Collections.Generic;
using TankGame;

namespace Assets.Core.Responses
{
    public class GetPlayersResponse
    {
        public IEnumerable<Player> Players;
    }
}

In this interface, we model some of the key operations of our scene. In your implementation of the scene, you will need to provide concrete implementations of these operations. You can see my simple implementation here:
https://github.com/michaelprosario/tanks/blob/master/Assets/Scenes/SceneOneView.cs

using Assets.Core.Responses;
using TankGame;

namespace Assets.Scenes
{
    public interface ISceneOneView
    {
        void HandleGetPlayersResponse(GetPlayersResponse response, string currentPlayer);
        PlayerMovedCommand GetPlayerMovedCommand(string currentPlayerId);
        PlayerStartedCommand GetNewPlayerCommand(string currentPlayerId);
        void HandlePlayerStarted(PlayerStartedCommand command);
        void HandlePlayerMoved(PlayerMovedCommand command);
    }
}

At this point, we’re ready to talk through the main socket controller script. I’m only going to highlight the key themes of the script. You can inspect the whole body of the script here. https://github.com/michaelprosario/tanks/blob/master/Assets/Scenes/SceneOneSocketController.cs

This controller depends upon the socketIO and a reference to the current player. The controller keeps a reference to the view interface so that messages from the server can be routed to scene actions.

private SocketIOComponent socket;
private string currentPlayerId;
private ISceneOneView view;

When we start the controller, we give our player an identity. Next, we resolve the references for the view and the socket IO component. Finally, we connect various socket events to handlers. We also let the server know that a new player started.

public void Start()
{
    currentPlayerId = System.Guid.NewGuid().ToString();

    GameObject go = GameObject.Find("SocketIO");
    socket = go.GetComponent<SocketIOComponent>();

    GameObject viewGameObject = GameObject.Find("View");
    view = viewGameObject.GetComponent<SceneOneView>();

    socket.On("open", HandleOpen);
    socket.On("error", HandleError);
    socket.On("close", HandleClose);
    socket.On("PlayerStartedEvent", HandlePlayerStartedEvent);
    socket.On("PlayerMovedEvent", HandlePlayerMovedEvent);
    socket.On("GetPlayersResponse", HandleGetPlayersResponse);

    StartCoroutine("SendPlayerStarted");
}

The following methods enable us to send commands and query requests to the server.

private void sendCommand(IServerCommand command)
{
    socket.Emit(command.GetType().Name, new JSONObject(command.ToJsonString()));
}

private void sendQuery(IServerQuery query)
{
    socket.Emit(query.GetType().Name, new JSONObject(query.ToJsonString()));
}

To announce a new player, we leverage the following method.

private IEnumerator SendPlayerStarted()
{
    yield return new WaitForSeconds(1);
    sendCommand(view.GetNewPlayerCommand(this.currentPlayerId));
    sendQuery(new GetPlayersQuery());
}

As we move around, we let the server know how we’re moving. My gut says that I need to find a more optimal way to communicate this kind of state. I think it will work fine for a small number of players on a local network.

public void Update()
{
    sendCommand(view.GetPlayerMovedCommand(this.currentPlayerId));
}

The following handlers map responses from the socket server to view actions.

private void HandleGetPlayersResponse(SocketIOEvent e)
{
    var jsonString = e.data.ToString();
    var response = JsonConvert.DeserializeObject<GetPlayersResponse>(jsonString);
    view.HandleGetPlayersResponse(response, currentPlayerId);
}

private void HandlePlayerStartedEvent(SocketIOEvent e)
{
    var jsonString = e.data.ToString();
    var command = JsonConvert.DeserializeObject<PlayerStartedCommand>(jsonString);
    if (command.PlayerId == this.currentPlayerId)
        return;

    view.HandlePlayerStarted(command);
}

private void HandlePlayerMovedEvent(SocketIOEvent e)
{
    var jsonString = e.data.ToString();
    var command = JsonConvert.DeserializeObject<PlayerMovedCommand>(jsonString);
    if (command.PlayerId == this.currentPlayerId)
        return;
    view.HandlePlayerMoved(command);
}

Hope this post gives you a general overview of how to start a multi-player Unity scene using NodeJS and SocketIO. We love to hear from our readers. Please share a comment and let us know what you’re building!

Be the first to comment

Leave a Reply

Your email address will not be published.


*