В предыдущих двух частях (Делаем вебсокеты на PHP с нуля и Межпроцессное взаимодействие) в качестве демонстрации я использовал чаты, но в этой статье на примере онлайн-игры я покажу, что сфера применения вебсокетов может быть гораздо шире.
Как обычно, в конце статьи ссылки на демонстрационную игру и исходный код на гитхабе.
Содержание:
- Поддержка вебсокетов браузерами
- Разработка онлайн-игры
- Благодарности
- Демка и исходный код
Поддержка вебсокетов браузерами
Некоторые считают, что вебсокеты ещё рано использовать, потому что они поддерживаются ещё не всеми браузерами. Поэтому, если их использовать, то только совместно с альтернативными транспортами: Adobe® Flash® Socket, AJAX long polling, AJAX multipart streaming, Forever Iframe, JSONP Polling.
Википедия нам подсказывает, какие браузеры поддерживают вебсокеты:
Google Chrome (начиная с версии 4.0.249.0);
Apple Safari (начиная с версии 5.0.7533.16);
Mozilla Firefox (начиная с версии 4);
Opera (начиная с версии 10.70 9067);
Internet Explorer (начиная с версии 10);
Как мы видим, самым слабым звеном является Internet Explorer с версиями меньше десятой. Согласно статистике liveinternet, для России — Internet Explorer с версиями 9, 8, 7 и 6 имеет доли 1.4, 1.7, 0.5 и 0.1 процентов соответственно. Суммарно получается 3.7%. Если добавить к этой цифре ещё пользователей с устаревшими версиями других браузеров, то итоговая оценка может немного увеличиться, но, не думаю, что она станет больше 4%.
Основываясь на этом, каждый должен решить для себя сам — нужно ли поддерживать зоопарк альтернативных транспортов или забыть про этих пользователей и жить дальше.
Справедливости ради хочу сказать, что за рубежом доля Internet Explorer больше, и ситуация с поддержкой вебсокетов там соответствующая. Согласно статистике с сайта w3schools Internet Explorer с версиями 9, 8, 7 и 6 имеет доли 2.3, 3.1, 0.4 и 0.1 процентов соответственно, что в сумме составляет 5.9%
Разработка онлайн-игры
Итак, теперь к главному. Для демонстрации работы сервера вебсокетов на php мне захотелось написать простую игру. Для начала мне нужно было определиться какую именно. Пожалуй, единственное требование к ней было таким:
все игроки должны находиться на одной карте и иметь возможность взаимодействовать с любым другим игроком
Я долго гуглил на эту тему, пока не наткнулся на эту страницу в «тостере», где TravisBickle, разработчик phpdaemon, просит у сообщества подсказать идею простой игры, которая бы продемонстрировала работу вебсокетов. Несмотря на то, что некоторые ответы были достаточно интересными, этому вопросу уже почти 3 года…
Из всех предложений я выбрал «танчики», но решил сделать упрощённую версию того что предлагали, а не полноценную игру, чтобы процесс разработки не затягивался и демка всё-таки увидела свет, а не осталась в чертогах моего разума.
Взяв код чата из предыдущей статьи, я дописал немного клиентскую часть, используя:
canvas
и метод объектаcontext
:drawImage
для отрисовки изображения танка,fillRect
— для закрашивания прямоугольников иfillText
для надписей (сразу скажу, что я с ними раньше никогда не работал)addEventListener
для обработки нажатий клавиш «вверх», «вниз», «влево», «вправо» и «пробел» (а также «w», «s», «a», «d»)
На серверной стороне я немного расширил обработчик сообщений от клиента:
- каждый танк — это массив состоящий из координат, имени и количества «жизней»
- при приходе от клиента команды «вверх», «вниз» и так далее я пересчитываю значения, соответствующие координатам и отправляю на клиент все массивы танков
- обмен данными с клиентом происходит с помощью json
Таким образом я реализовал танки двигающиеся по экрану.
Так как я рассчитывал где-то на 50 — 500 одновременных игроков, стало понятно, что все танки на один экран не влезут, поэтому я ограничил область видимости танка до размеров обычного поля Battle City, а также добавил миникарту. Из-за того что на оригинальном чёрном фоне непонятно, то ли движется танк, то ли всё остальное, мне пришлось использовать текстуру. Если вы можете предложить лучший вариант текстуры, оставьте пожалуйста ссылку на неё в комментарии.
Следующим шагом стала стрельба. Для этого необходимо не только обрабатывать сообщения от клиентов, но и срабатывать по таймеру для расчёта передвижения выпущенного снаряда (я решил, что 10 раз в секунду будет достаточно или каждые 100.000 микросекунд соответственно). Напомню, что я использовал функцию stream_select(array &$read, array &$write, array &$except, int $tv_sec [, int $tv_usec = 0])
, которая принимает массивы сокетов, необходимых для обработки и срабатывает либо при изменениях в них, либо по таймауту. Было принято решение использовать возможность таймаута этой функции для реализации таймеров, но, к сожалению, произошло то, что писали в документации.
Использовать функцию stream_select с таймаутом — плохая идея. Если вы всё же решились, то рекомендуем использовать таймауты больше хотя бы 200.000 микросекунд.
С моим таймаутом 100.000 микросекунд загрузка процессора составляла 100%.
В связи с этим я решил, что даже если мои танки не будут стрелять, то они всё равно должны взаимодействовать. Так я стал обрабатывать их столкновения :)
Не было понятно, какому танку из двух засчитывать очки за лобовое столкновение, а также сложности с ударом в бок. По этой причине я отказался от этих двух неоднозначных типов контакта и оставил только один оставшийся вариант — один танк таранит другого сзади.
На этом, вроде, можно было остановиться — цель «взаимодействие» была достигнута, но хотелось большего.
$this->base = event_base_new();
$this->event = event_new();
event_set($this->event, $this->_server, EV_READ | EV_PERSIST, array($this, 'accept'), $this->base);
event_base_set($this->event, $this->base);
event_add($this->event);
$timer = event_timer_new();
event_timer_set($timer, array($this, '_onTimer'), $timer);
event_base_set($timer, $this->base);
event_timer_add($timer, 100000);
event_base_loop($this->base);
Умные люди пишут, что event_timer это по сути буфер, который имеет таймаут, и я решил поискать, можно ли сделать что-то похожее на stream_select, но, увы, безрезультатно. Если вы знаете, как это сделать, пожалуйста, напишите в комментариях.
$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);//открываем парный сокет
$pid = pcntl_fork();//создаём форк
if ($pid == -1) {
die("error: pcntl_forkrn");
} elseif ($pid) { //родитель
fclose($pair[0]);
$pair[1];//один из пары будет в родителе, его мы будем обрабатывать в функции sream_select
} else { //дочерний процесс
fclose($pair[1]);
$parent = $pair[0];//второй в дочернем процессе, в него мы будем писать данные 10 раз в секунду
while (true) {
fwrite($parent, '1');
usleep(100000);
}
}
В результате чего загрузка процессора около 0%.
Теперь ничего не мешало мне добавить возможность стрельбы, но, по просьбам друзей, я решил оставить возможность «столкновений».
Благодарности
Я хотел бы поблагодарить всех тех, кто обращал моё внимание на недочёты в моём коде в первых двух публикациях:
Skpd, pavlick, mayorovp, truezemez, Fesor, sovok_kpss, spein, seriyPS
Спасибо вам большое и, конечно же, +1 в карму.
Благодаря вам удалось добиться стабильности для получившейся библиотеки и более глубокого понимания для меня.
Демонстрация и исходный код
Технические детали:
- одновременно у танка может быть только один запущенный снаряд, поэтому старайтесь попадать по противнику и не стрелять через всю карту
- пока летает ваш снаряд, вы можете таранить другие танки в заднюю часть
- ваш танк — жёлтый, противники — зелёные
- все танки находятся на одной большой карте, ориентируйтесь по мини-карте, которая находится в правом верхнем углу
- карта автоматически увеличивается в зависимости от количества игроков, но назад не уменьшается
- препятствия, орёл, лес, граната и т.д. не реализованы
Демонстрационная игра (с использованием stream_select)
Демонстрационная игра (с использованием libevent)
Исходный код библиотеки и примеров лежит на гитхабе и доступен под лицензией MIT
Автор: morozovsk