game-net

сетевые игры — идеи и решения

крис касперски ака мыщъх, no-email

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

Прежде чем погружаться в тонкости межсетевого взаимодействия возьмем мыщъха за хвост и вспомним старые игры, высаживающие на одном компьютере сразу несколько игроков. В пошаговых стратегиях игрой обычно управляли по очереди. В играх реального времени такой подход был уже неприемлем и приходилось делить одну клавиатуру на двоих (хорошо, если был джойстик!).

Идея — чтобы организовать сетевую игру достаточно загнать весь ввод/вывод в магистральный кабель, реализовав удаленный монитор и клавиатуру (джойстик/мышь). Получится точно так же, как и раньше, только намного круче. С клавиатурой все просто — тут базару нет — а вот осуществить передачу видеоизображения в реальном времени удастся едва ли. Даже если применить компрессию, модемного канала все равно не хватит и понадобиться локальная сеть или по меньшей мере DSL. Не слишком ли завышенные требования? На самом деле, все проблемы решаемы, стоит только покурить!

Рассмотрим простейший случай: игру для двоих игроков. Один компьютер будет «ведомым», другой «ведущим». Ведущий компьютер обсчитывает игровое пространство, управляет движением монстров (если они есть), обрабатывает столкновения и т. д. Для синхронизации игровых миров все события игрового пространства (перемещение объектов, взрывы мин, полеты торпед) передаются ведомому компьютеру, который отображает их на экране и отмечает на своей карте, при этом неявно предполагается, что обе карты (ведомого и ведущего компьютеров) полностью идентичны. Если же в игровом пространстве присутствуют случайные элементы (например, аптечки, расставленные в разных местах) они должны быть переданы ведомому компьютеру. Это достаточно сделать всего один раз — перед началом игры. При желании можно даже передать всю карту целиком (нужно учитывать, что ведущий компьютер может использовать дополнительные или «самопальные» миссии, которых нет у ведомого). Ведомый компьютер, в свою очередь управляет локальным игроком, отслеживая нажатия клавиш, и отправляет эту информацию ведущему.

Рисунок 1 схема взаимодействия ведомого и ведущего компьютера в мире из двух игроков

из локальной сети в интернет

На первый взгляд все просто и никаких греблей здесь нет. В локальной сети это, может быть, и так, но вот передача данных через Интернет сопряжена с не периодичными задержками, и если не предпринять дополнительных мер, монстры будут двигаться как припадочные. Как же быть? (Тянуть свое оптоволокно не предлагать). А что если передавать не сами перемещения, а их предполагаемый сценарий? Обычно движение монстров подчиняется набору шаблонов и компьютеру достаточно передать что-то типа: «двигайся от меня и до обеда/упора», «атакуй по сценарию D» или «уклоняйся по сценарию A». Количество передаваемой информации резко сокращаются и для обеспечения синхронизации достаточно лишь периодически (скажем раз в секунду) передавать «квитки», сигнализирующие о пересечении объектом некоторой клетки игрового поля. Как легко показать, такой протокол передачи устойчив даже к длительным задержкам, что не маловажно при работе на сильно загруженных каналах.

С игроками все значительно сложнее. Допустим, два горячих мудреца (в смысле думераца) стоят супротив друг друга и пускают по торпеде. Если информация о перемещении одного из игроков хоть чуть-чуть запоздает, очень может случиться так, что с точки зрения ведомого компьютера торпеда пройдет мимо, а ведущий увидит как противника разорвало в клочья. Первое, что приходит на ум — осуществлять перемещение только после подтверждения. Выглядит это так: игрок нажимает на стрелку. Компьютер А (не важно ведущий или ведомый) отправляет уведомление компьютеру B (стадия I). Компьютер B обновляет свое игровое пространство и посылает подтверждение компьютеру А (стадия II). Компьютер А принимает его и перемещает игрока (стадия III). Все хорошо? Мыщъх так не думает. Игрок давит на клавишу, но фигурка на экране остается неподвижной (задержка передачи данных по сети). Что делает игрок? Правильно! Давит на клавишу еще и еще! К тому же, если на стадии III случится задержка, в игровых мирах вновь произойдет рассинхронизация и дело закончится лесом. Следовательно, возникает необходимость в четвертой стадии, обеспечивающий дополнительный уровень подтверждений, но… как же тогда все будет тормозить!

"быстрая" и "медленная" синхронизация

Мыщъх рекомендует полуэвристический алгоритм: когда игроки находятся в разных концах игрового поля они перемещаются без подтверждений, но как только их игровые миры соприкоснуться (один игрок увидит другого или в их поле зрения попадет общий монстр), автоматически активируется режим «обмена подтверждениями».

