Вебсокеты на PHP. Часть 3. От чата до игры: Battle City

    В предыдущих двух частях (Делаем вебсокеты на 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

    Пример ответа от сервера с тремя танками
    [{«name»:«adsa»,«x»:25,«y»:38,«dir»:«right»,«health»:0},{«name»:«qwe»,«x»:20,«y»:18,«dir»:«right»,«health»:0},{«name»:«sgfd4»,«x»:5,«y»:7,«dir»:«right»,«health»:0}]

    Таким образом я реализовал танки двигающиеся по экрану.

    Так как я рассчитывал где-то на 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%.

    В связи с этим я решил, что даже если мои танки не будут стрелять, то они всё равно должны взаимодействовать. Так я стал обрабатывать их столкновения :)
    Не было понятно, какому танку из двух засчитывать очки за лобовое столкновение, а также сложности с ударом в бок. По этой причине я отказался от этих двух неоднозначных типов контакта и оставил только один оставшийся вариант — один танк таранит другого сзади.

    На этом, вроде, можно было остановиться — цель «взаимодействие» была достигнута, но хотелось большего.
    Потратив ещё некоторое время, я реализовал таймеры, используя libevent (теперь моя библиотека работает и со sream_select, и с libevent):
    $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_fork\r\n");
    } 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
    i-Free Group
    49.80
    Company
    Support the author
    Share post

    Comments 23

      +5
        –19
        Супер! Давай текстурой карту Киева с майданом!
          +1
          Не надо над этим шутить.
            0
            Не надо над этим шутить здесь, я бы сказал. Да и вообще затрагивать подобную тематику.
          0
          Есть ещё одна причина, по которой используются WebSocket альтернативы, в частности AJAX/JSONP варианты — прокси-сервера, что не позволяют метод CONNECT вообще (редко), либо позволяют только на 443 порт (часто, особенно в корпоративных сетях).

          Хотелось бы увидеть продолжение с fallback на эти варианты именно ради поддержки таких клиентов.
            0
            Если будет время, то сделаю скрипт, который будет пробовать открывать вебсокет, а при неудаче (или по таймауту) также отправлять запрос аяксом. Тогда если попросить пользователей хабра принять участие в тестировании, то можно собрать статистику — % людей, у кого вебсокеты не работают.

            Если процент не существенный, то я вижу два варианта продолжения:
            1) сделать полноценные танчики (с кирпичами, лесом, уровнями и т.д.)
            2) сделать pecl-расширение, чтобы моя библиотека работала быстрее (сейчас очень медленно работает кодирование/декодирование данных)
            Скорее всего буду двигаться по обоим направлениям, возможно даже одновременно.
              0
              Выборка по Хабру будет не слишком репрезентативной — местные люди либо работают в компаниях, где таких ограничений нет, либо, вроде меня, обошли это ограничение :)

              Вообще, что меня больше всего напрягает в WebSockets — это необходимость в отдельном порту, чего при остальных COMET вариантах не требуется…
                0
                Если проксировать через nginx, то отдельный порт не нужен будет. Пример конфига можно посмотреть в предыдущей моей статье: habrahabr.ru/company/ifree/blog/210228/
                  0
                  Хм, любопытно. Самое интересное, что это может как раз решить проблему с проксями.
                    +1
                    Если интересно: вот результат WebSocket теста для такого прокси из Chrome (прокси — Forefront TMG): websocketstest.com/result/299253.

                    К слову — интересный факт — если «пропроксировать» его ещё дополнительно через cntlm — то сразу вот такая картина: websocketstest.com/result/299256.

                    Ну а вот так выглядит после обхода прокси: websocketstest.com/result/299257.
                      0
                      Большое спасибо. Очень полезная информация.
                      Благодаря этому сайту я узнал, что прокси на моей работе не поддерживает вебскокеты на 80 порту. websocketstest.com/result/299261
                      Так же там есть общая статистика: websocketstest.com/ws/stats, но такое ощущение, что она агрегируется каждый раз свежая, поэтому открывается несколько минут.
                      скриншоты статистики:




                        0
                        У меня есть сайт, который не работает без WebSocket. Какие проблемы: Android в пролёте; Около 2% посетителей, зашедших на сайт видят заглушку (т.к. нет window.WebSocket); у ~8% от тех, кто не увидел заглушку (window.WebSocket есть) не получилось подключиться со 2-й попытки (прокси?).
                        Так что фоллбек всё ещё имеет смысл, если 10% аудитории вам дороги (хотя для реалтайм игры AJAX фолбек скорее всего будет добавлять заметных задержек даже при использовании keep-alive).
                        Ну и обязательно нужно реализовать периодический пинг-понг (есть такой тип фрейма в протоколе), чтобы закрывать сокеты отвалившихся клиентов.
                          0
                          Есть всякие COMET варианты — long pooling и прочие варианты. Так что задержек поидее быть не должно.
                            0
                            Задержки в основном на отправке данных будет.
                            Если для получения данных от сервера ещё можно использовать стриминг (не уверен, что все браузеры поддерживают. Накрайняк jsonp стриминг), то для отправки событий на сервер (нажатия клавиш) придётся слать отдельный AJAX запрос на каждое событие. А это 400-600 байт оверхеда на заголовки.
              0
              Все на хабратест хабротанчиков! Ура!
                0
                Сервер с использованием libevent лежит. :(
                  0
                  мой хостер не очень удачно разбил жёсткий диск на разделы, поэтому нужное место закончилось в самый неожиданный момент.
                    0
                    проблема устранена
                      0
                      но libevent всё равно подглючивает
                0
                А если поменять координаты мапы через консоль, где я нахожусь у других?
                IMG
                image
                  0
                  На сервер не уходят «координаты», а только команды («вверх», «вниз», «влево», «вправо»), сам же расчёт координат происходит на сервере исходя из команд.
                  0
                  Как показывает практика, в таких играх нужно запускать на поле бота, если игроков мало. Т.к. человек заходит в игру, видит что там никого нет и через минуту уже уходит и не возвращается.
                    0
                    Да, конечно в релизе будут боты, это просто демка.

                  Only users with full accounts can post comments. Log in, please.