[Из песочницы] Интерактивная сетевая игра на HTML, CSS и JavaScript

Как-то поиграв в оффисе, в hexbug, зародилась идея написать игрушку по схожим мотивам.По текущему роду деятельности я веб разработчик и поэтому захотелось чтобы в игре использовался только HTML, JavaScript и CSS — средства знакомые каждому вебразработчику. Никакого вам flash или даже canvas. Звучит хардкорно, но на самом деле сейчас HTML + CSS3 это очень мощные и гибкие средства визуализации, а писать игровой код на JavaScript — одно удовольствие. Вдобавок захотелось чтобы игра была с сетевым мультиплеером, притом интерактивной — никаких там шашек, карточных игр, пошаговых стратегий, все должно быть в действии и движении.Вот что получилось в итоге:

[embedded content]

В статье я оставлю набор заметок возникших при написании прототипа игрушки, нацеленных больше на подход «как сделать попроще и побыстрее». Думаю статья может сгодиться как некое подспорье для новичков в этом увлекательном деле.

ГеймплейЗадача игры — вырастить свою колонию жуков и уничтожить всех юнитов противника. Жуки хаотично бегают по игровому полю, а задача игрока — помогать им находить бонусы в виде различной еды и оказывать помощь своим юнитам, на которых было произведено нападение.Бонусы в игре: кекс — дает 5xp, при наборе 15xp жук размножаетсяяблоко — восстанавливает 50hp, если жук полностью здоров то добавляет 15 дополнительных hpперец — увеличивает атаку на 5dmжелудь — дает 2xp и швыряется в ближайшего противника, при попадании наносит тройной уронмухомор — дает 1хp и позволяет произвести ядовитый выстрел, при попадании наносит ½ урона и замедляет жертву

Играть могут от 2 до 4 человек. Можно также просто подключиться к серверу из разных вкладок браузера, и поиграть одному.Попробовать поиграть можно здесь.Исходники на github.Архив с игрой.

Графика HTML и CSS конечно весьма не шустрые в плане производительности, когда речь идет об отрисовке графики, требующейся в интерактивных играх. Но если наша цель написать прототип игрушки, то этот вариант вполне сойдет. В конечном итоге «узкие моменты» в виде отрисовки основной сцены игры можно в дальнейшем побыстрому перебросить на canvas.Для работы с графикой в 2д игре нам понадобятся операции перемещения, вращения и маштабирования спрайтов.

Перемещаем спрайт устанавливая у него position: absolute и изменяя left и top

40117561062ece0fd77d4deabaf190e0.gif

Для вращения спрайтов воспользуемся transform: rotate. А с помощью transform: origin можно задать ось вращения (по умолчанию она в центре спрайта).

75369c54dd1e6170b341aee7d5244666.gif

Для маштабирования изменяем размреры спрайта с помощью свойств width и height, перед этим установив подходящее значение в background-size:

174eb43f952ad6b3333896a80afb69f7.gif

Аппаратное ускорение Для повышения производительности и соответственно плавности анимации можно заставить браузер использовать GPU для отрисовки анимаций. Для этого нужно работать со спрайтами как с трехмерными объектами. Теперь сделаем операции перемещения, вращения и маштабирования через translate3d, rotate3d и scale3d: 0178a82e96ce543736a3fa21c18c0bcb.gif

aebb465aeae7bca132d24dc84520f6de.gif

035396ad68b58fd2fc33378fe56e5e26.gif

Всех этих операций вполне хватило чтобы собрать графику в игре из нескольких нарисованных в «пэйнте» спрайтов.

Физика Помимо отрисовки игровых объектов, нужно также наладить их взаимодействие друг с другом.В bugsarena все взаимодейсвие заключается в обработке столкновений спрайтов.Так как планируется делать все максимально по простому, ограничимся школьной математикой.Наверно одна из самых частых математических операций в играх — нахождение расстояния между двумя точками. По суте задача сводится к нахождению гипотенузы в треугольнике: 23a28b071203e753352e79001f9dcdd2.gif

Получаем формулу:

330110f531070d4ceb92153f9b234fbc.jpg

