How to make a basic multiplayer online system with websockets

peq42_

Yeet
Veteran
Joined
Feb 5, 2016
Messages
486
Reaction score
290
First Language
Portuguese(BR)
Primarily Uses
RMMV
Introduction

Today we will go over making a very basic multiplayer online system using websockets. This is usually a topic that many people struggle with, because it usually involves installing many things, dealing with heavily written plugins and libraries, etc and most people that create plugins/demos for doing that in MV, does it in a complex way, making it hard for a beginner to get the basics.

What we will build here is the very basic foundation of a multiplayer online system. Our project will allow up to 3 players at the same time and we will make a kick system as well as a command for closing the server. At the end, there will be ideas and indications for those who want to expand it.

What will we need
For this project, we will use just the RPG Maker MV(I recommend the last version, for bette performance) for the client, and a software of mine for the server, which you can download for free here:
https://jitjs.blogspot.com/p/download.html
(You can also use node.js for the server, but at least I believe my project to be easier to work with. Also, the command for kick and closing the server that we will later do requires it to work).
You also need a text/code editor and some basic knowledge of javascript.

The server

Now let's start building our server. Since what we're doing is something very basic, our server will only register each player under an ID number, receive information about its location and broadcast back.

To begin, we will need to require a library called "nodejs-websocket". If you're using nodejs, open it and do "npm install nodejs-websocket", if you're using JIT.js, its already installed by default. So let's begin:

Code:
var ws = require("nodejs-websocket") // require our websocket server library
var playercounter=0 //Create a variable to count the number of players connected, so each will receive one ID

var server = ws.createServer(function (connection) { //this creates the server
//our code will go here
}).listen(8081) //start running the server, listening to port 8081

Done, this is our basic server structure. With just that, we already opened a websocket server(which is listening to the port 8081) that anyone can connect. That function within ws.createServer() is our callback function which is automatically added to the "connection" event. The connection event represents... well... a connection, and we can add to each of those connections a property. Let's create a property called "ID" to more easily idenfity a player.

Code:
var ws = require("nodejs-websocket")
var playercounter=0

var server = ws.createServer(function (connection) {
    connection.playerid = null //For now, let's define it as null

}).listen(8081)

Now, the task of broadcasting to all players a certain information will be repeated many times in this structure we're making. Let's build a function, outside of it, that we can call and it will send to all players the information we want. This will save us some work and lines later.

Code:
function broadcast(str) {
    server.connections.forEach(function (connection) {
        connection.sendText(str)
    })
//This function will send the argument "str" received to all connected players
//Place this after the ws.createServer created above
}

Good, now we can continue. The next part of our server is its response whenever a player sends a message. To build it, we will use the "text" event, which function is exactly what we want.

Code:
var ws = require("nodejs-websocket")// require our websocket server library
var playercounter=0 //Create a variable to count the number of players connected, so each will receive one ID

var server = ws.createServer(function (connection) {
    connection.playerid = null
    connection.on("text", function (str) {
        if (connection.playerid === null) { /*if connection.playerid===null, it means that its the first time the player is connecting, so this part executes*/
            playercounter++ //we add 1 to the player counter
            connection.playerid = playercounter //the id of the player will be equal to the current number of playercounter
            console.log("New player joined, ID:"+playercounter) /*displays a message showing that a new player connected within JIT.js*/
            connection.sendText("id:"+playercounter) //we send to the player his ID
            broadcast("eventid:"+playercounter) //We send to all users that there's a new player under that ID
        } else //if its not the first connection
            broadcast("update:"+connection.playerid+":"+str) //we send to everyone the position of that player
    })


}).listen(8081)

Nice! Our server now can receive and send information about position to/from everyone! If you want, you can also add the following event, within our callback function, for error tracking in connection:

Code:
    connection.on("error", function(err){
        console.warn(err)
    })

Now to the promised "close" and "kick" commands. Here we will deal with a few things that are specific of JIT.js, but can be adapted to work on node.js, but, as said, it's a lot more work, and honestly I don't know how to do.

First, let's begin with creating our askmode "container". After everything(synchronous) outside of it runs, that container takes place and keeps looping itself for each command you send in the terminal. Each time it loops, a variable called "jitask" is given to you, with the last input string given within the terminal. Using it, we will build some basic commands for our server.(For more information about this mode click here).

Code:
askmode{//Enter's askmode

if(jitask==="close"){//if you write "close" within JIT.js, this runs
        server.connections.forEach(function (connection) {//runs this function for each active connection
        connection.close() //close the current connection
    })
console.log("Server closed.")
backtoconsole() //ends the askmode and returns to JIT.js' terminal
}
if(jitask.startsWith("kick")){//If you write something that starts with kick
var kickid=jitask.split(" ")//splits the string received from the terminal


            server.connections.forEach(function (connection) {//repeats this function for each connection

        if(String(connection.playerid)===String(kickid[1]))/*If the number written after ID is the same as one of the players' ID */
        {
            connection.close()//close's the connection with that specific player
            console.log("Player id "+kickid[1]+"has been kicked.")
        }
    })
}

}askmode
(Add it under the broadcast function)

