服务端会收到来自客户端的协议,这些协议包括ping协议、窗口变化协议(windowResized)、玩家下线协议(disconnect)、聊天请求、分身请求等等,那么当服务端收到这些协议后,它会怎样处理呢?
当客户端点击play的时候,客户端发起连接,服务端io.on('connection',fun)的回调函数被执行,具体过程可参见该系列的第2篇文章:程序流程
io.on('connection', function (socket) {
……
}
当服务端收到客户端发送的pingcheck协议后,立即返回pongcheck协议,供客户端计算网络延迟时间。
socket.on('pingcheck', function () {
socket.emit('pongcheck');
});
服务端收到窗体大小变化的协议windowResized后,会更新玩家数值,以调整sendUpdates的数据量。
socket.on('windowResized', function (data) {
currentPlayer.screenWidth = data.screenWidth;
currentPlayer.screenHeight = data.screenHeight;
});
当服务端收到disconnect协议,它会把玩家从玩家列表中删除,然后广播playerDisconnect,通知所有客户端该玩家下线。
socket.on('disconnect', function () {
if (util.findIndex(users, currentPlayer.id) > -1)
users.splice(util.findIndex(users, currentPlayer.id), 1);
console.log('[INFO] User ' + currentPlayer.name + ' disconnected!');
socket.broadcast.emit('playerDisconnect', { name: currentPlayer.name });
});
服务端收到聊天协议playerChat后,通过serverSendPlayerChat协议将聊天内容广播给所有客户端。
socket.on('playerChat', function(data) {
var _sender = data.sender.replace(/(<([^>]+)>)/ig, '');
var _message = data.message.replace(/(<([^>]+)>)/ig, '');
if (c.logChat === 1) {
console.log('[CHAT] [' + (new Date()).getHours() + ':' + (new Date()).getMinutes() + '] ' + _sender + ': ' + _message);
}
socket.broadcast.emit('serverSendPlayerChat', {sender: _sender, message: _message.substring(0,35)});
});
玩家点击play按钮,客户端在连接服务端后,会发送respawn协议添加玩家。服务端先把原先小球的干掉(如果有),然后发welcome回给客户端。
socket.on('respawn', function () {
if (util.findIndex(users, currentPlayer.id) > -1)
users.splice(util.findIndex(users, currentPlayer.id), 1);
socket.emit('welcome', currentPlayer);
console.log('[INFO] User ' + currentPlayer.name + ' respawned!');
});
客户端收到welcome协议后,发送gotit协议,服务端做一些判断,比如看看名字是否合法,查一查玩家是否有重复登录。然后服务端广播playerJoin协议,并使用gameSetup协议向客户端发送游戏地图大小的信息。
socket.on('gotit', function (player) {
console.log('[INFO] Player ' + player.name + ' connecting!');
if (util.findIndex(users, player.id) > -1) {
……//已经登录,断开连接
} else if (!util.validNick(player.name)) {
……//名字不合法,断开连接
} else {
……//一些初变量始化
io.emit('playerJoin', { name: currentPlayer.name });
socket.emit('gameSetup', {
gameWidth: c.gameWidth,
gameHeight: c.gameHeight
});
}
});
Pass协议用于管理员认证,暂未发现客户端哪里发送该协议。
socket.on('pass', function(data) {
if (data[0] === c.adminPass) {
console.log('[ADMIN] ' + currentPlayer.name + ' just logged in as an admin!');
socket.emit('serverMSG', 'Welcome back ' + currentPlayer.name);
socket.broadcast.emit('serverMSG', currentPlayer.name + ' just logged in as admin!');
currentPlayer.admin = true;
} else {
console.log('[ADMIN] ' + currentPlayer.name + ' attempted to log in with incorrect password.');
socket.emit('serverMSG', 'Password incorrect, attempt logged.');
// TODO: Actually log incorrect passwords.
}
});
管理员可以踢人,但客户端中暂未发现相关的功能,应该只是一个预留功能。
socket.on('kick', function(data) {
if (currentPlayer.admin) {
var reason = '';
var worked = false;
for (var e = 0; e < users.length; e++) {
if (users[e].name === data[0] && !users[e].admin && !worked) {
if (data.length > 1) {
for (var f = 1; f < data.length; f++) {
if (f === data.length) {
reason = reason + data[f];
}
else {
reason = reason + data[f] + ' ';
}
}
}
if (reason !== '') {
console.log('[ADMIN] User ' + users[e].name + ' kicked successfully by ' + currentPlayer.name + ' for reason ' + reason);
}
else {
console.log('[ADMIN] User ' + users[e].name + ' kicked successfully by ' + currentPlayer.name);
}
socket.emit('serverMSG', 'User ' + users[e].name + ' was kicked by ' + currentPlayer.name);
sockets[users[e].id].emit('kick', reason);
sockets[users[e].id].disconnect();
users.splice(e, 1);
worked = true;
}
}
if (!worked) {
socket.emit('serverMSG', 'Could not locate user or user is an admin.');
}
} else {
console.log('[ADMIN] ' + currentPlayer.name + ' is trying to use -kick but isn\'t an admin.');
socket.emit('serverMSG', 'You are not permitted to use this command.');
}
});
由于玩家实时控制小球移动,每一帧都会更新目标位置,这个协议会频繁调用,故而选用简单的名字,命名为0号协议,以减少传输的数据量。通过该协议更新目标位置,然后记录心跳时间。
// Heartbeat function, update everytime.
socket.on('0', function(target) {
currentPlayer.lastHeartbeat = new Date().getTime();
if (target.x !== currentPlayer.x || target.y !== currentPlayer.y) {
currentPlayer.target = target;
}
});
发射massfood的操作由1号协议通信,玩家发射massfood就发送一条该协议,然后服务端记录起来。
socket.on('1', function() {
// Fire food.
for(var i=0; i= c.defaultPlayerMass + c.fireFood) && c.fireFood > 0) || (currentPlayer.cells[i].mass >= 20 && c.fireFood === 0)){
var masa = 1;
if(c.fireFood > 0)
masa = c.fireFood;
else
masa = currentPlayer.cells[i].mass*0.1;
currentPlayer.cells[i].mass -= masa;
currentPlayer.massTotal -=masa;
massFood.push({
……//massFood的信息
});
}
}
});
玩家点击屏幕中的分身按钮,客户端发送2号协议,服务端收到后做一些判断,然后调用splitCell执行小球分身。
socket.on('2', function(virusCell) {
……
if(currentPlayer.cells.length < c.limitSplit && currentPlayer.massTotal >= c.defaultPlayerMass*2) {
//Split single cell from virus
if(virusCell) {
splitCell(currentPlayer.cells[virusCell]);
}
else {
//Split all cells
if(currentPlayer.cells.length < c.limitSplit && currentPlayer.massTotal >= c.defaultPlayerMass*2) {
var numMax = currentPlayer.cells.length;
for(var d=0; d
splitCell方法如下所示,它将小球分成两个等质量的球。
function splitCell(cell) {
if(cell.mass >= c.defaultPlayerMass*2) {
cell.mass = cell.mass/2;
cell.radius = util.massToRadius(cell.mass);
currentPlayer.cells.push({
mass: cell.mass,
x: cell.x,
y: cell.y,
radius: cell.radius,
speed: 25
});
}
}