Зная направление и скорость движения игрока/монстра/торпеды удаленный компьютер может с высокой степенью достоверности рассчитать произойдет ли столкновение на данном временном участке или нет. Даже самый прыткий игрок не может менять направление своего движения несколько раз в секунду, поэтому достаточно подтверждать лишь изменение направления! Конечно, при возникновении задержки в сети, фигурка на экране отреагирует на действие игрока не сразу, но во всяком случае она не будет тупо стоять, а продолжит движение. То есть, субъективно поведение компьютера станет более реалистичным.

борьба с жульничеством

А вот другая серьезная проблема — читерство. Ведущий компьютер может как угодно мухлевать, ведь код и данные игрового процесса находятся в полном ведении игрока. Что ему стоит, слегка повозившись с отладчиком, приобрести божественное здоровье или нескончаемые патроны? Некоторые программисты просто отмахиваются от этой проблемы, делая вид, что ее не существует и говорят: «вы либо доверяете ведущему компьютеру, либо нет». На самом деле, решение лежит на поверхности! Пусть ведомый и ведущий компьютеры периодически меняются ролями. Это достаточно просто реализуется на программном уровне и надежно защищает от взлома. Конечно, хакер может исправить свой экземпляр программы так, чтобы пули не кончались, но работать это будет только тогда, когда его компьютер будет ведущим! Согласен, даже такой «половинчатый хак» дает огромное преимущество в игре, однако, оба игрока находятся в равных условиях и хакерствовать можно каждый из них! Нету никакого «выделенного» компьютера, владелец которого приравнивается к богу!

Играть вдвоем довольно скучно и в какой-то момент возникает желание взять третьего. А где трое, там и пятеро. Какие проблемы ждут нас на этом пути? Ведущий компьютер может обслуживать множество ведомых игроков, количество которых (в теории) ограничивается пропускной способностью канала и мощностью процессора. Найти мощный процессор — не проблема, а с учетом того, что мы передаем не сами перемещения, а изменения направления с лихвой хватит и хлипкого модемного канала. Камень преткновения в другом: задумаемся, что произойдет, если ведущий компьютер внезапно отвалиться? (Разорвется соединения или его владелец просто устанет и отправится спать). В ситуации с двумя игроками никакой проблемы не возникает. Если один из игроков «исчезает», другой автоматически переходит в режим одиночной игры, но вот с тремя игроками такая стратегия уже не срабатывает. Получается, что один отдувается за всех! А оно ему надо?

Кто-то наверняка предложит установить выделенный сервер, только для трех-пяти игроков это будет слишком расточительным решением. Хорошо, поступим так: пусть все игроки устанавливают соединение не только с ведущим компьютером, но так же и друг с другом, по очереди становясь то ведомыми, то ведущими. Тогда, если ведущий компьютер исчезнет, он просто-напросто будет выброшен из очереди и ничего катастрофического не произойдет!

Как вариант, можно вообще отказаться от концепции «ведомого» и «ведущего».Каждый компьютер управляет движением «своего» игрока и «своих» монстров, рассылая информацию о перемещении всем остальным. Такая схема существенно упрощает проектирование и реализацию игры, снимая множество проблем, правда не все…

Рисунок 2 схема взаимодействия компьютеров в игре из трех игроков

Описанная выше схема страдает двумя серьезными недостатками. При большом количестве игроков объем трафика увеличивается настолько, что ни в какие каналы он уже не вмещается и все начинает дико тормозить, особенно в тот момент когда игроки сойдутся в смертельном поединке. Правило «семеро одного не ждут» тут не срабатывает и динамика игры определяется самым медленным компьютером в сети — все остальные терпеливо ждут пока придет подтверждение. Приходится усложнять протокол, и запрашивать подтверждения только у того, «против кого мы дружим». Допустим, игрок А пускает в игрока Б торпеду, а игрок С за этим внимательно наблюдает — интересно же. Так вот, чтобы не допустить рассинхонизации компьютеры А и Б вынуждены обмениваться подтверждениями, а компьютер С может и отдохнуть.

Проблема большого количества соединений снимается путем создания своеобразного «поезда». Вместо того, чтобы рассылать данные всем узам, компьютер А посылает их компьютеру B, тот посылает их (вместе со своими перемещениями) компьютеру С и т. д. Естественно, чтобы восстановить цепочку в случае «падения» одного из узлов, компьютер А должен знать адреса компьютера С и D, чтобы при необходимости установить с ними соединение. На самом деле, структура сети не обязательно должна быть линейной (это самый худший вариант). Вот типичный случай из жизни: компьютеры А и C находятся в локальной сети, а компьютер B – где-то далеко в Интернете. За каким чертом мы будем гонять трафик через B, когда логичнее соединить компьютеры так: B  A  C. И это действительно можно сделать!