With the code above, when we run our server, we will lock JIT.js for receiving only 2 commands: "close" and "kick <NUMBER>". If you write the first one within the terminal, all connections are closed, and the askmode ends. If its the second one, and the number given is the same as one of the player's ID, that given player will be disconnected.

If you did everything correctly until here, your server code should look like this:

Code:
var ws = require("nodejs-websocket")// require our websocket server library
var playercounter=0 //Create a variable to count the number of players connected, so each will receive one ID

var server = ws.createServer(function (connection) { //this creates the server
    connection.playerid = null
    connection.on("text", function (str) {/*if connection.playerid===null, it means that its the first time the player is connecting, so this part executes*/
        if (connection.playerid === null) {
            playercounter++//we add 1 to the player counter
            connection.playerid = playercounter//the id of the player will be equal to the current number of playercounter
            console.log("New player joined, ID:"+playercounter)/*displays a message showing that a new player connected within JIT.js*/
            connection.sendText("id:"+playercounter)//we send to the player his ID
            broadcast("eventid:"+playercounter)//We send to all users that there's a new player under that ID
        } else
            broadcast("update:"+connection.playerid+":"+str)/*We send to all players the position information received from one of them. */
    })
    connection.on("error", function(err){
        console.warn(err)
    })

}).listen(8081) //start running the server, listening to port 8081

function broadcast(str) {
    server.connections.forEach(function (connection) {
        connection.sendText(str)
    })
}
console.log("Server running!")
console.log('Type "kick <id>" to kick a player from the server, or "close" to close the server.')

///////// From now on, it won't work in node.js

askmode{//Enter's askmode

if(jitask==="close"){//if you write "close" within JIT.js, this runs
        server.connections.forEach(function (connection) {//runs this function for each active connection
        connection.close()//close the current connection
    })
console.log("Server closed.")
backtoconsole()//ends the askmode and returns to JIT.js' terminal
}
if(jitask.startsWith("kick")){//If you write something that starts with kick
var kickid=jitask.split(" ")//splits the string received from the terminal


            server.connections.forEach(function (connection) {//repeats this function for each connection

        if(String(connection.playerid)===String(kickid[1]))/*If the number written after ID is the same as one of the players' ID */
        {
            connection.close()//close's the connection with that specific player
            console.log("Player id "+kickid[1]+"has been kicked.")
        }
    })
}

}askmode

Save it as server.js, open JIT.js, use the "cd" command to go to the folder you saved and run it with "jit ./server.js". Done! Your server is up and running!
Let's now head to our Client part.

The client

Our client won't require a lot of things. At least not external(like libraries and softwares). In order to it to work not only on PC(Windows, Linux and Mac) but also browsers, here we will be using only HTML5 features here(which shouldn't be much different from how we coded our server part).

First, make a new project. Create an Event in a map with 3 pages. The first empty, the second checking if the variable with ID 1 is >=1, and the third with the same check + Check if self switch A is on. Also, add a image to that last page(of a character). Make the second page have a conditional branch, checking if the variable ID 1 is different from the variable ID 10, and that all pages have priority as "Bellow Characters". Now copy this event 2 times, changing the checks in the second and third pages(both in the page and in the conditional branch) to ID 2(for the second copy) and 3(for the third copy).

Those objects will represent the other players. The variables 1,2 and 3 will receive 1, 2 and 3 respectively whenever a first, second and third player join. When a client connects, the variable of ID 10 will receive our ID. With that system, no objects will show in the map, until a player joins, and we won't see a copy of ourselves in the map too.

[IMG]https://i71.servimg.com/u/f71/15/27/47/75/110.png[/IMG]
[IMG]https://i71.servimg.com/u/f71/15/27/47/75/210.png[/IMG]
[IMG]https://i71.servimg.com/u/f71/15/27/47/75/310.png[/IMG]

After that, create a common event, change the trigger to "parallel" and leave the switch to "001". This event will catch the player's position every frame. Select the variable ID 4, mark "Game data", click it, in the new window opened mark "Character", select "Player" and then "Map X". Then add another one and do the same, but now for Variable ID 4 and in the end select Map Y.

[IMG]https://i71.servimg.com/u/f71/15/27/47/75/111.png[/IMG]

Ok, finished that, save the event, close the window, and let's go back to the map. Make it set the location of the events we made before(ID 1,2 and 3) to be the same as the place where the player spawns, then make it set the Switch 1 to "ON", call the common event we made in the last part and finally add a Script event within it.

