Multiplayer Interaction with p5js
In this article I will describe the basic setup to create a multiplayer interactive website/game using p5js, socket.io and express.
The goal of the article is to setup a basic system from which you can expand to create your own multiplayer interactivity / game. We will go over how to use socket.io on the client and the server to continually update positions of each client in p5js which can represent a player playing the game or someone interacting with the website.
Server
This tutorial assumes you have some basic knowledge of node.js and npm. If not try to follow this tutorial first to install and get the basic knowledge of npm.
We only need two dependencies on the server:npm install express socket.io
There is a lot going on here, so lets break it down.
The first part of the server code is importing express, http and socket.io.
Express is used to create the main server, where users can go to a url and it will return something. In this case I have set it up to return ‘index.html’ when someone goes to the root of my website.
app.get(“/”, (req, res) => { res.render(‘index.html’); });
socket.io is a library that provides the real time updates from each client to the server. You are not able to do this with just the express server, as a standard request takes too long to process. Instead socket.io mainly uses WebSockets under the hood, which makes real-time communication between a client, the server and other clients possible.
To store the position of each client that comes to my website, I just use a plain javascript object. const positions = {}
whenever a new client connects by going to my website, the ‘connection’ event will fire. They will be given a unique id that will act as a key in this object. So for example if 1 client connects the positions object would look like this:
{ unique_id1: {x: 0.5, y: 0.5} }
and when second client connects…
{
unique_id1: {x: 0.5, y: 0.5}
unique_id2: {x: 0.5, y: 0.5}
}
Then if for example the first person leaves the website that id will be deleted and the object will look like this:
{ unique_id2: {x: 0.5, y: 0.5} }
From the client side, people can send data to the server with a message. In this case we can receive ‘updatePosition’ from the client which will update the position of that client. So for example if client 2 calls ‘updatePosition’ with the data: { x: 0.2, y: 0.6 }
it will update the positions object to:
{ unique_id2: {x: 0.2, y: 0.6 }
Finally we send the position of each client to everyone who is connected. We can do that with the io.emit('positions', positions);
line. which sends a message called ‘positions’ with the positions object to each client. We wrap that into a setInterval
so the positions are send 30 times per second.
Now we have the basic server code up, we can start to look at the client side and how to set up the website to connect to the server and send and receive messages related to their position on the canvas we will create with p5js. We will start by setting up the canvas and positioning with p5js and then add the socket.io usage later on.
The client (p5js)
First I set up a basic html page that imports the code so we can use socket.io and p5js in our javascript files. It also sets up some basic styling so our canvas
element will always be the same aspect ratio and fill up the screen.
Inside the <div id='sketch-container'>
p5js will put the canvas. We will set this all up in our script.js
we include at the last part of the index file just before the closing body
tag.
Now we can setup p5js inside our canvas, here is the code first, we will go over it below:
This code sets up a very basic p5 canvas that draws a circle at the top left corner of the canvas. When the client clicks on the canvas the position of the circle changes to the position where the user clicked.
Notice that we are using p5js in instance mode. Which is why I first set up a function sketch
that takes p5js as an argument. Although p5js describes this as a more advanced way to use p5, I find it easier as you can easily see which functions are from p5 as all the functions are now bound to the argument that is given in the sketch function, so we call p.setup
and p.draw
etc.. The function immediately gets invoked at the line new p5(sketch, sketchContainer);
.
Getting the positions from the server
Same as before, lets look at the code first, and below we will go over some of the key points of interest.
In our final script.js
file, we now connect to the server with the line const socket = io();
We initialize it to only use websockets as a connection just like on the server. This takes away the possibility for people with older browsers to use our app, but it makes sure that all of the communication between the client and server is fast and uses less bandwidth.
Inside our sketch
function we now have a let positions = {}
object instead of just one position. Here we will store the positions of every client that connect to our site. We get the positions by setting up a callback with the socket.on("positions", callback);
function. Inside the callback we receive the data and we simply set our positions object to the data from the server. Remember that this “positions” message is send 30 times per second, so our data will be updated continuously.
Inside our draw function we now have to make some changes as well. We want to display all the circles instead of just one, so we loop over each key in the positions object with a for...in
loop. Then for each position, we draw our circle, notice we now have to multiply the position by the width and height inside the draw function, as each position is just a number between 0 and 1.
This is necessary because we can’t store the actual position on the server. For example, when 1 client is on a small screen where the canvas is only 500 pixels wide, if their x position is halfway (250px) and stored on the server it would only appear a quarter of the way on someone who views the website with a width of a 1000 pixels.
Finally we have to adjust our cnv.mousePressed
function as well. We want to update our position by sending the new position to the server with updatePosition
. The server will then send the updated position back to us and everyone else via the “positions”
message listener we already set up before. (The one that sends the position continuously).
For the same reason outlined above, we have to store the relative value of our position to the server instead of our position in absolute pixel values, so we divide our p.mouseX
by p.width
, and similarly for the height.
That is it! Now you have a basic setup to experiment with multiplayer interactivity or games. From here you can expand the code to your own liking, for example you can start to look at giving the circle that represents ‘me’ a different colour or size which you can do by looking up the socket id from the server which you can get withsocket.io.engine.id;
and then comparing that to all the id’s that are used in the positions
object.
Full Project
Thank you for reading! Feel free to comment with questions or tips! If you are interested, here is a video of an art project I made using this basic setup and a live streamed video that also reacts to everyone's positions.