динамическое балансирование нагрузки

Выбираем самый быстрый компьютер с мощным процессором (выбираем, естественно, автоматически — по скорости передачи данных и времени отклика на ping), подключаем к нему чуть-чуть менее быстрые компьютеры, которые будет нести на своих плечах еще менее быстрее и т. д. Тогда тормозные компьютеры окажутся задвинутыми в самый зад, а весь трафик ляжет на плечи узлов, висящих на быстрых каналах. Эта схема не является заданной раз и навсегда. Она может (и должна!) динамически обновляться. Главное, выбрать протокол обмена так, чтобы равномерно распределить трафик на все узлы.

Чтобы игроки (а их количество в этой схеме измеряется десятками) не толпились на крошечном пяточке, необходимо создать достаточно большое игровое пространство, которое никакой отдельно взятый Pentium не сможет обсчитать. Значит, нам потребуется распределенная система, нарезающая «объекты» игрового мира на куски и раскидывающая их по наименее загруженным машинам.

Использованная ранее схема «каждый компьютер обрабатывает своих монстров» уже не катит. И вот почему: как уже говорилось, когда некоторый объект (монстр или игрок) попадет в поле зрения «чужого» объекта (монстра или другого игрока) для синхронизации игровых миров требуется задействовать механизм подтверждений, а это тормоза — вполне терпимые в случае трех-пяти игроков, но неприменимые в мире, где их десятки! Допустим, мы модернизируем протокол обмена и решим: пусть каждый компьютер обрабатывает только тех монстров, которые находятся в его игровом пространстве (то есть если монстр уходит из «своего» игрового пространства, его обработка передается другому компьютеру). Все будет замечательно работать до тех пор, пока все монстры не соберутся в пространстве одного игрока и тогда его компьютер конкретно ляжет, будучи не в состоянии обрабатывать эту армию в реальном времени. Так что без распределенной системы с автоматическим балансированием нагрузки — никуда. К сожалению, подобные системы очень сложны реализации и отладке. В общем случае они не оправдывают себя и становится выгоднее установить выделенный сервер.

Рисунок 3 система обмена в игре со множеством игроков

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

Это были достоинства. А теперь поговорим о недостатках. Поддержка постоянно работающего сервера — удовольствие не из дешевых. Обычного хостиига за 8$ в год с Perl, PHP и MySQL тут будет явно недостаточно. Нам потребуется прямой доступ к машине с возможностью выполнять двоичные файлы, написанные под XP (или что у нас там) и очень мощный процессор, способный поддерживать огромный игровой мир в подвижном состоянии. Такое может позволить себе только крупная компания! Но это ладно. Это вопросы финансов.

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

К тому же игроки (живые люди, а не фигурки) находятся в полной зависимости от сервера, компании-разработчика и своего провайдера. Уже нельзя собраться в куче и поиграть по локалке. Надо обязательно выходить в интернет или… устанавливать свой собственный сервер, только как его установишь, если в открытом доступе его нет?

Очень остро встает и проблема читерства. Чем больше игроков, тем выше вероятность обмана. И хотя код, управляющий игровым пространством, находится на сервере, в который непосредственно залезть никак нельзя, хакеры могут повесить бота, то есть написать скрипт, управляющий движением игрока и с нечеловеческой меткостью и быстротой мочащий все, что попадается ему на глаза. Увы! Защититься от этого никак нельзя! Единственное, что остается — распознавать обманщиков по слишком большому количеству трупов, оставленных в единицу времени и ставим им бан, однако, всегда найдется игрок, который поднимет вселенский визг, возмущаясь за что его так?! Ведь он играл по всем правилам, ну а меткость и реакция, как известно, не порок.

В принципе, обработку игровых миров можно поручить и клиентам, а сервер будет только коммутировать потоки данных и распределять нагрузку по узлам. Короче говоря, мы приходим к той же самой самоорганизующийся системе, но только с центральным сервером. Это снимает требование к вычислительной мощности сервера и отменяет принудительную синхронизацию, однако, вызывает много путаницы с «отрубавшимися» клиентами и серверу приходится постоянно перепроверять — был ли обработан данный блок игрового пространства или нет. При огромном количестве клиентов это нереально. Поэтому, на практике зачастую используются гибридные схемы. Весь игровой мир храниться на сервере, но максимум перемещений обрабатываются локально. То есть, схватка двух игроков неизбежно происходит через сервер, а разборки игрока с монстром, может обработать и сам клиент, доложив серверу конечный результат: кто кого порешил. Естественно, в этом случае со стороны клиента возможно наглое читерство.

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

