https://habrahabr.ru/post/330058/- Разработка игр
- Node.JS
- JavaScript
Доброго времени суток, Хабр! Сегодня я покажу каким образом сделать простенький real-time 2D шутер от третьего лица на node.js и phaser.js.
Сразу оговорюсь, что статья не претендует на лучшую статью хабра и рунета, а представленный код служит лишь наглядным примером, а не примером идеального кода. Просто, к сожалению, о использовании связки node.js и phaser.js есть только англоязычные статьи и хочется, по возможности, это исправить.
Итак, писать мы будем простой 2d шутер с видом от третьего лица. В дизайн примера никто особо силы не вкладывал, поэтому в конечно итоге получится, что-то в таком духе:
Вот список технологий, какие мы будем использовать:
— node.js;
— express;
— socket.IO;
— phaser.js
В начале серверная часть:
var express = require('express'); //подключаем экспресс
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http); //подключаем socket.IO
var port = process.env.PORT || 3000; //указываем порт на котором у нас будет работать игра
var players = {}; //переменная со всеми игроками
/* При открытии страницы, открываем новое подключение для обмена сокетами*/
io.on("connection", function(socket) {
console.log('an user connected ' + socket.id); //выводим в консоль node.js инфу о том, что подключился новый пользователь
players[socket.id] = {
"x": Math.floor(Math.random(1) * 2000),
"y": Math.floor(Math.random(1) * 2000),
"live": true,
}; //генерирует параметры нового игрока
io.sockets.emit('add_player', JSON.stringify({
"id": socket.id,
"player": players[socket.id]
})); //создаем нового игрока на клиенте
socket.emit('add_players', JSON.stringify(players));// создаем нового игрока на клиенте(если игроков более одного)
socket.on('player_rotation', function(data) {
io.sockets.emit('player_rotation_update', JSON.stringify({
"id": socket.id,
"value": data
}));
}); // пакет получающий данные о направлении игрока и отправляющий их на клиент для синхронного отображения для всех пользователей
socket.on('player_move', function(data) {
data = JSON.parse(data); //получаем данные какая кнопка нажата
data.x = 0; //создаем новое свойство объекта x и заодно сбрасываем его до нуля, при каждом обновлении сокета
data.y = 0; // аналогично предыдущему, только ось y
/*передаем различные параметры в зависимости от нажатой кнопки*/
switch (data.character) {
case "W":
data.y = -5;
players[data.id].y -= 5;
break;
case "S":
data.y = 5;
players[data.id].y += 5;
break;
case "A":
data.x = -5;
players[data.id].x -= 5;
break;
case "D":
data.x = 5;
players[data.id].x += 5;
break;
}
io.sockets.emit('player_position_update', JSON.stringify(data)); //отправляем данные на клиент
});
socket.on('shots_fired', function(id) { //получаем id стреляющего
io.sockets.emit("player_fire_add", id); //отправляем на клиент вызов функции выстрелов от конкретного пользователся
});
socket.on('player_killed', function (victimId) { //функция при попадания пули в игрока
io.sockets.emit('clean_dead_player', victimId); //чистим поле от проигравших
players[victimId].live = false; //заканчиваем выполнение ряда функций для проигравшего игрока
io.sockets.connected[victimId].emit('gameOver', 'Game Over'); //выводим пользователю в которого попали информацию о проиграше
});
socket.on('disconnect', function() { //убираем с поля отсоединившихся игроков
console.log("an user disconnected " + socket.id);
delete players[socket.id];
io.sockets.emit('player_disconnect', socket.id);
});
});
app.use("/", express.static(__dirname + "/public")); //пути к файлам клиента
app.get("/", function(req, res) {
res.sendFile(__dirname + "/public/index.html"); // главная страница
});
http.listen(port, function() {
console.log('listening on *:' + port); // запуск сервера
});
Теперь клиентская часть:
var width = window.innerWidth; //получаем ширину монитора
var height = window.innerHeight; //получаем высоту монитора
var game = new Phaser.Game(width, height, Phaser.CANVAS, 'phaser-example', { preload: preload, create: create, update: update, render: render }); //создаем игровое поле с высотой и шириной экрана пользователя;
/*инициализируем все наши переменные */
var player;
var socket, players = {};
var map;
var map_size = 2000; //размер карты
var style = { font: "80px Arial", fill: "white" }; //стили для надписи "Game Over"
var text;
var bullets;
var fireRate = 100;
var nextFire = 0;
var balls;
var ball;
var player_speed = 400; //скорость движения игрока
var live;
var moveBullets;
/*предзагружаем графические элементы*/
function preload() {
game.load.image('unit', 'img/unit.png');
game.load.image('bullet', 'img/bullet.png');
game.load.image('killer', 'img/killers.png');
game.load.image('map', 'img/grid.png');
}
function create() {
socket = io.connect(window.location.host); //подключаем сокеты
game.physics.startSystem(Phaser.Physics.ARCADE);
game.time.advancedTiming = true;
game.time.desiredFps = 60;
game.time.slowMotion = 0;
bg = game.add.tileSprite(0, 0, map_size, map_size, 'map'); //спрайт карты
game.world.setBounds(0, 0, map_size, map_size); //размеры карты
game.stage.backgroundColor = "#242424"; //цвет фона
socket.on("add_players", function(data) {
data = JSON.parse(data);
for (let playerId in data) {
if (players[playerId] == null && data[playerId].live) {
addPlayer(playerId, data[playerId].x, data[playerId].y, data[playerId].name);
}
}
live = true;
}); //создаем игроков
socket.on("add_player", function(data) {
data = JSON.parse(data);
if (data.player.live) {
addPlayer(data.id, data.player.x, data.player.y, data.player.name);
}
}); //создаем игрока
socket.on("player_rotation_update", function(data) {
data = JSON.parse(data);
players[data.id].player.rotation = data.value;
}); //вращение вокруг своей оси, ориентируясь на курсор
socket.on("player_position_update", function(data) {
data = JSON.parse(data);
players[socket.id].player.body.velocity.x = 0;
players[socket.id].player.body.velocity.y = 0;
players[data.id].player.x += data.x;
players[data.id].player.y += data.y;
}); //обновляем положение игроков
socket.on('player_fire_add', function(id) {
players[id].weapon.fire();
}); //исполняем выстрелы
game.input.onDown.add(function() {
socket.emit("shots_fired", socket.id);
}); //вызываем выстрелы
socket.on('clean_dead_player', function(victimId) {
if (victimId == socket.id) {
live = false;
}
socket.on("gameOver", function(data){
text = game.add.text(width / 2, height / 2, data, { font: "32px Arial", fill: "#ffffff", align: "center" });
text.fixedToCamera = true;
text.anchor.setTo(.5, .5);
});
players[victimId].player.kill();
}); //чистим поле от игроков в которых попали и выводим им текст о проигрыш
socket.on('player_disconnect', function(id) {
players[id].player.kill();
}); //чистим поле от отключившихся игроков
keybord = game.input.keyboard.createCursorKeys(); //инициализируем клавиатуру
}
function update() {
if (live == true) {
players[socket.id].player.rotation = game.physics.arcade.angleToPointer(players[socket.id].player); //вращаем игрока в сторону курсора
socket.emit("player_rotation", players[socket.id].player.rotation); //отправляем на сервер данные о вращени
setCollisions(); //функция вызывающаяся при столкновении пули с игроком
characterController(); //управление игроком
}
}
function bulletHitHandler(player, bullet) {
socket.emit("player_killed", player.id);
bullet.destroy(); // убираем пулю с поля, после попадания
} //функция при столкновении пули с игроком
function setCollisions() {
for (let x in players) {
for (let y in players) {
if (x != y) {
game.physics.arcade.collide(players[x].weapon.bullets, players[y].player, bulletHitHandler, null, this);
}
}
}
} //Проверка столкновения игрока с пулей
function sendPosition(character) {
socket.emit("player_move", JSON.stringify({
"id": socket.id,
"character": character
}));
} //отправляем инфу о том, куда игрок двинулся на сервер
function characterController() {
if (game.input.keyboard.isDown(Phaser.Keyboard.A) || keybord.left.isDown) {
//players[socket.id].player.x -= 5;
sendPosition("A");
}
if (game.input.keyboard.isDown(Phaser.Keyboard.D) || keybord.right.isDown) {
//players[socket.id].player.x += 5;
sendPosition("D");
}
if (game.input.keyboard.isDown(Phaser.Keyboard.W) || keybord.up.isDown) {
//players[socket.id].player.y -= 5;
sendPosition("W");
}
if (game.input.keyboard.isDown(Phaser.Keyboard.S) || keybord.down.isDown) {
//players[socket.id].player.y += 5;
sendPosition("S");
}
} //управление
function render() {
game.debug.cameraInfo(game.camera, 32, 32);
}
function addPlayer(playerId, x, y) {
player = game.add.sprite(x, y, "unit");
game.physics.arcade.enable(player);
player.smoothed = false;
player.anchor.setTo(0.5, 0.5);
player.scale.set(.8);
player.body.collideWorldBounds = true; //границы страницы
player.id = playerId;
let weapon = game.add.weapon(30, 'bullet'); //подключаем возможность выстрелов
weapon.bulletKillType = Phaser.Weapon.KILL_WORLD_BOUNDS;
weapon.bulletSpeed = 600; //скорость выстрелов
weapon.fireRate = 100;
weapon.trackSprite(player, 0, 0, true);
players[playerId] = { player, weapon };
game.camera.follow(players[socket.id].player, ); //слежение за игроком который открыл страницу
} //создаем игрока и даем ему ствол :)
Вот ссылка на гитхаб, чтобы задеплоить(допустим на heroku) и посмотреть как оно работает:
github.com/Dmitry-Rudenko/phaser_mmo_shooter
В целом, все написано не очень оптимизировано и вероятнее всего игра не будет быстро работать при большом количестве игроков, но геймплей игры как бы сам просит возможность создавать отдельные комнаты для разных игроков.
Ещё раз повторюсь, что статья несет ознакомительный характер и не стремится претендовать в топ лучших статьей гейм-разработки.
Надеюсь, эта информация оказалось полезной.
Всем спасибо!