Jump to content

Updating players positions using Node.js and Socket.io (Multiplayer)


Recommended Posts

Hi guys,

 

So I'm trying to expand the game I'm making to be a multiplayer game using Node.js and Socket.io.

 

The connection between server-side and client-side is working, however I don't know how to update the positions of the players.

 

This is how it goes, whenever a player moves the socket.sessionid and coordinates are send to the server.

The server then broadcasts this data to all the other clients except the client which sent the data.

In the client-side that information gets decoded and stored in 2 variables (at this moment), a player variable (with the id), and a X variable (with the x value, duh).

 

At this moment I really don't know how to update the positions of a certain sprite in the game.

 

Are there any examples on how to create a basic multiplayer Phaser game using websockets?

 

I'm kinda stuck.

 

 

The code I'm using on the client side is as follows, it is very dirty atm since I'm experimenting..

 

The 'xChanged' function sends the new position to the server, and 'posChanged' reads the data that gets sent from the server.

var game = new Phaser.Game(window.innerWidth, window.innerHeight, Phaser.AUTO, '', {	preload: preload,	create: create,	update: update});function preload() {	game.load.spritesheet('player', 'img/ally_sprite.png', 64, 64);	game.load.image('boss', 'img/boss.png');	game.load.image('bullet', 'img/bullet.png');}var player, boss, cursors, x, y, z, xTouch, yTouch, fireButton, bullets, bulletTime = 50, text, bulletsCount = 30;var players = {};var io = io.connect('http://192.168.0.13:3000');// server ipvar xPos, yPos, label, sock;function create() {    game.renderer.clearBeforeRender = false;    game.renderer.roundPixels = true;	game.physics.startSystem(Phaser.Physics.ARCADE);	player = game.add.sprite(game.world.centerX, game.world.centerY, 'player');	player.anchor.setTo(.5,.5);	player.animations.add('fly'); 	player.animations.play('fly', 10, true);	game.physics.enable(player, Phaser.Physics.ARCADE);	player.enableBody = true;	player.body.collideWorldBounds = true;    // new player           var pos = JSON.stringify({         player: io.socket.sessionid,        x: game.world.centerX,        y: game.world.centerY,        angle: 0    });            socket.emit('newPos', pos);    	boss = game.add.sprite(game.centerX, game.centerY, 'boss');	boss.enableBody = true;	boss.physicsBodyType = Phaser.Physics.ARCADE;	bullets = game.add.group();	bullets.enableBody = true;    bullets.physicsBodyType = Phaser.Physics.ARCADE;    bullets.createMultiple(30, 'bullet');    bullets.setAll('anchor.x', 0.5);    bullets.setAll('anchor.y', 1);    bullets.setAll('outOfBoundsKill', true);	    game.stage.backgroundColor = '#ccc';    game.input.addPointer();    fireButton = game.input.pointer1;    if(isMobile.any) {        if(gyro.hasFeature('devicemotion')) {        	console.log('gyrojs loaded');        	if(gyro.getFeatures().length > 0) {    	    	gyro.frequency = 10;    	    	gyro.startTracking(function(o) {          		    	var anglePlayer = Math.atan2(o.y, o.x);    		    	angleRadians = anglePlayer * Math.PI/180;    		    	anglePlayer *= 180/Math.PI;    		    	anglePlayer = 180 - anglePlayer;    		    	player.angle = game.math.wrapAngle(anglePlayer, false);    				if(fireButton.isDown) {    					fire();    				}    	    		if(o.z < 9.5 || o.z > 10) {    		    		player.body.velocity.x -= o.x * 20;    		    		player.body.velocity.y += o.y * 20;    	    		} else {    	    			player.angle = 0;    	    		}                                      console.log(player.body.velocity.x);                    // Send new position to server                    var newPos = JSON.stringify({                        player: io.socket.sessionid,                        x: player.body.velocity.x,                        y: player.body.velocity.y,                        angle: player.angle                    });                    socket.emit('newPos', newPos);        	    });        	}        }        else {        	// fallback als gyro.js niet werkt..        	console.log('gyrojs not loaded');        	window.addEventListener('devicemotion', function(event) {        		x = event.accelerationIncludingGravity.x;        		y = event.accelerationIncludingGravity.y;        		z = event.accelerationIncludingGravity.z;        		var anglePlayer = Math.atan2(y, x);        		anglePlayer *= 180/Math.PI;    		    anglePlayer = 180 - anglePlayer;    		    player.angle = game.math.wrapAngle(anglePlayer, false);    		    if(fireButton.isDown) {    		    	fire();    		    }    		    if(z < 9.5 || z > 10) {    		    	player.body.velocity.x -= x * 40;     		    	player.body.velocity.y += y * 40;    		    } else {    		    	player.angle = 0;    		    }                // Send new position to server                var newPos = JSON.stringify({                    player: io.socket.sessionid,                    x: player.body.velocity.x,                    y: player.body.velocity.y,                    angle: player.angle                });                socket.emit('newPos', newPos);    		    var interval = 10;        	});        }    }    else {        // Niet mobiel, bewegingen omzetten in keys        console.log('Niet Mobiel');        cursors = game.input.keyboard.createCursorKeys();    }    text = game.add.text(game.world.centerX, 50, "Bullets: 30", {    	font: "65px Arial",        fill: "#000000",        align: "center"    });    text.anchor.setTo(0.5, 0.5);    // Add new players to the screen    socket.on('newPlayerwithPos', function(data) {        var obj = JSON.parse(data);        var xNew = obj.x;        var yNew = obj.y;                console.log(xNew, yNew);                var p = game.add.sprite(xNew, yNew, 'player');        p.anchor.setTo(.5,.5);        p.animations.add('fly');         p.animations.play('fly', 10, true);        game.physics.enable(p, Phaser.Physics.ARCADE);        p.enableBody = true;        p.body.collideWorldBounds = true;    });}function update() {	player.body.velocity.setTo(0,0);    if(!isMobile.any) {        if(cursors.left.isDown) {            player.body.velocity.x -= 40;        }        else if(cursors.right.isDown) {                    player.body.velocity.x += 40;            var newPos = JSON.stringify({                x: player.x,                player: io.socket.sessionid            });        }        if(game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR).isDown) {            fire();        }    }    // Get new positions from all other players    socket.on('updatePos', function(data) {        for(var playerData in data) {            // update array of players            var player = {};            player.name = data[playerData].sessionID;            player.x = data[playerData].x;            player.y = data[playerData].y;            player.angle = data[playerData].angle;            players[player.name] = player;        }    });    // loop through all players and draw sprites    for(var playerData in players) {        var p = '';        var playerX = players[playerData].x;        var playerY = players[playerData].y;        var playerAngle = players[playerData].angle;        var p = game.add.sprite(playerX, playerY, 'player');        game.physics.enable(p, Phaser.Physics.ARCADE);        p.enableBody = true;        p.body.velocity.x -= playerX;        p.body.velocity..y += playerY;        p.angle = playerAngle;    }}function fire() {	if(game.time.now > bulletTime) {		bullet = bullets.getFirstExists(false);		if(bullet) {			bullet.reset(player.body.x + 32, player.body.y + 32);			bullet.rotation = player.rotation;            game.physics.arcade.velocityFromRotation(player.rotation, 400, bullet.body.velocity);			bulletTime = game.time.now + 50;			// Update bullet counter			bulletsCount --;			text.setText("Bullets: " + bulletsCount);		}	}}
Link to post
Share on other sites

