I created a webpage using node.js, express js, socket.io, and jquery. It is a simple game wherein players join, give themselves a name, and then move themselves(a square) around a canvas on the page. What I've noticed is: when more people join the game and move around, there is enough lag to make the game unplayable (at only three people connected. Two people doesn't lag it much). I can't figure out if this is server-side lag or client-side (this is my first project dealing with multiplayer). I am doing all of the location calculations on the server, and sending an array of all the player objects back to every socket so that each client can render all of the players. The client only sends input and draws the players.
This is the client side script for the game. This is where I handle the input and rendering.
$(document).ready(function()
{
var socket = io.connect();
var canvas = document.getElementById("canvas_html");
var ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 480;
document.body.appendChild(canvas);
var player =
{
id: '',
is_it: false,
x: canvas.width / 2,
y: canvas.height / 2,
velx: 0,
vely: 0
}
//tell the server to initialize this client as a new player
socket.emit('init_client', player);
var client_player_list = [];
//receive a list of the player objects from the server
socket.on('load_players', function(players)
{
client_player_list = players;
});
var keysDown = {};
addEventListener('keydown', function(e)
{
keysDown[e.keyCode] = true;
}, false);
addEventListener('keyup', function(e)
{
delete keysDown[e.keyCode];
}, false);
//take input from keys and send input to server
var update = function()
{
if(87 in keysDown)//player holding w
socket.emit('up');
if(83 in keysDown)//player holding s
socket.emit('down');
if(65 in keysDown)//player holding a
socket.emit('left');
if(68 in keysDown)//player holding d
socket.emit('right');
if(74 in keysDown)//player holding j
socket.emit('tag');
};
//render all players, update players when server updates
var render = function()
{
//ctx.clearColor = "rgba(0, 0, 0, .3)";
ctx.clearRect( 0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#079641";
ctx.textAlign = 'center';
//loop through the players array and render each one
for(var i = 0; i < client_player_list.length; i++)
{
//if the player is_it, render them as red
if(client_player_list[i].is_it)
{
ctx.fillStyle = "#8F0E0E";
ctx.fillRect(client_player_list[i].x, client_player_list[i].y, 20, 20);
//draw the players name above them
ctx.fillStyle = "#FFF";
ctx.font="15px Arial";
ctx.fillText(client_player_list[i].name, client_player_list[i].x + 8, client_player_list[i].y - 3);
continue;
}
//if the player !is_it, render them as green
ctx.fillStyle = "#079641";
ctx.fillRect(client_player_list[i].x, client_player_list[i].y, 20, 20);
//draw the players name above them
ctx.fillStyle = "#FFF";
ctx.font="15px Arial";
ctx.fillText(client_player_list[i].name, client_player_list[i].x + 8, client_player_list[i].y - 3);
}
//when the server sends an update, replace the current players array with the one that the server just sent
socket.on('sv_update', function(players)
{
client_player_list = players;
});
};
//main loop
var main = function()
{
setInterval(function()
{
update();
render();
}, 1000/60);
};
main();
//trigger the disconnect event when a page refreshes or unloads
$(window).bind('beforeunload', function()
{
socket.emit('disconnect');
});
});
Below, I have included the app.js file (the server-side script) for my game. I am not sure exactly where the problem is, so I figured I'd just post the whole thing.
var express = require('express');
var http = require('http');
var io = require('socket.io', { rememberTransport: false, transports: ['WebSocket', 'Flash Socket', 'AJAX long-polling'] });
var app = express();
var server = http.createServer(app);
server.listen(8080);
app.use(express.static('public'));
io = io.listen(server);
//Declare variables for working with the client-side
var player_speed = 5;
var player_size = 20;
var vel_increment = 0.5;
var canvas_height = 480;
var canvas_width = 512;
//Declare list of players connected
var players = [];
io.sockets.on('connection', function(socket)
{
var socket_id = socket.id;
var player_index, player_exists = false;
var this_player;
var check_bounds = function()
{
//Keep player in the canvas
if(this_player.y < 0)
this_player.y = 0;
if(this_player.y + player_size > canvas_height)
this_player.y = canvas_height - player_size;
if(this_player.x < 0)
this_player.x = 0;
if(this_player.x + player_size > canvas_width)
this_player.x = canvas_width - player_size;
//Keep velocity between -5 and 5
if(this_player.vely > player_speed)
this_player.vely = player_speed;
if(this_player.velx > player_speed)
this_player.velx = player_speed;
if(this_player.vely < -player_speed)
this_player.vely = -player_speed;
if(this_player.velx < -player_speed)
this_player.velx = -player_speed;
};
var sv_update = function()
{
io.sockets.emit('sv_update', players);
if(player_exists)
{
if(players.length == 1)
this_player.is_it = true;
check_bounds();
}
};
//When a client connects, add them to players[]
//Then update all clients
socket.on('init_client', function(player)
{
player.id = socket.id;
players.push(player);
for(var i = 0; i < players.length; i++)
if(players[i].id == socket_id)
player_index = i;
player_exists = true;
this_player = players[player_index];
sv_update();
socket.emit('load_players', players);
console.log(players);
});
//====================CHAT==========================//
var address = socket.request.connection.remoteAddress;
socket.on('new user', function(data, callback)
{
if(player_exists)
{
this_player.name = data;
console.log(address + " has connected as '" + data + "'.");
callback();
}
});
socket.on('send message', function(data)
{
io.sockets.emit('broadcast', this_player.name, data);
});
//====================CHAT==========================//
//if player is_it and is within another player, hitting 'j' will make the other player is_it.
socket.on('tag', function()
{
if(player_exists)
{
for(var i = 0; i < players.length; i++)
{
if((this_player.x + player_size >= players[i].x && this_player.x + player_size <= players[i].x + player_size )||
(this_player.x <= players[i].x + player_size && this_player.x + player_size >= players[i].x))
if((this_player.y + player_size >= players[i].y && this_player.y + player_size <= players[i].y + player_size )||
(this_player.y <= players[i].y + player_size && this_player.y + player_size >= players[i].y))
{
if(this_player.is_it)
{
this_player.is_it = false;
players[i].is_it = true;
}
}
}
sv_update();
}
});
//Gather key input from users...
socket.on('up', function()
{
if(player_exists)
{
this_player.y -= player_speed;
sv_update();
}
});
//Gather key input from users...
socket.on('down', function()
{
if(player_exists)
{
this_player.y += player_speed;
sv_update();
}
});
//Gather key input from users...
socket.on('left', function()
{
if(player_exists)
{
this_player.x -= player_speed;
sv_update();
}
});
//Gather key input from users...
socket.on('right', function()
{
if(player_exists)
{
this_player.x += player_speed;
sv_update();
}
});
//When a player disconnects, remove them from players[]
//Then update all clients
socket.on('disconnect', function()
{
for(var i = 0; i < players.length; i++)
{
if(players[i].id == socket.id)
{
players.splice(i, 1);
}
}
sv_update();
});
});
--------------------------------------------------------- EDIT ------------------------------------------------------------------ I took Ruslanas Balčiūnas's suggestion for moving the sv_update handler in the client out of the render function. This stops the player from lagging, but the new problem is that the server doesn't send enough updates for all the players to move fluidly on any given client. Each client seems to be running smoothly for just themselves, but the other clients see them as choppy/lagging.
Here is the updated code:
CLIENT:
$(document).ready(function()
{
var socket = io.connect();
var canvas = document.getElementById("canvas_html");
var ctx = canvas.getContext("2d");
canvas.width = 512;
canvas.height = 480;
document.body.appendChild(canvas);
var player =
{
id: '',
name: '',
is_it: false,
x: canvas.width / 2,
y: canvas.height / 2,
velx: 0,
vely: 0
};
var client_player_list = [];
socket.on('load_players', function(players)
{
client_player_list = players;
});
var keysDown = {};
addEventListener('keydown', function(e)
{
keysDown[e.keyCode] = true;
}, false);
addEventListener('keyup', function(e)
{
delete keysDown[e.keyCode];
}, false);
//take input from keys and send input to server
var update = function()
{
if(87 in keysDown)//player holding w
socket.emit('input', 'up');
if(83 in keysDown)//player holding s
socket.emit('input', 'down');
if(65 in keysDown)//player holding a
socket.emit('input', 'left');
if(68 in keysDown)//player holding d
socket.emit('input', 'right');
if(74 in keysDown)
socket.emit('input', 'tag');
};
//render all players, update players when server updates
var render = function()
{
//ctx.clearColor = "rgba(0, 0, 0, .3)";
ctx.clearRect( 0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#079641";
ctx.textAlign = 'center';
for(var i = 0; i < client_player_list.length; i++)
{
if(client_player_list[i].is_it)
ctx.fillStyle = "#8F0E0E";
else
ctx.fillStyle = "#079641";
ctx.fillRect(client_player_list[i].x, client_player_list[i].y, 25, 25);
ctx.fillStyle = "#FFF";
ctx.font="15px Arial";
ctx.fillText(client_player_list[i].name, client_player_list[i].x + 8, client_player_list[i].y - 3);
}
};
socket.on('sv_update', function(players)
{
client_player_list = players;
});
//main loop
var main = function()
{
setInterval(function()
{
update();
render();
}, 1000/60);
};
main();
//---- Chat stuff ----
var toggle = 1;
$('#users').fadeIn(1000);
$('#name_field').focus();
$('#user_form').submit(function(e)
{
console.log('ezpz');
e.preventDefault();
socket.emit('init_client', player, $('#name_field').val(), function()
{
$('#users').fadeOut('slow');
window.setTimeout(function(){$('#chat').fadeIn('slow');$('#canvas_html').fadeIn('slow');$('#info').fadeIn('slow')}, 1000);
});
});
$('#desk').submit(function(e)
{
e.preventDefault();
socket.emit('send message', $('#message').val());
$('#message').val('');
});
socket.on('broadcast', function(name, data)
{
$('#message_window').append('<p class="p' + toggle + '">' + name + ": " + data + '</p>');
$('#message_window')[0].scrollTop = $('#message_window')[0].scrollHeight;
if(toggle == 1)
toggle = 2;
else
toggle = 1;
});
//trigger the disconnect event when a page refreshes or unloads
$(window).bind('beforeunload', function()
{
socket.emit('disconnect');
});
});
SERVER:
var express = require('express');
var http = require('http');
var io = require('socket.io', { rememberTransport: false, transports: ['WebSocket', 'Flash Socket', 'AJAX long-polling'] });
var app = express();
var server = http.createServer(app);
server.listen(8080);
app.use(express.static('public'));
io = io.listen(server);
//Declare variables for working with the client-side
var player_speed = 5;
var player_size = 25;
var canvas_height = 480;
var canvas_width = 512;
//Declare list of players connected
var players = [];
io.sockets.on('connection', function(socket)
{
var socket_id = socket.id;
var player_index, this_player, player_exists = false;
//When a client connects, add them to players[]
//Then update all clients
socket.on('init_client', function(player, name, callback)
{
player.id = socket.id;
players.push(player);
for(var i = 0; i < players.length; i++)
if(players[i].id == socket_id)
player_index = i;
player_exists = true;
this_player = players[player_index];
this_player.name = name;
callback();
sv_update();
socket.emit('load_players', players);
});
socket.on('send message', function(data)
{
io.sockets.emit('broadcast', this_player.name, data);
});
//Gather key input from users...
socket.on('input', function(key)
{
if(player_exists)
{
if(key == 'up')
this_player.y -= player_speed;
else if(key == 'down')
this_player.y += player_speed;
else if(key == 'left')
this_player.x -= player_speed;
else if(key == 'right')
this_player.x += player_speed;
else if(key == 'tag')
for(var i = 0; i < players.length; i++)
if(can_tag(players[i]))
{
this_player.is_it = false;
players[i].is_it = true;
}
sv_update();
}
});
//When a player disconnects, remove them from players[]
//Then update all clients
socket.on('disconnect', function()
{
for(var i = 0; i < players.length; i++)
if(players[i].id == socket.id)
players.splice(i, 1);
sv_update();
});
var check_bounds = function()
{
//Keep player in the canvas
if(this_player.y < 0)
this_player.y = 0;
if(this_player.y + player_size > canvas_height)
this_player.y = canvas_height - player_size;
if(this_player.x < 0)
this_player.x = 0;
if(this_player.x + player_size > canvas_width)
this_player.x = canvas_width - player_size;
};
var sv_update = function()
{
io.sockets.emit('sv_update', players);
if(player_exists)
{
if(players.length == 1)
this_player.is_it = true;
check_bounds();
}
};
var can_tag = function(target)
{
if(this_player.x < target.x + player_size && this_player.x + player_size > target.x && this_player.y < target.y + player_size && player_size + this_player.y > target.y && this_player.is_it)
return true;
}
sv_update();
});
Your help would be greatly appreciated :)