Spy vs Spy on the canvas and node.js



Once decided, my brother (brdsoft) to create a browser game. Of experience creating games we had, so the game had to be simple. A little after some discussion, we decided to make a copy of the NES game of Spy vs Spy with multiplayer.

This article will consist of two parts:
1. General information and server implementation
2. The client implementation

Selected tools


The server part:
Node.js (+expansion socket.io, mysql, node-v8-clone) to implement all game mechanics (hereinafter — the server).
MySQL to store the users and maps.
PHP for user registration, obfuscation, client-side js file, interact with the API social. networks

Client part:
JavaScript to implement client side logic (hereinafter — client)
Canvas for painting

Basic requirements


the
    the
  • Performance in all browsers that support canvas
  • the
  • Implementation of all game interactions on the server (to prevent cheating)
  • the
  • Ability to embed a game on social network
  • the
  • Multiplayer
  • the
  • Minimal traffic

Server


Describe the registration process in the game don't see the point. The only important point: after registration, each user gets a unique token (token is stored in cookies), which in the future will be used to identify the user by the server.

In the interaction of the client and server can identify the main events:
The client is connected
The client received the message (the server sent the message)
The server received the message (the client sent the message)
The client disconnected

All in good time.

The server listens to a port and waits 55705 connection:
the
var io = require('/usr/lib/node_modules/socket.io').listen(55705, {log: false});
var clone = require('/usr/lib/node_modules/node-v8-clone').clone;
var db = require('/usr/lib/node_modules/mysql').createConnection({
host : 'host',
user : 'user',
password : 'password',
database : 'database'
});
db.connect();

io.sockets.on('connection', function (socket){
//Handler for new connections
});

When a client creates a new User object, and the socket hung events:
the
var users = {};
io.sockets.on('connection', function (socket){
users[socket.id] = new User(socket);
socket.on('auth', users[socket.id].auth);
socket.on('createGame', createGame); //Command to create a new game (this is one of the many teams sent from client)
//Various game teams
//...
socket.on('disconnect', users[socket.id].disconnect);
});

Socket.io there are two basic methods for data exchange between server and client: emit to send the message, and on reception.

Here is the connection from the client side:
the
 socket = io.connect('http://spyvsspy.ru:55705');
socket.on('connect', function() {
socket.emit('auth', {token: token});
});

token is taken from the cookie and sent immediately after the connection for authentication. Function auth and disconnect are methods of the User class. In simplified form, the User class looks like this:
the
function user(socket)
{
this.socket = socket;
this.profile = {}; //a Profile filled after a request to the database
this.isAuthorized = false;
this.auth = function(data)
{
//...Validation of data and data.token
//Query the users table
db.query("SELECT*FROM `users` WHERE `token` = '"+data.token+"'", function(err, rows){
if (rows[0])
{
for(var i in users)
{
if (users[i].profile.id == rows[0].id) //If the same user connects twice
{
users[socket.id].socket.disconnect(); //Disable it
return;
}
}
users[socket.id].profile = rows[0];
users[socket.id].isAuthorized = true;
users[socket.id].socket.join('main'); //Connect the socket to the main room (needed for chat)
users[socket.id].socket.emit('auth', {login: users[socket.id].profile.login rating: users[socket.id].profile.rating socId: users[socket.id].profile.soc_id}); //Pass some profile data to the client
return;
}
users[socket.id].socket.disconnect();
});
}

this.disconnect = function() //Function is called when the client disconnects, including when a forced disconnect
{
if (this.gameId) //Disable the player
{
games[this.gameId].removeUser(socket.id);
}
delete users[socket.id]; //Delete user
}
}

I described the process of connecting and disconnecting customers. Everything else is game mechanics. When developing the game we tried to impose on the server to calculate most of the game events. So to avoid cheating and to compensate for the high ping some players. Here is a list of all game events on the server:
socket.on('joinGame', joinGame); socket.on('startGame', startGame); socket.on('chatReceive', chatReceive); socket.on('openAttr', openAttr); socket.on('toggleDoor', toggleDoor); socket.on('goDoor', goDoor); socket.on('moveTo', moveTo); socket.on('exitGame', exitGame); socket.on('setMaxPlayers', setMaxPlayers); socket.on('kick', kick); socket.on('getGames', getGames); socket.on('setTool', setTool); socket.on('hit', hit); socket.on('chooseTool', chooseTool);
createGame — the command to create a new game. Is returned to the client id of the created game. Games as users are stored in "object-array". Every game there is until there is at least one user. Getting the message about the creation of the game, the client sends joinGame. From this moment the game becomes visible to other players. To start game — startGame, to exit game exitGame.