I have updated the code which I have so far.

 

Now the player gets drawn on the screen, but the problem is, it doesn't update the location, it draws a new player everytime.

 

I put all the players with their coordinates in a array 'players'. 

What I think I should do is: 

1. empty the array everytime so no duplicates are shown on the screen.

2. update the coordinates of each player in the array 'players'.

 

Don't really know how to accomplish this right now :/

Link to post
Share on other sites

I'm doing this same thing atm (but my backend is .NET with SignalR but the idea is the same). You don't want to empty the players array each time. You just want to find said player via some key and update it's x/y.

// Get new positions from all other playerssocket.on('updatePos', function(data) {for(var playerData in data) {// update array of playersvar player = {};player.name = data[playerData].sessionID;player.x = data[playerData].x;player.y = data[playerData].y;player.angle = data[playerData].angle;players[player.name] = player;}});

I've been programming for a long time but not with javascript. I'm getting used to javascript but what I'm reading here is that you are making a new player and adding it to your array on each update position. var player = {}; is making a new object. Then you fill that object in with properties and information. Then you assign it to the players array. You don't want to do that I don't think. You want to find the existing player in your players array and modify the found player.

var sessionID = data[playerData].sessionID;players[sessionID].x = data[playerData].x//etc

I don't see you adding remote clients to the players array outside of the update position message. That's not where you would want to add them to the array. You want a separate message for doing that like "msgRemoteClient", where you add it to the array, then x times a second you send from server to clients the position and update the existing client in the array. Then you have a "msgRemoteClientDisconnect" message to remove them from the array.

