DEVELOPER’S GUIDE TO UNITY3D MMO WITH NODE.JS USING SOCKET.IO



What is Node.js?
Node.js is a JavaScript engine which is built on Chrome’s V8. It is network application for communicates with client and server.It uses event-driven non-blocking I/O model. Node.js’s package ecosystem its called “npm”, which includes open source libraries in the world.Node is specially designed to broadly and scalable network applications for the device/computers.

What is Socket Programming?
Sockets are the mechanism of communication between two computers or devices using TCP. A client program which is installed in device/computer which creates a socket at the end of the communication when socket connection to a server. When a connection is done, the server creates its own socket object from the server side when an end of the communication.

                 Game Architecture between Unity Engine and Server

                       

Goals :
  • Describe the mechanism of events works with client side and server side. 
  • Unity 3d broadcasting a player’s X-Y-Z position, per frame, to other players grabbing the data via node.js. 
  • Update all players position, rotation, player data etc. 
  • Updating each time someone connects or disconnects from the socket. 
  • Refreshing the game object after taken any action by the user/client.

Requirements :
Unity Engine
Any Dedicated Server


Required External Tools :
Node.js – For connecting via server side.
Download Link : https://nodejs.org/en/

Best HTTP Unity Package – For connecting via client side.

Download Link : https://assetstore.unity.com/packages/tools/network/best-http-2-155981


Simple JSON : For parsing json data.
Download Link : Github - SimpleJSON.cs


Conceptual Overview :

The object broadcasting the information of the other player like X,Y,Z coordinates of players. 

Here I have demonstrated the basic game concept of moving one game object within a screen, the same reaction will be affected by the other user’s screen also.

It can works on Android/iOS/Windows Mobile device, as well it can work fine with a standalone build for Windows and Mac PC also.

For the cross-platform no need to do any extra stuff.


How Event Works?



How to use :


First, you have to setup the server with Node.js.
Tutorial Link for setup node server

Step – 1 : Import namespace

using System;
using BestHTTP;
using BestHTTP.SocketIO;

Step – 2 : Create Socket

Here we have to create socket for connecting socket of client with server.