chatReceive message in the chat. The game has a General chat and gaming. Each game created has its own roomId which is substituted when the message is sent:
the
function chatSend(room, message, level, from)
{
io.sockets.in(room).emit('chatReceive', {message: message, level: level, from: from});
}

The ability to create rooms ("rooms") — a very useful feature of the socket.io.

openAttr — sent when a customer clicked on any locker and approached him. The function execution result openAttr, the player receives the contents of the exploding locker or if the locker was booby-trapped. Locker mined if the player was holding the bomb. The contents of the player's inventory is stored on the server. A subject that is in the hands is also stored on the server. The client only renders everything and gives orders.

toggleDoor — open/close the door. goDoor — go through the door. The server returns either death or information about the new room.

moveTo — move around the room. Information about movement is transmitted to all spies that are in the same room. In addition to the displacements transmitted data on the subject in hand, about with doors and furniture, laughter, shock, death. To save traffic data on the movements are transmitted only in the time of the click. But thanks to the simple interpolation, the server always knows where in the room the player was in, and if in motion, the room will become another spy, he will see a running opponent.
Source code for moveTo example
function isFloor(x, y)
{
return (y > 96) && (y < 184) && (x > 192 - y) && (x < 160 + y);
}

function moveTo(data)
{
var userId = this.id;
if (!users[userId].isAuthorized)
return;
if (users[userId].locked)
return;
var gameId = users[userId].gameId;
if (!games[gameId])
return;
if (games[gameId].status != 'game')
return;
if (!isFloor(data.x data.y) || !isFloor(data.prevX, data.prevY))
return;

var roomId = users[userId].roomId;
var room = games[gameId].rooms[roomId];

users[userId].attrDialog = 0;
users[userId].attrAttr = ";

if (users[userId].tool == 'gas')
{
room.mined = 1;
users[userId].stock.gas -= 1;
users[userId].tool = ";
users[userId].laugh();
users[userId].setStock();
users[userId].setTool();
return;
}

users[userId].prevX = data.prevX;
users[userId].prevY = data.prevY;
users[userId].moveToX = data.x;
users[userId].moveToY = data.y;
users[userId].move();

for (var i in games[gameId].users)
{
if (users[i].roomId == users[userId].roomId && i != userId)
users[i].socket.emit('moveSpy', {color: users[userId].color, roomId: users[userId].roomId, x: data.x, y: data.y});
}
}



Interpolation functions:
the
 this.move = function(){
var d = new Date();
this.moveTime = d.getTime(); //Start motion
}

this.getCurrentCoord = function(){
var dx = this.moveToX - this.prevX;
var dy = this.moveToY - this.prevY;
if (Math.abs(dx) < Math.abs(dy))
{
var t = (Math.abs(dy) + 0.41 * Math.abs(dx)) * background 5.714;
}
else
{
var t = (Math.abs(dx) + 0.41 * Math.abs(dy)) * background 5.714;
}
var d = new Date();
var k = (d.getTime() - this.moveTime) / t;
if (k < 0) k = 0;
if (k > 1) k = 1;
return {x: Math.round(this.prevX + k * dx), y: Math.round(this.prevY + k * dy)};
}


I will not describe the remaining events. From their names clear about the purpose.

Some facts

To determine the strength of the players in the game uses the Elo rating without the negative values.
Unlike the original game, we can play five of them on one card.
The game is being developed since November 19, and at the time of writing, had spent approximately 250 man-hours. server.js contains a 1480 lines of code. game.js — 1300 rows.
The company, which owns the rights to the game, knows nothing :)
The game has only 5 maps, but the map editor is ready soon and there will be many.
Card supports up to 8 floors 8x8 each, you can do serious mazes.
You can play one without the rating.
JS client file has been slightly obfuscated to protect from crooks, but anyone can get it whole or read our next post.

Links

game Site
Game Vkontakte
Facebook Group

The rules of the game and management is described in the game. The rules differ little from the original game.
Screenshot


On the development of the client system sprites, the complexities encountered when working with the canvas we will write in the next article. Comments, suggestions are welcome.

Thank you for your attention

PS. The UFO moved the topic to "I'm a PR", will not continue
Article based on information from habrahabr.ru

Комментарии

Популярные сообщения из этого блога

Monitoring PostgreSQL + php-fpm + nginx + disk using Zabbix

Templates ESKD and GOST 7.32 for Lyx 1.6.x

Customize your Google