Теперь благодаря этой простой формуле можно делать множество операций, таких как нахождения расстояния до объекта, нахождение самого ближайшего и самого удаленного объекта, нахождение объектов в заданном радиусе, а так же обнаруживать столкновение объектов в форме круга.Все объекты игры отрисовываются в достаточно небольшие спрайты размером 20×20, можно пренебречь их формой и расчитывать столкновения как-будто они все вписанны в окружность с диаметром 20. Тогда можно сказать что 2 объекта столкнулись когда растояние между их центрами меньше или равно сумме их радиусов.

d0a29f76c0de592673709ad7ec6a8a95.gif

И еще несколько заметок:

Для задания угловых значений используйте радианы, а не градусы. Все угловые значения из Math возвращаются именно в них. Напомню полный оборот равняется 2 * PI радиан Используйте понятие вектора для задания велечин у которых есть направление. Даже положение спрайтов можно описывать вектором. Можно создать свой класс вектора или воспользоваться классом описанным в этой статье либо любым другим.Для примера, вектором задается скорость объектов, так как она имеет велечину и направление. В этом случае чтобы увеличить скорость в двое мы просто умножаем вектор на 2, а чтобы изменить скорость в обратное направление мы инвертируем вектор (умножаем на -1). Если в игре требуется сложная физика то можно посмотреть в сторону box2d-js. Эта библиотека позволит создать игровой мир с объектами различной формы, гравитацией, массой, инерцией, силой трения и прочими благами ньютоновской физики Пример класса Вектор // инициализация function Vec (x_, y_) { if (typeof x_ == 'object') { this.setV (x_); return; } this.x= typeof x_ == 'number' ? x_ : 0; this.y= typeof y_ == 'number' ? y_ : 0; }

Vec.prototype = {

// установка в 0 setZero: function () { this.x = 0.0; this.y = 0.0; },

// установка значений x и y set: function (x_, y_) {this.x=x_; this.y=y_;},

// установка значений из объекта setV: function (v) { this.x=v.x; this.y=v.y; },

// реверс вектора negative: function (){ return new Vec (-this.x, -this.y); },

// копия вектора copy: function (){ return new Vec (this.x, this.y); },

// сложение с вектором add: function (v) { this.x += v.x; this.y += v.y; return this; },

// вычетание вектора mubtract: function (v) { this.x -= v.x; this.y -= v.y; return this; },

// умножение на число multiply: function (a) { this.x *= a; this.y *= a; return this; },

// деление на число div: function (a) { this.x /= a; this.y /= a; return this; },

// получение длины вектора length: function () { return Math.sqrt (this.x * this.x + this.y * this.y); },

// нормализация вектора (приведение к вектору с длиной = 1) normalize: function () { var length = this.length (); if (length < Number.MIN_VALUE) { return 0.0; } var invLength = 1.0 / length; this.x *= invLength; this.y *= invLength;

return length; },

// получение угла вектора angle: function () { var x = this.x; var y = this.y; if (x == 0) { return (y > 0) ? (3 * Math.PI) / 2: Math.PI / 2; } var result = Math.atan (y/x);

result += Math.PI/2; if (x < 0) result = result - Math.PI; return result; },

// получение растояния до другого вектора (полезно если вектором задается положение спрайта) distanceTo: function (v) { return Math.sqrt ((v.x — this.x) * (v.x — this.x) + (v.y — this.y) * (v.y — this.y)); },

// получение вектора проведенного от вершины x, y данного вектора до вершины x, y другого вектора vectorTo: function (v) { return new Vec (v.x — this.x, v.y — this.y); },

// поворот вектора на заданный угл rotate: function (angle) { var length = this.length (); this.x = Math.sin (angle) * length; this.y = Math.cos (angle) * (-length); return this; } }; Используемые паттерны разработки В нескольких словах игровую логику можно описать так: Есть объект класса «Game» описывающий игровой мир у которого есть массив объектов-наследников от класса «GameObject» — это все объекты игрового мира. Каждый игровой кадр Game проходится по всем игровым объектам и вызывает у каждого метод step. В методе step каждого объекта описывается что он должен сделать за этот кадр (переместиться, обработать столкновения, уничтожиться и тд.) Для реализации ООП в игре используется объект Class из Simple JavaScript Inheritance от John Resig, доработанный до поддержки миксинов и статических свойств.Наверное один из самых удачных патернов для создания новых объектов в играх это использование фабричного метода. Суть в том что мы не будем напрямую через вызов new Создавать объекты, а воспользуемся методом который за нас это сделает. Фабричный метод избавит нас от возни с подключением нового объекта в игровой мир.Например мы хотим создать объект класса Block включить его в игровой мир и расположить в заданном месте:

game.create ('Block', {x: 100, y: 150}); Код метода create:

create: function (objectName, params) {

// для удобства все классы доступные для создание через фабричный метод хранятся в Game.classes // Создаем объект получая его класс из Game.classes var object = new Game.classes[objectName](params);

// присваиваем ему уникальный идентефикатор object.id = ++this.idx;

// задаем объекту ссылку на игровой мир object.game = this;

// добавляем получившийся объект в массив игровых объектов this.objects[object.id] = object;

// если объект может сталкиваться с другими объектами то дополнительно // помещаем ссылку на него в соответствующий массив if (object.isColliding) this.collidingObjects[object.id] = object;

// сообщаем объекту что он полностью подключен к игровому миру // с помощью вызова метода birth, в котором он может завершить инициализацию object.birth ();

// возвращаем готовый объект return object; }, Создание игровых карт Итак когда игра уже написанна хочется разнообразить ее несколькими игровыми картами. Создавать все игровые объекты кодом (вызывая метод за методом) очень утомительно и ненаглядно. Писать свой редактор карт займет достаточно много времени. Но есть простой способ — можно воспользоваться текстовым редактором или своей ide для наглядного создания следующим подходом: ! function () {

var WIDTH = 20; var HEIGHT = 12;

var B = 'Block'; var P = 'Bonus';

var MAP = [ , , , , , , , , , , , , , , , , , , , , , B, , B, , B, B, B, , B, , , , B, , , , B, B, B, , B, , B, , B, , , , B, , , , B, , , , B, , B, , B, B, B, , B, B, B, , B, , , , B, , , , B, , B, , B, , B, , B, , , , B, , , , B, , , , B, , B, , B, , B, , B, B, B, , B, B, B, , B, B, B, , B, B, B, , , , , , , , , , , , , , , , , , , , , , B, , B, , B, B, B, , B, B, B, , B, B, B, , , , , , B, , B, , B, , B, , B, , B, , B, , B, , , , , , B, B, B, , B, B, B, , B, B, , , B, B, , , , , , , B, , B, , B, , B, , B, , B, , B, , B, , , , , , B, , B, , B, , B, , B, B, B, , B, , B, , P, , , ];

Game.maps['Hello'] = Game.Map.extend ({

build: function () {

var blockSize = 20; for (var i = 0; i < HEIGHT; i++) { for (var j = 0; j < WIDTH; j++) { var index = WIDTH * i + j; if (MAP[index]) this.game.create(MAP[index], {x: blockSize * j, y: blockSize * i}); } } }

}) }(); Результат: 9ef38e62ae33451184a0c5060bc95b41.png

Сетевой код Сетевой код написан с использованием вебсокетов спомощью библиотеки socket.io, сервер игры написан на nodejs.Сделать по простому реализацию интерактивной сетевой игры да и с условием что нам доступнен только протокол TCP та еще задачка.Сейчас для таких игр используют быстрый протокол UDP который к сожалению недоступен через socket.io, правда если есть сильное желание можно посмотреть в сторону WebRTC. Важно чтобы игра шла плавно без рывков и была синхронизированна на всех клиентах. Сервер будет простой и будет заниматься только передачей сообщений клиентов, так как только их действия влияют на ход игры. Он не будет заниматься передачай состояний игровых объектов, и вобще ничего не будет знать об игровом мире, кроме состояния игры — ожидание игроков/идет играВсю временную ленту игры можно разбить на кадры. Клиенты посылают сообщения о своих действиях серверу, сервер накапливает эти сообщения и через определенное количество кадров рассылает накопленное клиетам. Это как некая вариация очень ускоренной пошаговой стратегии — всем игрокам дано всего несколько кадров чтобы сделать свой ход (отправить сообщения серверу). По истечении этих кадров сервер рассылает клиентам все действия за предыдущий ход, которые тут-же начинают воспроизводиться. В это же время игроки могут сделать новый ход.

f33c7058be774027a491e1b8332fab69.gif

Этот подход хорош тем что он прост и клиенты всегда знают в какой по счету кадр запускаются действия приходящие с сервера и могут спокойно продолжать игру до наступления этого кадра. Сервер же должен прислать действия клиентов за некоторое время до наступления этого ключевого кадра, чтобы клиенты не простаивали. Недостаток этого подхода — не слишком быстрая реакция на действия игроков, и какой-нибудь активный шутер было бы играть сложновато.