Рисунок 4 игровой мир с выделенным сервером

Какой протокол выбрать? Очень многие разработчики выбирают TCP, потому что он самостоятельно заботиться о поддержании надежного соединения, гарантирует доставку данных и т. д. Все это так, мыщъх ничего не оспаривает, но все-таки выбор TCP будет фатальной ошибкой и колоссальным техническим просчетом. Все дело в том, TCP ориентирован на постоянное поддержание соединение и непрерывную передачу данных. С ним тесно связано мерзкое понятие «медленного страта» (подробности о котором можно прочитать в любой книжке по TCP/IP). Крохотные пакеты отправлять крайне нерентабельно. По ряду причин они доходят с большими задержками, а для синхронизации игровых миров это более чем актуально!

Протокол UDP доставляет пакеты намного быстрее, однако, не гарантирует успешности доставки. Можно (и нужно!) конечно организовать собственную службу подтверждений, только это ничего не меняет. Послали мы в пакет, а в ответ вместо подтверждения — тишина. Повторить передачу или подождать еще? А если ждать то сколько? А, может, стоит продублировать посылку по TCP? Увы, чудес не бывает и сам TCP действует точно так же, как и мы — подтверждает приемку или повторяет пересылку по тайм-аутуту. По своему личному опыту работы в плохих сетях, мыщъх может сказать, что «шторм» UDP пакетов все-таки предпочтительнее, однако, он осуществим только если пропускная способность канала с лихвой покрывает наши потребности в трафике иначе случится сплошной затор и затык. Так же, можно взвести бит срочности, однако, судя по всему большинство провайдеров его нагло игнорирует и он не сильно влияет на скорость обмена данных.

Тем не менее, TCP по ряду параметров все-таки удобнее UDP, поэтому можно использовать гибридную схему, передавая по TCP несрочные данные, а по UDP — критическую информацию о перемещениях объектов (типа торпеды, пущенной мыщъху под хвост). Как мы увидим ниже, в некоторых случаях протокол UDP недоступен и тогда приходится работать только по TCP. Это не слишком усложняет программу — основная информация все равно передается по TCP, и если UDP отсутствует, это всего лишь замедлит синхронизацию и вызовет некоторые тормоза, но игровой мир не рухнет и в программисту не придется ничего переписывать.

Еще меньшим оверхидом обладает протокол IP, который есть смысл использовать в игровых мирах, рассчитанных на большое количество игроков, в противном случае выигрыш не стоит приложенных усилий, тем более, что через IP удается работать далеко не всегда.

Вообще говоря, существует огромное количество самых разнообразных протоколов, но мы не будет рассматривать их здесь, поскольку TCP/UDP для наших задач вполне достаточно.

При разработке сетевой игры следует учитывать, что прямой доступ в интернет имеется далеко не у всех пользователь и брандмауэр и proxy-сервер вовсе не пустые слова. Зачем нам отсекать потенциальных клиентов? Давайте научим нашу программу работать по HTTP и поддерживать Proxy — сколько людей нам скажут спасибо! При работе с выделенным игровым сервером никаких проблем не возникает (исключая «медленную синхронизацию», в связи со значительной латентностью TCP, да еще и «проксированного» но тут уже ничего не поделаешь!) — просто поддерживаем Proxy-протокол и все. Ради прикола можно предусмотреть связь по ICMP, а что? Очень даже неплохой способ связи!

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

А если оба игрока находятся за брандмауэром? (естественно, каждый за своим). Тогда пробить тоннель прямого соединения уже не получится (ну разве только через ICMP или NAT, но это уже тема отдельного разговора) и придется подключаться к кому еще. К тому кто находится в диком Интернете безо всяких там проки и брандмауэров. Устанавливать централизованный сервер совершенно необязательно! Достаточно найти еще одного игрока (третьим будешь, да?) и соединиться через него! Чем популярнее будет ваша игра — тем проще это будет сделать, так что дерзайте!

Разработка сетевых игр — это захватывающее занятие, а сам игровой процесс отрывает хвост с головой, ведь это не с тупым компьютером сражаться который только и умеет, что посылать монстров по прямой, тут сталкивается живой интеллект реальных игроков от которых можно ожидать любых хитростей и тактических маневров! Естественно, в процессе написания игру необходимо тестировать, то есть попросту говоря играть. По сети. Ага! Какое там программирование!

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