var socketManagerRef = new SocketManager(new Uri(“http://chat.socket.io/socket.io/”));

The /socket.io/ path in the url is very important, by default the Socket.IO server will listen on this query. So don’t forget to append it to your test url too!

Example :
public void CreateSocketRef ()
{
TimeSpan miliSecForReconnect = TimeSpan.FromMilliseconds (1000);

options = new SocketOptions ();
options.ReconnectionAttempts = 3;
options.AutoConnect = true;
options.ReconnectionDelay = miliSecForReconnect;

options.ConnectWith = BestHTTP.SocketIO.Transports.TransportTypes.WebSocket;


//Server URI
socketManagerRef = new SocketManager (new Uri ("http://127.0.0.1:4001/socket.io/"), options);
}


Step – 3 : Connecting To Namespace

By default, the SocketManager will connect to the root(“/”) namespace while connecting to the server. You can access it through the SocketManager’s Socket property:

Socket root = manager.Socket;
Non-default namespaces can be accessed through the GetSocket(“/nspName”) function or through the manager’s indexer property:

Socket nsp = manager[“/customNamespace”];
// the same as this methode:
Socket nsp = manager.GetSocket(“/customNamespace”);

First access to a namespace will start the internal connection process.


Example :
public void SetNamespaceForSocket ()
{
namespaceForCurrentPlayer = socketNamespace;
mySocket = socketManagerRef.GetSocket (“/Room-1);
}


Step – 4 : Subscribing and receiving events

You can subscribe to predefined and custom events. Predefined events are “connect”, “connecting”, “event”, “disconnect”, “reconnect”, “reconnecting”, “reconnect_attempt”, “reconnect_failed”, “error”. Custom events are programmer defined events that your server will send to your client. You can subscribe to an event by calling a socket’s On function:

manager.Socket.On("login",OnLogin);
manager.Socket.On("new message", OnNewMessage);


An event handler will look like this:

void OnLogin(Socket socket, Packet packet, params object[] args) {
Debug.Log(“Login.”);
}

  • The socket parameter will be the namespace-socket object that the server sent this event.
  • The packet parameter contains the internal packet data of the event. The packet can be used to access binary data sent by the server, or to use a custom JSON parser lib to decode the payload data. More on these later.
  • The args parameter is a variable length array that contains the decoded objects from the packet’s payload data. With the default Json encoder these parameters can be ‘primitive’ types(int, double, string) or list of objects(List<object>) or Dictionary<string, object> for objects.

A message emitted on the server(node.js):


// send a message to the client
socket.emit('message', ‘MyNick’, ‘Msg to the client’); can be caught by the client:

// subscribe to the "message" event 

 manager.Socket.On("message", OnMessage);

// event handler
void OnMessage(Socket socket, Packet packet, params object[] args) {
// args[0] is the nick of the sender
// args[1] is the message

Debug.Log(string.Format("Message from {0}: {1}", args[0], args[1]));
}
  • “connect”: Sent when the namespace opens.
  • “connecting”: Sent when the SocketManager start to connect to the socket.io server.
  • “event”:Sentoncustom(programmerdefined)events.
  • “disconnect”: Sent when the transport disconnects, SocketManager is closed, Socket is closed or when no Pong message received from the server in the given time specified in the handshake data.
  • “reconnect”: Sent when the plugin successfully reconnected to the socket.io server.
  • “reconnecting”: Sent when the plugin will try to reconnect to the socket.io server.
  • “reconnect_attempt”: Sent when the plugin will try to reconnect to the socket.io server.
  • “reconnect_failed”: Sent when a reconnect attempt fails to connect to the server and the ReconnectAttempt reaches the options’ ReconnectionAttempts’ value.
  • “error”: Sent on server or internal plugin errors. The event’s only argument will be a BestHTTP.SocketIO.Error object.

Example :
//Connect
mySocket.On ("connect", OnConnect);

//Get UserAction Data From Server
mySocket.On ("action", OnGetActionData);

//Leave Room
Socket.On ("leave", OnLeaveRoomData); //Disconnect mySocket.On ("disconnect", OnDisconnect); }

//=== On Event's Methods ===//

//Connect To Room
private void OnConnect (Socket socket, Packet packet, params object[] args) {
Debug.Log ("Connect...");
}

//Get User Action Data
private void OnGetActionData (Socket socket, Packet packet, params object[] args) {
var res = JSON.Parse (packet.ToString ()); Debug.Log(res); //Here display response... }

//Leave Room Data
private void OnLeaveRoomData (Socket socket, Packet packet, params object[] args) { Debug.Log ("Leave Room"); }

//Disconnect From Room
private void OnDisconnect (Socket socket, Packet packet, params object[] args) {Debug.Log ("Disconnect..."); }


Step – 5 : Sending Events

Here Emit function is for the sending event. We have to pass the event name for the primary parameter and others parameters are optionally. These all the data will encoded to json/ dictionary data and then send these data to server. Optionally all the server response set to the callback.


// Send a custom event to the server with two parameters
manager.Socket.Emit("message", "userName", "message");

// Send an event and define a callback function that will be called as an //acknowledgement of this event
manager.Socket.Emit("custom event", OnAckCallback, "param 1", "param 2");

void OnAckCallback(Socket socket, Packet originalPacket, params object[] args) {
Debug.Log("OnAckCallback!");
}


Sending acknowledgement to the server

You can send back an acknowledgement to the server by calling the socket’s EmitAck function. You have to pass the original packet and any optional data:

manager["/customNamespace"].On("customEvent", (socket, packet, args) => {socket.EmitAck(packet, "Event", "Received", "Successfully"); });

You can keep a reference to the packet, and call the EmitAck from somewhere else.

Example :

//Send User Action Data
public void SendGameActionDataToServer (float objXPos, float objYPos, string senderDeviceUniqueId) {
Dictionary<string, object> gameActionData = new Dictionary<string, object> ();
gameActionData.Add ("objXPos", objXPos);
gameActionData.Add ("objYPos", objYPos);
gameActionData.Add ("senderDeviceUniqueId", senderDeviceUniqueId);
mySocket.Emit ("action", OnSendEmitDataToServerCallBack, gameActionData);
}

//Send Leave Room Data
public void LeaveRoomFromServer (){
mySocket.Emit ("leave", OnSendEmitDataToServerCallBack);
}


Step – 6 : Sending Binary Data [Optional]

This is the most efficient way, because it will not convert the byte array to a Base64 encoded string on client side, and back to binary on server side.

byte[] data = new byte[10];manager.Socket.Emit("eventWithBinary", "textual param", data);


Step – 7 : Receiving Binary Data [Optional]


On client side these packets will be collected and will be merged into one packet. The binary data will be in the packet’s Attachments property.

Socket.On("frame", OnFrame);void OnFrame(Socket socket, Packet packet, params object[] args){texture.LoadImage(packet.Attachments[0]); }


Step – 8 : Make Game concept in Unity Engine




Step – 9 : Create and Implement Socket.IO script

Add MySocketManager script to empty gameobject.




MySocketManager.cs


using UnityEngine;
using System;
using BestHTTP;
using BestHTTP.SocketIO;
using SimpleJSON;
using System.Collections.Generic;

public class MySocketManager : MonoBehaviour
{
//Instance
public static MySocketManager instance;


//Set Namespace For Current Player
public string namespaceForCurrentPlayer;

//SocketManager Reference
public SocketManager socketManagerRef;

//Socket Reference
public Socket mySocket;

//Options
SocketOptions options;

// Use this for initialization
void Start ()
{
//Instance
instance = this;

//Create Socket Ref
CreateSocketRef ();
}

//Leave Room After Quite The Game
void OnApplicationQuit ()
{
LeaveRoomFromServer ();
DisconnectMySocket ();
}

public void CreateSocketRef ()
{
TimeSpan miliSecForReconnect = TimeSpan.FromMilliseconds (1000);

options = new SocketOptions ();
options.ReconnectionAttempts = 3;
options.AutoConnect = true;
options.ReconnectionDelay = miliSecForReconnect;

options.ConnectWith = BestHTTP.SocketIO.Transports.TransportTypes.WebSocket;

//Server URI
socketManagerRef = new SocketManager (new Uri ("http://127.0.0.1:4001/socket.io/"), options);

}

public void SetNamespaceForSocket (string socketNamespace)
{
namespaceForCurrentPlayer = socketNamespace;
mySocket = socketManagerRef.GetSocket (namespaceForCurrentPlayer);

//Set All Events, When Join The New Room
SetAllEvents ();
}

//=== Emit Events ===//

//Send User Action Data
public void SendGameActionDataToServer (float objXPos, float objYPos, string senderDeviceUniqueId)
{
Dictionary<string, object> gameActionData = new Dictionary<string, object> ();
gameActionData.Add ("objXPos", objXPos);
gameActionData.Add ("objYPos", objYPos);
gameActionData.Add ("senderDeviceUniqueId", senderDeviceUniqueId);

mySocket.Emit ("action", OnSendEmitDataToServerCallBack, gameActionData);
}

//Send Leave Room Data
public void LeaveRoomFromServer ()
{
mySocket.Emit ("leave", OnSendEmitDataToServerCallBack);
}

//Disconnect My Socket
public void DisconnectMySocket ()
{
mySocket.Disconnect ();
}

private void SetAllEvents ()
{

//Get UserAction Data From Server
mySocket.On ("action", OnGetActionData);

//Leave Room
mySocket.On ("leave", OnLeaveRoomData);

//Connect
mySocket.On ("connect", OnConnect);

//Re-Connect
mySocket.On ("reconnect", OnReConnect);

//Re-Connecting
mySocket.On ("reconnecting", OnReConnecting);

//Re-Connect Attempt
mySocket.On ("reconnect_attempt", OnReConnectAttempt);

//Re-Connect Attempt
mySocket.On ("reconnect_failed", OnReConnectFailed);

//Disconnect
mySocket.On ("disconnect", OnDisconnect);

}

//=== On Event's Methods ===//

private void OnConnect (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Connect...");
}

private void OnReConnect (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Re-Connect...");
}

private void OnReConnecting (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Re-Connecting...");
}

private void OnReConnectAttempt (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Re-Connect Attempt...");
}

private void OnReConnectFailed (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Re-ConnectFailed...");
}

//Get User Action Data
private void OnGetActionData (Socket socket, Packet packet, params object[] args)
{
var res = JSON.Parse (packet.ToString ());
GameManager.instance.RefreshGamePlayerUI (res [1]);
}

//Leave Room Data
private void OnLeaveRoomData (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Leave Room");
}

//Disconnect From Room
private void OnDisconnect (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Disconnect...");
}

//=== Emit Event's Methods ===//

private void OnSendEmitDataToServerCallBack (Socket socket, Packet packet, params object[] args)
{
Debug.Log ("Send Packet Data : " + packet.ToString ());
}
}


Step – 10 : Create and ImplementGameManager script

MyDeviceUnique ID : Get device’s unique id for getting unique user for real MMO(Massively Multiplayer Online) functionality.
Main Menu Screen : Mainmenu UI Screen.
Game Play Screen : Gameplay Object Screen.
Game Play UI : Gam Play UI Screen.
Blue Obj : Reference object for display own movement.
Red Obj : Reference object for showing opponent(Red Obj)’s movement.
Movement Speed : Object movement speed.

GameManager.cs


using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using SimpleJSON;

public class GameManager : MonoBehaviour
{

//Instance
public static GameManager instance;

//My Device Unique ID
public string myDeviceUniqueID;

//GameOver Screen
public GameObject mainMenuScreen;
public GameObject gamePlayScreen;
public GameObject gamePlayUI;

//Objects
public Transform blueObj;
public Transform redObj;

//Movement Speed
public float movementSpeed;

//Store RedObj New Position
private Vector2 newPosition;

// Use this for initialization
void Start ()
{
//Instance
instance = this;

//Set My Device Unique ID
myDeviceUniqueID = SystemInfo.deviceUniqueIdentifier;

//Init Thief New Position
newPosition = new Vector2 (0, 0);
}

//On Let's Play Btn Clicked
public void OnLetsPlay ()
{
GetRoomData ();
}

//On PlayAgain Btn Clicked
public void OnPlayAgain ()
{
Time.timeScale = 1;
Application.LoadLevel (0);
}

//Show My Screen
public void ShowMyScreen (GameObject myScreen)
{
myScreen.SetActive (true);
}

//Hide My Screen
public void HideMyScreen (GameObject myScreen)
{
myScreen.SetActive (false);
}

//Move Object
public void MoveObject (string ObjDir)
{
if (ObjDir == "Left") {
blueObj.Translate (Vector3.left * movementSpeed * Time.deltaTime);
} else if (ObjDir == "Right") {
blueObj.Translate (Vector3.left * -movementSpeed * Time.deltaTime);
} else if (ObjDir == "Up") {
blueObj.Translate (Vector3.down * -movementSpeed * Time.deltaTime);
} else if (ObjDir == "Down") {
blueObj.Translate (Vector3.down * movementSpeed * Time.deltaTime);
}

//clamping
blueObj.position = new Vector2 (Mathf.Clamp (blueObj.localPosition.x, -28, 28), Mathf.Clamp (blueObj.localPosition.y, -15, 15));

//Send Object Position To Server
MySocketManager.instance.SendGameActionDataToServer (blueObj.position.x, blueObj.position.y, myDeviceUniqueID);
}

//Refresh Game UI Data
public void RefreshGamePlayerUI (JSONNode gameUIData)
{
newPosition.x = gameUIData ["objXPos"].AsFloat;
newPosition.y = gameUIData ["objYPos"].AsFloat;

if (gameUIData ["senderDeviceUniqueId"].Value != myDeviceUniqueID) {
redObj.localPosition = newPosition;
}
}

private void GetRoomData ()
{
//Get Room Data
WWW getRoomData = new WWW ("http://127.0.0.1:4001/ws/getRoom");
StartCoroutine (WaitForGetRoomDataResponse (getRoomData));
}

IEnumerator WaitForGetRoomDataResponse (WWW www)
{
yield return www;
var res = JSON.Parse (www.text);

if (res ["status"].Value == "true") {
HideMyScreen (mainMenuScreen);
ShowMyScreen (gamePlayS creen);
ShowMyScreen (gamePlayUI);

//=== Send Data To Server For Create Namespace ===//
string namespaceForRoom = "/room-" + res ["data"] ["room_id"].Value;

//Create Namespace For Current User And Send To Server
MySocketManager.instance.SetNamespaceForSocket (namespaceForRoom);
}}}


Step – 11 : Build the Apk or standalone file and let’s play and enjoy the game.

Comments

  1. Replies
    1. Thank you so much for your valuable feedback.

      Delete
  2. Replies
    1. Thank you so much for your valuable feedback.

      Delete

Post a Comment

Popular posts from this blog

GDLC [Game Development Life Cycle]

Data Encryption - Decryption [Unity3d - C#]

Unity Tiny | Tiny Mode | Instant Games | Playable Ads

NAMING CONVENTIONS | CODING CONVENTIONS | PROGRAMMING GUID

DOTS - Unity3d

Unity's Back-end System

ANDROID GO - LIGHTER, SMARTER & OPTIMIZED ANDROID OS

Unity3d – Attributes