This script will be responsible for connecting us to the server, and create a function that every 16ms(+/- the time that each frame stays in the screen, when running the game at 60FPS) will send our position to the server.

Code:
var update

    var connection = new WebSocket("ws://localhost:8081")/*Creates our websocket connection to the server we made. You can change locahost by the IP:Port of a dedicated server if you have one */

        connection.onopen = function () {//This function runs when we connect to the server
            console.log("Connection made")

            setInterval(()=>{//repeats the following code every 16ms
                coordinates=$gameVariables.value(4)+":"+$gameVariables.value(5);//makes a variable to hold the player X and Y
                connection.send(coordinates);//Sends to the server the variable with player X and Y
            },16)

        }

        connection.onmessage = function (event) {//This function runs when we receive a message from the server

            update=event.data //variable "update" receives the information from the server
            update=update.toString().split(":")/*since we're sending multiple informations in the same message, we add this ":" to separate them*/
            if(update[0].startsWith("update")){//If the first part of the message received begins with "update"
                if(Number(update[1])!==Number($gameVariables.value(10))){//and the second part is not the same number as our ID
                    $gameMap.event(Number(update[1])).setPosition(Number(update[2]), Number(update[3]))/*we update that event ID, X and Y*/
                    $gameVariables.setValue(Number(update[1]),Number(update[1]))//This is just to make sure that other players that joined before will be visible
                    }
            }
            else if(update[0].startsWith("id")){//If the first part starts with ID
                $gameVariables.setValue(10,Number(update[1]))/*We set the variable ID 10 to the our ID. This will help us make sure that there's not an event copying our movement*/
            }
            else if(update[0].startsWith("eventid")){//If the first part starts with eventid
                $gameVariables.setValue(Number(update[1]),Number(update[1]))/*We give the variable with that ID a value equal to the ID. This way, we can make sure that the 3 events we made in the map will only be visible and move when there's a player with ID equal to their ID*/
            }

        }
(Read the code comments to understand it better. You will also probably need to minify that code, because the MV's code window is really limited. Try using an online service such as this one)

Now add "erase event" to it, set trigger to autorun, save and close. If you did everything right, the client should be done, and you can deploy it for testing(To run multiple windows of your RPG Maker MV game at once for testing, you will need to change "name" parameter within a file called "package.json" each time you open it).

Final considerations

The project we made in this tutorial is, once again saying, really simple and basic, it basically teleports events, that represents our players, to the same location in a map that our players are. But, using the basic knowledge you got here, and doing some research on how websockets work, you can easily extend it to do your liking.

For example:
  • Using the same websocket system above, you can easilly build a in-game chat.
  • You can easilly send a message before all else(In the function thar runs right when the client connects) from the client to the server, and make that message contain the nickname of the player, displaying it above the players head using this plugin
  • You can make the events move in a less robotic way using this plugin
  • You can create a basic database system, requiring a username and password from the client upon connection, and storing it in the server as a .json file(encrypting the password using itself as the encryption key, so each password is encrypted with a different key)
Theres much you can do using just the basics of server and client seen here. How will your project look like, and work, is up to you.

As a final word, I would like to say that, this is my first time making a tutorial. I did made some documentations(for my software, for example), but those were only for informing "this do that". So if you didn't understand something and/or would like me to change/add something, feel free to PM me about it or comment bellow, and I will do whatever I can to help.

If you want, you can download a demo with all the code used:
https://www.dropbox.com/s/35la3zbaqdwm94w/demo project.zip?dl=0
Also check this open-source game of mine made using the principles/suggestions given in this tutorial:
https://forums.rpgmakerweb.com/index.php?threads/rpg-battle-royal-alpha-0-1.106007/
(You may use it as a base for a project of yours)
 
Last edited:

Aloe Guvner

Walrus
Veteran
Joined
Sep 28, 2017
Messages
1,628
Reaction score
1,130
First Language
English
Primarily Uses
RMMV
Neat, I'll give it a try sometime!
 

Latest Threads

Latest Posts

Latest Profile Posts

Ami
tomorrow is the finale of my Endurance
2dfloor.png
What a fun tileset I've got here so far!
Minecraft Tiles, Maid Day, Custom Menu, Retry at GameOver, Save in Appdata | RPG Maker News #36

side view special bust.png
Reworking the art for the 'special busts' in important game dialogue. Potentially replacing the more 'dead-on' look of the original:
original.png
Me: Imma work on my game! Let's get some progress done
Me, five minutes later smoking a cigarette: *screams into the void*

Forum statistics

Threads
111,263
Messages
1,059,671
Members
144,546
Latest member
ItsMeDevRoland
Top