Link to post
Share on other sites

What I want to do is generate a new sprite dynamically if a player joins the game.

 

That's not difficult at all, but I want to access that certain sprite at a later stage to update it's coordinates.

So I would have to create a dynamically generated variable name for that sprite so I can access the sprite by using the variable name.

 

I have never worked with dynamic variables in Javascript, so I don't really know how to use them..

 

What I have got so far is this:

 

function newPlayer(player) {    // new player variables    var newSession = player.session;    var newPlayerNick = player.nickname;    var newPlayerX = player.x;    var newPlayerY = player.y;    // 'spawn' new player    window[newSession] = game.add.sprite(newPlayerX, newPlayerY, 'player');    // configurations for new player    window[newSession].anchor.setTo(.5,.5);    window[newSession].animations.add('fly');     window[newSession].animations.play('fly', 10, true);    game.physics.enable(window[newSession], Phaser.Physics.ARCADE);    window[newSession].enableBody = true;    window[newSession].body.collideWorldBounds = true;    // save sessionid from player in 'players' array    players.push(newSession);} 
Link to post
Share on other sites

Here is what I do:

 

// when we first connect we get all clients already connectednetwork.client.responseAddRemoteClients = function (clients) {    for (var c in clients) {        if (clients.hasOwnProperty(c)) {            var id = clients[c].ID;            var obj = clients[c];            AddRemoteClient(id, obj.X, obj.Y);        }    }}// when we are already connected and a client connectsnetwork.client.responseAddRemoteClient = function (clientID, x, y) {    AddRemoteClient(clientID, x, y);}// helper functionfunction AddRemoteClient(clientID, x, y) {    remoteClients[clientID] = game.add.sprite(x, y, 'player');    // store 4 snapshots    remoteClients[clientID].snapshots = new Array();    remoteClients[clientID].anchor.setTo(0.5, 0.5);    remoteClients[clientID].scale = { x: 2, y: 2 };    remoteClients[clientID].animations.add('north.stand', [1], 6, true);    remoteClients[clientID].animations.add('north.walk', [0, 1, 2], 6, true);    remoteClients[clientID].animations.add('east.stand', [4], 6, true);    remoteClients[clientID].animations.add('east.walk', [3, 4, 5], 6, true);    remoteClients[clientID].animations.add('south.stand', [7], 6, true);    remoteClients[clientID].animations.add('south.walk', [6, 7, 8], 6, true);    remoteClients[clientID].animations.add('west.stand', [10], 6, true);    remoteClients[clientID].animations.add('west.walk', [9, 10, 11], 6, true);    remoteClients[clientID].animations.play(playerDir.concat('.stand'));    // add the player to the layer between layer2 and layer3    actorLayer.add(remoteClients[clientID]);}

 

 

Note that  you'll need one function for adding 1 client (when you are already connected and clients connect). Then you'll need another that adds all already connected clients for when you first connect you'll need to see all clients who are already connected.

Link to post
Share on other sites

I update the direct x/y value and not the velocity. Since that remote client is moving via it's velocity on it's PC and in my system clients are sending their position to the server 5 times a second and the server sends out positions of all clients to all clients 5 times a second, the client itself is using physics so all other clients need to do is set x/y directly.

remoteClient[clientID].x = xremoteClient[clientID].y = y

Getting this working is a big step in the right direction and I would advice doing that before trying what I say next. In fact I'm trying what I'm saying next and having some issues so maybe when you get there we can help each other out and possible release some kind of multiplayer networking that makes this easier for the next guy :)

 

 

