По текущему роду деятельности я веб разработчик и поэтому захотелось чтобы в игре использовался только HTML, JavaScript и CSS — средства знакомые каждому вебразработчику. Никакого вам flash или даже canvas. Звучит хардкорно, но на самом деле сейчас HTML + CSS3 это очень мощные и гибкие средства визуализации, а писать игровой код на JavaScript — одно удовольствие. Вдобавок захотелось чтобы игра была с сетевым мультиплеером, притом интерактивной — никаких там шашек, карточных игр, пошаговых стратегий, все должно быть в действии и движении.
Вот что получилось в итоге:
В статье я оставлю набор заметок возникших при написании прототипа игрушки, нацеленных больше на подход «как сделать попроще и побыстрее». Думаю статья может сгодиться как некое подспорье для новичков в этом увлекательном деле.
Геймплей
Задача игры — вырастить свою колонию жуков и уничтожить всех юнитов противника. Жуки хаотично бегают по игровому полю, а задача игрока — помогать им находить бонусы в виде различной еды и оказывать помощь своим юнитам, на которых было произведено нападение.
Бонусы в игре:
кекс — дает 5xp, при наборе 15xp жук размножается
яблоко — восстанавливает 50hp, если жук полностью здоров то добавляет 15 дополнительных hp
перец — увеличивает атаку на 5dm
желудь — дает 2xp и швыряется в ближайшего противника, при попадании наносит тройной урон
мухомор — дает 1хp и позволяет произвести ядовитый выстрел, при попадании наносит 1/2 урона и замедляет жертву
Играть могут от 2 до 4 человек. Можно также просто подключиться к серверу из разных вкладок браузера, и поиграть одному.
Попробовать поиграть можно здесь.
Исходники на github.
Архив с игрой.
Графика
HTML и CSS конечно весьма не шустрые в плане производительности, когда речь идет об отрисовке графики, требующейся в интерактивных играх. Но если наша цель написать прототип игрушки, то этот вариант вполне сойдет. В конечном итоге «узкие моменты» в виде отрисовки основной сцены игры можно в дальнейшем побыстрому перебросить на canvas.
Для работы с графикой в 2д игре нам понадобятся операции перемещения, вращения и маштабирования спрайтов.
Перемещаем спрайт устанавливая у него position: absolute и изменяя left и top
Для вращения спрайтов воспользуемся transform: rotate. А с помощью transform: origin можно задать ось вращения (по умолчанию она в центре спрайта).
Для маштабирования изменяем размреры спрайта с помощью свойств width и height, перед этим установив подходящее значение в background-size:
Аппаратное ускорение
Для повышения производительности и соответственно плавности анимации можно заставить браузер использовать GPU для отрисовки анимаций. Для этого нужно работать со спрайтами как с трехмерными объектами. Теперь сделаем операции перемещения, вращения и маштабирования через translate3d, rotate3d и scale3d:
Всех этих операций вполне хватило чтобы собрать графику в игре из нескольких нарисованных в «пэйнте» спрайтов.
Физика
Помимо отрисовки игровых объектов, нужно также наладить их взаимодействие друг с другом.
В bugsarena все взаимодейсвие заключается в обработке столкновений спрайтов.
Так как планируется делать все максимально по простому, ограничимся школьной математикой.
Наверно одна из самых частых математических операций в играх — нахождение расстояния между двумя точками. По суте задача сводится к нахождению гипотенузы в треугольнике:
Получаем формулу:
Теперь благодаря этой простой формуле можно делать множество операций, таких как нахождения расстояния до объекта, нахождение самого ближайшего и самого удаленного объекта, нахождение объектов в заданном радиусе а так же обнаруживать столкновение объектов в форме круга.
Все объекты игры отрисовываются в достаточно небольшие спрайты размером 20х20, можно пренебречь их формой и расчитывать столкновения как-будто они все вписанны в окружность с диаметром 20. Тогда можно сказать что 2 объекта столкнулись когда растояние между их центрами меньше или равно сумме их радиусов.
И еще несколько заметок:
- Для задания угловых значений используйте радианы, а не градусы. Все угловые значения из 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}); } } } }) }();
Результат:
Сетевой код
Сетевой код написан с использованием вебсокетов спомощью библиотеки socket.io, сервер игры написан на nodejs.
Сделать по простому реализацию интерактивной сетевой игры да и с условием что нам доступнен только протокол TCP та еще задачка.
Сейчас для таких игр используют быстрый протокол UDP который к сожалению недоступен через socket.io, правда если есть сильное желание можно посмотреть в сторону WebRTC. Важно чтобы игра шла плавно без рывков и была синхронизированна на всех клиентах. Сервер будет простой и будет заниматься только передачей сообщений клиентов, так как только их действия влияют на ход игры. Он не будет заниматься передачай состояний игровых объектов, и вобще ничего не будет знать об игровом мире, кроме состояния игры — ожидание игроков/идет игра
Всю временную ленту игры можно разбить на кадры. Клиенты посылают сообщения о своих действиях серверу, сервер накапливает эти сообщения и через определенное количество кадров рассылает накопленное клиетам. Это как некая вариация очень ускоренной пошаговой стратегии — всем игрокам дано всего несколько кадров чтобы сделать свой ход (отправить сообщения серверу). По истечении этих кадров сервер рассылает клиентам все действия за предыдущий ход, которые тут-же начинают воспроизводиться. В это же время игроки могут сделать новый ход.
Этот подход хорош тем что он прост и клиенты всегда знают в какой по счету кадр запускаются действия приходящие с сервера и могут спокойно продолжать игру до наступления этого кадра. Сервер же должен прислать действия клиентов за некоторое время до наступления этого ключевого кадра, чтобы клиенты не простаивали. Недостаток этого подхода — не слишком быстрая реакция на действия игроков, и какой-нибудь активный шутер было бы играть сложновато.
Можно задаться вопросом — если мы передаем только действия клиентов, то как синхронизировать поведение объектов основанное на случайности? Ведь различные бонусы появляются в совершенно случайных местах, но у всех клиентов это должны быть одни и теже места. Жуки бегают весьма хаотично, постоянно меняя направление своего бега, и приэтом весь этот «хаос» должен быть совершенно одинаковым и идти по одному и тому же сценарию у всех. Проблемму с синхронизацией такого поведения можно решить тем, чтобы везде где используются случайные величины, не использовать для этого 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. И к нам в игру заходит игрок с именем "<script>alert(‘В игру заходит Вася!’)</script>". Его имя добавляется в div со списком игроков, и все клиенты получают назойливое сообщение alert’ом. И это еще цветочки. Через XSS уязвимость можно спокойно подгрузить любой скрипт с любого сайта. Так что не забываем об экранировании передаваемых с клиентов данных.
ссылка на оригинал статьи http://habrahabr.ru/post/225669/
Добавить комментарий