What is Node.js ?
Node.js is a JavaScript engine which is built on Chrome’s V8. It is network application for communicates between 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 is 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 connect to server. When connection is done, then server creates a its own socket object from the server side when 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 with the socket.
- Refreshing the gameobject 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://www.assetstore.unity3d.com/en/#!/content/10872 - Simple JSON : For parsing json data.
Download Link : http://wiki.unity3d.com/index.php/SimpleJSON
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 screen, the same reaction will be affected with the other user’s screen also.
It can works for Android/iOS/Windows Mobile device, as well it can works fine with stand alone build for Windows and Mac PC also.
For the cross platform no need to do any extra stuff.
How Event Works ?
How to use :
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; //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 :
private void SetAllEvents () { //Connect mySocket.On ("connect", OnConnect); //Get UserAction Data From Server mySocket.On ("action", OnGetActionData); //Leave Roommy 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 { #region PUBLIC_VARS //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; #endregion #region PRIVATE_VARS #endregion #region UNITY_CALLBACKS // Use this for initialization void Start () { //Instance instance = this; //Create Socket Ref CreateSocketRef (); } //Leave Room After Quite The Game void OnApplicationQuit () { LeaveRoomFromServer (); DisconnectMySocket (); } #endregion #region PUBLIC_METHODS public void CreateSocketRef () { TimeSpan miliSecForReconnect = TimeSpan.FromMilliseconds (1000); options = new SocketOptions (); options.ReconnectionAttempts = 3; options.AutoConnect = true; options.ReconnectionDelay = miliSecForReconnect; //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 (); } #endregion #region PRIVATE_METHODS 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 ()); } #endregion }
Step – 10 : Create and Implement GameManager 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 { #region PUBLIC_VARS //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; #endregion #region PRIVATE_VARS //Store RedObj New Position private Vector2 newPosition; #endregion #region UNITY_CALLBACKS // 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); } #endregion #region PUBLIC_METHODS //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; } } #endregion #region PRIVATE_METHODS 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 (gamePlayScreen); 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); } } #endregion }