[More advanced but really needed to be smooth]

After you get the x/y setting x times a second you'll notice the remote clients jump/teleport. It's not smooth. You could increase the frequency but that takes up bandwidth and you'll find that it still looks jerky no matter what. The common way to make smooth movement of remote clients is to actually delay the drawing of them by x ms (I'm using 400ms). What this means is that every remote client (even yourself on other PC's) will be x ms in the past. However, since everyone is like this it's fine. If your game has shooting then you'll want higher frequency (20 times a second) but still use this delayed method.

 

So now we know we want to delay the drawing at it's position by x ms, but that alone won't give us smooth movement, but what it does give us is a chance to store 3-4 position updates that the server gave us so that we can use 2 positions and interpolate between them to get a value. Every render frame we go back x ms, find the 2 positions that surround this delayed time, interpolate, and use the interpolated position. In order to do this we need to store what we'll call a "snapshot" which stores a timestamp along with a position that we got from the server. The timestamp is needed because each render we'll take (now - x ms) to get a time in the past. Then we'll go through our snapshots looking for the 2 snapshots that are directly behind and directly in front of our (now - x ms).

 

 

This is all described in the Entity Interpolation section on this site: https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking

 

This is how valve does it's multiplayer. This is a good read all the way through on multiplayer coding.

 

 

 

My code does this, but it has some issues in that it's creating a back and forth movement, but I just have to debug it and would love some extra eyes on it to see why it's not doing what we'd expect.

 

network.client.reponseUpdatePositionRemoteClient = function (clients) {    for (var c in clients) {        if (clients.hasOwnProperty(c)) {            var id = clients[c].ID;            var obj = clients[c];            // the server might send this before we've added them to our container so make sure this id exists in our container            if (id in remoteClients) {                // don't update ourselves                if (localID != id) {                    remoteClients[id].snapshots.unshift({ timestamp: new Date().getTime(), position: new Vec2(obj.X, obj.Y) });                    if (remoteClients[id].snapshots.length > 4) {                        remoteClients[id].snapshots.pop();                    }                }            }        }       }}function interpolateClient(currentSnapshot, nextSnapshot, factor) {    return currentSnapshot.position.lerp(nextSnapshot.position, factor);}function applyNewPosition(actor, pos) {    actor.x = pos.x;    actor.y = pos.y;}function getInterpolationFactor(currentSnapshot, nextSnapshot, delayTime, currentTime) {    var total = currentSnapshot.timestamp - nextSnapshot.timestamp;    var perc = currentTime - delayTime - nextSnapshot.timestamp;    return perc / total;}function updateRemoteClients() {    var currentTime = new Date().getTime();    var renderLatency = 400    var delayTime = currentTime - renderLatency;    console.log("======================================");    // loop over all our remote clients and find the interpolated position to draw them at    for (var c in remoteClients) {        var actor = remoteClients[c];                for (var i = 0; i < actor.snapshots.length; i++) {            // make sure we don't go out of bounds with our checks            if (i + 1 < actor.snapshots.length) {                var currentSnapshot = actor.snapshots[i];                var nextSnapshot = actor.snapshots[i + 1];                var diff = delayTime - currentSnapshot.timestamp;                var nextDiff = delayTime - nextSnapshot.timestamp;                if(diff > 0 && nextDiff < 0)                    continue;                var factor = getInterpolationFactor(currentSnapshot, nextSnapshot, renderLatency, currentTime);                var newPos = interpolateClient(currentSnapshot, nextSnapshot, factor);                applyNewPosition(actor, newPos);                // logging                console.log("diff = ".concat(diff));                console.log("nextDiff = ".concat(nextDiff));                console.log("factor = ".concat(factor));                console.log("newPos = ".concat(newPos));            }        }    }    console.log("======================================");}function render() {    // when it's time to actually draw, then we need to find our interpolated location for our remote clients    updateRemoteClients();}
Link to post
Share on other sites

Rather than adjusting the velocity you will probably want to just set the x value to that of the one received from the server so: remoteClients[clientID].body.x = 40;

 

I have just completed a project with socket.io and phaser, and if you are trying to keep the players updated like that in realtime you will notice quite a bit of lag because web sockets aren't great for this sort of thing. Not when testing locally, but once online. One way to smooth out the laggy motion is to use tweens to move the remote players to their new positions each time an update is received. 

 

The way I do this is as follows:

  1. When a player sends an update of their position to the server, they also send with it the time elapsed since they last sent an update.
  2. The server then relays this new position and timestamp to the other players.
  3. This update is received client side, and a new tween is added to move the remote player to their new position, and we set the tween time equal to the time elapsed sent with the update. Make sense?

Even when using tweens you will experience odd motion every now and then. One way you can further improve it is to create a small buffer to hold the positions updates. So no each remoteClient would contain a states array, or something similar, and now on receiving and update instead of moving the remote player straight away, you can add the update to their buffer. Next you wait until the buffer is above a certain size before removing the first state in the queue, and creating a tween using it. 

 

​This does actually induce a slight bit of lag because you are essentially playing back a very short recording of the remote player's movements, but it helps to smooth out the problems caused by web socket's limitation. 

 

Hope that helps a bit and wasn't too confusing. I will be happy to release my code for you to have a look at once the university assignment I created it for has been marked :)

Link to post
Share on other sites

@Zef I would love to see what you have. I have the theory down on how to do this via Valve's article, but if you want to see what happens with my implementation just go http://www.your-world-game.com/ and open up 2 browsers and use the cursor keys to move around. You'll see the rubberbanding effect that my code is giving me. I'm having issues figuring out why it's doing this though. It sort of works, but I must be missing something to cause this strange effect.

Link to post
Share on other sites

I seem to have the moving of a player in another client working, however it looks like it is adding a new sprite instead of updating the x and y my code is supposed to do.
 
Really weird, you can see it in the screenshot below..

Also, whenever I move just a little with the client on the left, the client on the right has much greater x and y increase..
 
p.s. don't mind the error on the left, I'm having some issues with syncing the already online players when a new player joins.. 

yvOX8yD.png

 

My current codes are:

server.js --> http://jsbin.com/xomuqoro/1/

game.js --> http://jsbin.com/hiduteso/1/

Link to post
Share on other sites

@rpiller Oo yes looks like you are very close but that is a strange effect. Can't see anything glaringly obvious in your code, but it looks as though every 4 states or so teleports forward then corrects itself. That suggests to me that the error is probably lurking in your code that selects the states to interpolate between. Could be completely wrong though!

 

Unfortunately I can't post my code just yet until the project is marked, but I promise I will do once it is done. Marks should be back very soon, but for now I'll try to give a more detailed explanation.

 

My implementation is actually a bit different to yours. its not perfect, but results in smooth movement most of the time, unless a client has a very poor connection. I wish I had read through that Valve doc before as it would have saved me a lot of time trying to work out how to do this myself haha! I should also note that there are no physics interactions between the players in my game. It is essentially a side scrolling racing game where players have to reach the end of the level first.

 

So like yours each client updates the server with their position x times per second, in my case I think it is 20 but I can't remember of the top of my head. This update contains their id, their x and y, and a timestamp generated by them client side. The server than relays this state to the other clients.

 

On receiving a state update from the server, the corresponding remote player has the new state added to their state queue. Now unlike your implementation I have a min queue size, and a max queue size. I think my minimum queue size is 10 and maximum is 30. States continue to be added to the queue until it becomes bigger than the max size 30, at which point I drop the 20 oldest states, as for whatever reason the client is lagging too far behind the remote player. This all seems a bit odd now, but it should make sense once I explain how the remote players are actually updated.

 

Now in each remote player's update function i first check to see if their state queue is bigger than its minimum size. If it is I will go on to update the players position if its not, we just wait for it to fill up more. This is essentially the equivalent of your 400 milliseconds of induced lag. Presuming the queue is long enough I then pop enough states to bring the queue length back down to 10. So if there were 14 states in the queue, I would pop the 4 oldest off etc. Again this seems a bit weird and illogical, but I found that often with web sockets you would receive a sudden lump of states in between an update loop. If I had simply popped one state off the queue each time one was received there would be a big jump to the new position. This is the reason for the upper and lower limits of the queue.

 

Now for each of the states that are popped off the queue during the update loop, I create a tween to move the sprite to the positions provided in the state. Each remote player has a previousStateTime variable which holds the timestamp of the last state to be popped and tweened. Using this I then work out how long the tween should take by calculating the difference in timestamps of the previous state and current state. I then take another 5ms off this value just to make sure that the player is moved between positions slightly faster than it really did to prevent it from falling behind.

 

And that is pretty much it. Sorry if its not that clear, I don't have my code to hand so was working from memory. I must admit that this was my first ever experience trying to create a multiplayer game, and I created that solution myself without doing much research or really having a clue what I was doing, so I am sure there are many reasons why it is awful haha. But hey it seems to work! 

 

That said, after reading that valve article I think I will try to see if I can implement their interpolation technique as it seems a lot more logical. 

Link to post
Share on other sites

Ok, so I've noticed some strange stuff. The problem you see in the screenshot above doesn't occur in Firefox.

I think it has something to do with the update rate of the position.

At the moment I send a update to the server everytime a arrow key is pressed, how could i delay that?

 

Also, I've tweaked the code alot. It works fine now, but gets really slow if more than 4 clients are online.

 

Updated codes can be found here:

server.js --> http://jsbin.com/xomuqoro/1/

game.js --> http://jsbin.com/hiduteso/1/

Link to post
Share on other sites

I used setInterval() to fire 5 times a second and inside here I send the position of the local client to the server. This will help make a more consistent transfer rate. In your method if I can spam my arrow key and I could send a lot of data. In fact if I programmatically spam it I could maybe cause lots of issues for your game.

 

You'll want to use an interval on the server as well.

 

With your method how fast I can press the key has a direct impact on how much data is sent out to everyone. That's a little dangerous. Sending from the server to clients and clients to server at a given rate helps reduce this.

Link to post
Share on other sites

Also be careful: the client should not send it's x and y coordinates to the server.  It should only send "up button pressed", for example.

 

If the client sends x and y coordinates to the server it would be easy for the player to send arbitrary x and y coordinates, hacking your game.

Link to post
Share on other sites

I'm very interested in knowing how you are using setInterval() in your fuctions.

Whenever I use it with like an interval of 200ms, it just keeps executing every 0,2s even if the trigger (arrow key) isn't pressed anymore.

Should I just use setTimeout() instead with a delay of 200ms?

Link to post
Share on other sites

Also be careful: the client should not send it's x and y coordinates to the server.  It should only send "up button pressed", for example.

 

If the client sends x and y coordinates to the server it would be easy for the player to send arbitrary x and y coordinates, hacking your game.

 

Different games can do this differently. You can send x/y from client and do validation on the server to prevent cheating. World of Warcraft is one such popular game, along with many others, that have the client send their position to the server. The server can do all sorts of validations to prevent cheating including checking that you aren't moving too fast or inside collision areas.

 

 

Whenever I use it with like an interval of 200ms, it just keeps executing every 0,2s even if the trigger (arrow key) isn't pressed anymore.

 

 

The interval function should always run no matter what. However, what it does inside can be branched off. For example you can keep the last position of your local client in the update function and inside this function if the current position and last position are equal don't send any information to the server. If they've changed, send the information.

Link to post
Share on other sites
  • 1 year later...

I think you people make things very complicated.

 

Update( ) fires at 60Hz.

let's emit position inside the update.

 

emit anything you want... emit speed, position, whatever.

var object= new game.add.blablablaYourSpriteOrWhatever;update(){  socket.emit('updatePositions', {posX: object.x, posY: object.y, speed: object.speed});  }

 I think is just as easy as that. Isn't it?

Link to post
Share on other sites

@Silverium this post seems to be dead now... But anyway firing socket updates 60 times per second is an incredibly bad idea, as it would slaughter performances. Sockets are built on TCP and it's a (relatively) slow protocol. For the client, receiving more than 8-10 sockets per second will have a huge impact on performances (and I'm talking about solid desktop, not even mobiles).

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...