Можно задаться вопросом — если мы передаем только действия клиентов, то как синхронизировать поведение объектов основанное на случайности? Ведь различные бонусы появляются в совершенно случайных местах, но у всех клиентов это должны быть одни и теже места. Жуки бегают весьма хаотично, постоянно меняя направление своего бега, и приэтом весь этот «хаос» должен быть совершенно одинаковым и идти по одному и тому же сценарию у всех. Проблемму с синхронизацией такого поведения можно решить тем, чтобы везде где используются случайные величины, не использовать для этого Math.random, а использовать свой генератор псевдослучайных чисел (ГПСЧ). Суть в следующем — перед запуском игры сервер генерирует случайное число и передает его каждому присоединившемуся клиенту. С помощью этого числа клинет инициализирует ГПСЧ котрый на всех клиентах будет выдавать одинаковую последовательность псевдослучайных чисел. Простейшая реализация такого ГПСЧ — генератор парка-миллераРеализация на js:

var ParkMillerGenerator = function (initializer) { this.a = 16807; this.m = 2147483647; this.val = initializer || Math.round (2147483647 / 3); }

ParkMillerGenerator.prototype = { next: function () { this.val = (this.a * this.val) % this.m; return (this.val / 1000000) % 1; } } Использование:

var initializer = 333; // задаеем инициализирующее число, у всех клиентов оно должно быть одинаковое var gen = new ParkMillerGenerator (initializer); // создаем ГПСЧ gen.next (); // 0.5967310000000001 gen.next (); // 0.46109599999999773 gen.next (); // 0.07891199999994569; Делаем сервис из nodejs приложения Может немного не втему, но тоже полезная заметка. Когда сервер написан, неплохо бы запустить его на боевой машине в виде службы для постоянной работы. Опишу как это можно сделать на примере Ubuntu.Переходим в /etc/init.d и создаем там шелл-скрипт с названием нашей службы, у меня будет bugsarena. Обращу внимание что блок начинающийся с «BEGIN INIT INFO» не просто коментарий, а настройки нашей службы и удалять его не стоит. #!/bin/sh

### BEGIN INIT INFO # Provides: bugsarena # Required-Start: $local_fs $remote_fs $network $syslog # Required-Stop: $local_fs $remote_fs $network $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: starts the bugsarena servers # Description: starts the bugsarena servers ### END INIT INFO

# задаем пути и параметры к исполняемым файлам (нужно указать свои) NODE=/usr/bin/node DAEMON_SERVER=/home/me/projects/bugs-arena/server/server.js SERVER_PARAMS=«name=Arena-Dogfight map=Dogfight port=8090»

NAME=bugsarena DESC=«bugsarena servers»

# сервис должен принимать 3 команды — start, stop и restart. # опишем обработчики этих комманд

start () { # запускаем nodejs приложение в качестве демона и сохраняем его pid в файл start-stop-daemon --start --make-pidfile --background --pidfile /var/run/$NAME-server.pid \ --exec $NODE — $DAEMON_SERVER $SERVER_PARAMS

}

stop () { # останавливаем nodejs приложение echo -n «Stopping $DESC:» start-stop-daemon --stop --quiet --pidfile /var/run/$NAME-server.pid }

case »$1» in start) start ;; stop) stop ;; restart) stop sleep 1 start ;; *) echo «Usage: $NAME {start|stop|restart}» >&2 exit 1 ;; esac

exit 0 Теперь можно воспользоваться командами

service bugsarena start

и service bugsarena stop

для запуска и остановки службы.Также можно сделать чтобы игровой сервер стартовал при запуске системы выполнив update-rc.d bugsarena defaults

Не забываем о XSS! На последок просто необходимо напомнить об очень простой атаке свойственной для браузерных игр. Представим что у нас есть список игроков в каком-нибудь div’e. И к нам в игру заходит игрок с именем «alert ('В игру заходит Вася!')». Его имя добавляется в div со списком игроков, и все клиенты получают назойливое сообщение alert’ом. И это еще цветочки. Через XSS уязвимость можно спокойно подгрузить любой скрипт с любого сайта. Так что не забываем об экранировании передаваемых с клиентов данных.

© Habrahabr.ru