Создание аркадных игр для мобильных устройств на Java

Создание аркадных игр для мобильных устройств на Java

Начало в КГ № 5

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

Вот еще несколько вещей, которые будут добавлены в сценарий игры, чтобы сделать ее интересней. Некоторые уровни будут содержать дополнительные жизни, которые могут подбираться любым из игроков. Также в некоторых местах будут находиться объекты, взяв которые, игрок получает дополнительную энергию: на некоторое время игрок будет двигаться вдвое быстрее обычного. Если игрок натыкается на одно из вышеупомянутых препятствий, то он автоматически лишается одной жизни. Каждый из игроков начинает игру с тремя жизнями, и когда количество жизней обращается в ноль, игра завершается.
И, наконец, некоторые комнаты будут содержать объекты-лопаты и/или реактивные пакеты. Как только игрок подобрал реактивный пакет и нажал клавишу '1' на своем устройстве, он начинает летать. При полете игрок может безболезненно пересекать большинство препятствий. В этом состоянии стены, объекты-яды и слизь не страшны игроку. Однако это не спасает его от столкновения с границами уровня или своим соперником. Состояние полета отображается на экране другим стилем прорисовки игрока, не как в обычном режиме ползания. Игрок может летать только небольшой определенный отрезок времени. Как только время проходит, все препятствия возобновляют свою деструктивную функцию по отношению к этому игроку.

Аналогичные правила действуют и в случае, когда игрок находится в землеройном состоянии. Это происходит, когда игрок подбирает объект-лопату и нажимает клавишу '3' на своем мобильном устройстве. Обратите внимание: чтобы произошло столкновение двух игроков, необходимо, чтобы они оба находились в одинаковом состоянии, т.е. под землей, в состоянии полета или же в обычном состоянии. Иначе они могут без потерь пересекать друг друга, находясь в разных состояниях. Внизу экрана отображается вся статистическая информация, касающаяся количества оставшихся жизней у игрока, а также имеет или не имеет игрок какие-либо объекты (например, реактивные пакеты или лопаты).
При запуске игры на экран выводится специальный информационный экран. Этот экран представляет собой краткое введение и помощь в игре. Очень краткая справка представляет основные цели игры, ее объекты и управление игрой.
Несмотря на то, что игра достаточно примитивна в управлении, вы должны придерживаться следующего подхода. Вы не должны заставлять людей читать руководство по игре внушительных размеров или же разделы справки, чтобы они могли начать играть. Однако при этом вы не должны совсем оставлять их без справочной информации. Запомните, что игры на мобильных устройствах обычно используют немного с другой целью, нежели игры для стационарных персональных компьютеров. Обычно люди, играя на мобильных устройствах, хотят не более чем убить несколько выдавшихся свободных минут в ожидании чего-то. И при этом они вряд ли хотели бы читать объемные инструкции, чтобы приступить к игре.

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

Из-за этих задержек и отсутствия синхронизации между устройствами в беспроводной сети будут возникать проблемы в процессе игры. Рассмотрим ситуацию, когда игрок A направляется прямо на близлежащую стенку. Игрок A оценил ситуацию и в последнюю секунду решил повернуть налево, чтобы избежать столкновения со стенкой. На устройстве A игрок избегает столкновения, и игра продолжается нормально. Как часть сетевой логики, устройство A посылает код нажатой клавиши устройству B. Это событие прибывает на устройство B с задержкой в одну секунду. Однако на устройстве B таймер уже пришел к следующему значению. Таким образом, на устройстве B игрок A сталкивается со стенкой до того, как оно приступит к обработке только что прибывшего сообщения о нажатой игроком A кнопке. Таким образом, получается неоднозначность. На устройстве A игрок A продолжает играть как ни в чем не бывало, а на устройстве B игрок A, столкнувшись со стенкой, теряет очередную жизнь. Даже учитывая очень небольшие задержки в сети, рано или поздно такая ситуация может возникнуть. Подобные проблемы с синхронизацией делают игру неиграбельной, поэтому от них нужно избавляться в самом начале, на этапе проектирования.

Мы изберем другой подход к проектированию наших червяков. Пусть будет один сервер, который действует как независимый арбитр между двумя игроками. Сервер будет нести индивидуальную ответственность за управление состоянием игры. Оба клиента имеют возможность непрерывно доставлять события о нажатых клавишах на сервер. Сервер сливает эти события в одно целое и обрабатывает их в фиксированные интервалы времени. По завершению каждого интервала сервер обновляет информацию о состоянии игры на основании имеющихся (полученных от игроков) данных и генерирует события, которые в точности отражают измененное состояние игры. Когда состояние игрока никак не изменяется, никаких событий генерироваться не будет. Сервер будет посылать события обоим игрокам. Они в свою очередь, согласно полученным данным, будут обновлять визуальную картину игры на экранах мобильных устройств. Таким образом, клиенты не хранят на своей стороне совсем никакой информации о состоянии игры; вместо этого они просто отправляют все возникающие события на сервер для обработки. Клиентские устройства фактически представляют собой удаленные терминалы с экраном и клавишами ввода.

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

Данные о состоянии игры спроектированы таким образом, что различие для двух конкретных состояний может быть передано минимальным объемом данных. Каждый уровень состоит из относительно небольшого числа клеток. При этом каждая клетка может находиться в одном из нескольких состояний (пусто, реактивный пакет, местонахождение первого игрока и т.д.). Каждый из игроков всегда располагается лишь на одной из клеток и может переходить на другую за один ход. Не принимая во внимание некоторые специализированные события, во время игры события в основном будут представлять собой обновления состояний клеток (состоящих из координат x и y и из идентификатора состояния клетки ID).
Код на стороне клиента тоже может быть разделен на две части: независимая от игры часть для работы с сетью и непосредственно компонент визуализации игры. С помощью кода для работы с сетью клиент регистрируется на сервере и создает подходящий компонент для визуализации согласно свойству, прочитанному из файла-описателя MIDlet'а. Также клиент определяет и адрес сервера. Таким образом, изменения обоих атрибутов могут производиться без необходимости повторной компиляции кода. Код визуализации делает две вещи:
1. Отслеживает нажатые пользователем клавиши и отсылает их на сервер с помощью кода для работы с сетью.
2. Обрабатывает события состояния игры, полученные с сервера, и в соответствии с этими событиями обновляет экран клиентского устройства.
Сервер и клиенты должны иметь возможность обмениваться данными, чтобы отправлять события об изменениях и сообщать друг другу о других изменениях состояния игры. В качестве подходящего низлежащего коммуникационного механизма мы выбрали сокеты TCP/IP. Сокеты достаточно быстры и имеют минимум проблем. Но при этом они не предоставляют удобный интерфейс для обмена данными комплексного типа. Данные, которые можно передавать с помощью этого механизма — это фактически массивы байтов. Платформа J2SE предоставляет механизм, который называется сериализацией объектов и позволяет разработчикам представлять большинство объектов Java в виде массивов байтов. Однако, этот программный интерфейс (API) пока недоступен для мобильных устройств (платформа J2ME).

Мы определяем несколько типов данных, удобных для взаимодействия между клиентом и сервером. Они представляют собой нормальные Java-объекты, но, несмотря на это, их можно будет легко конвертировать в массив целых чисел для передачи посредством сокетов. В качестве дополнительного абстрактного слоя мы будем заворачивать наши данные в специальные сообщения, у которых будет заголовок с данными о типе сообщения, серийном номере и размере. Благодаря такому подходу принимающей стороне будет проще обрабатывать получаемые данные. Т.е. на другом конце приложение может легко определить природу тех или иных полученных данных.
Наш вариант сетевой игры состоит из уровней, которые могут быть легко представлены в виде данных. Следовательно, у нас нет необходимости зашивать определение игровых уровней в сам код игры. Данные об уровнях будут храниться отдельно и могут быть прочитаны уже на этапе работы игры. Первые несколько комнат можно представить в виде однородных файлов. Однако это не означает, что вы не можете использовать в качестве источника данных об уровнях ресурс, который находится где-то в сети, или что-то другое. Наш код будет принимать ссылку на входной поток, который содержит необходимые данные. А источником этого потока может быть не только обычный файл. Формат для описания уровней нагляден и прост. Например, данные для представления первого игрового уровня выглядят следующим образом:
....................... ....................... ....................... ......................
. ....................... ............|.......... ............|.......... ............|.......... ..
..........|.......... 1...........|.......... ............|...@...... 2...........|.......... ......
......|.......... ............|.......... ............|.......... ............|.......... ..........
............. ....................... ....................... ....................... ..............
.........

Это массив с 23-мя символами в ширину и 21-им в высоту. Пустой клетке назначен символ '.'. Стартовые места расположения игроков заданы символами '1' и '2'. Символ '|' обозначает стенку, а '@' — это целевая клетка, куда должен прийти один из игроков, чтобы победить на этом уровне. Таким образом, для тестирования вы можете использовать обычный текстовый редактор для создания новых уровней. Если вы решите позже написать свой собственный редактор уровней, то конвертирование графического представления уровня в подобный текстовый формат не составит большого труда.

Реализация
Классы, которые будут представлены далее, разделены на три группы: общие для клиента и для сервера, классы, составляющие только клиентское приложение, и классы сервера.

Начнем с общих классов.

Класс Data
Класс Data — это суперкласс (родитель) для всех типов данных, которые будут писаться и читаться из сокетов. Внутренне объект данных представляет собой массив целых чисел (integer). Это позволяет с легкостью конвертировать объекты этого класса в данные, совместимые с сокетами, т.е. теми, из которых мы будем читать и в которые будем эти самые данные писать. Вместо того чтобы иметь дело с многочисленными кусками данных переменного размера и разного типа, в нашем случае все данные будут представлены в виде компактного массива целых чисел. Подклассы класса Data, которые используют переменную buf в качестве внутренней структуры данных, с таким подходом дают ощутимые результаты. Они обеспечивают методы доступа (аксессоры), которые имитируют типы данных, удобные для отдельных подклассов (смотрите класс Item для примера).
Код класса Data: http://java.sun.com/blueprints/code/slugs10/com/sun/j2me/blueprints/slugs/shared/Data.java.html

Класс Item
Первоначальная задача класса Item — представлять изменения игрового состояния. Каждый объект этого класса содержит координаты x, y и идентификатор ID. В классе Item определяется целый список таких идентификаторов, которые в основном являются описанием элементов клеток игровых уровней. Но некоторые из них используются для внутренних целей, как, например, идентификатор JETPACK_COUNT.
Обратите внимание, каким образом аксессоры прячут внутреннюю реализацию "логических" атрибутов x, y и статуса. Поскольку изначально эти атрибуты хранятся в переменной buf экземпляра суперкласса Data.
Суперкласс Item также определяет временный бит данных, флаг. Этот бит используется кодом серверной части приложения и не передается клиенту. Игровой движок хранит экземпляр класса Item для каждой клетки уровня. Каждая клетка, которую нужно будет отослать клиентам (обновить состояние), маркируется этим флажком. После каждого обновления движок очищает этот флаг во всех клетках.
Код класса Item: http://java.sun.com/blueprints/code/slugs10/com/sun/j2me/blueprints/slugs/shared/Item.java.html

Класс Frame
Класс Frame представляет полный список изменений, которые произошли между двумя снимками состояний игры. Каждый элемент этого списка представлен в виде объекта Item. Поскольку элементы, описывающие игроков, на стороне клиента должны трактоваться особым образом, они сгруппированы в начале списка. Метод getPlayerCount() возвращает количество игроков. Так же, как и класс Item, класс Frame внутренне представляет свои данные с помощью переменной buf своего суперкласса, таким образом существенно упрощая передачу экземпляров класса Frame через сокеты.
Код класса Frame: http://java.sun.com/blueprints/code/slugs10/com/sun/j2me/blueprints/slugs/shared/Frame.java.html

Класс Message
Класс Message упрощает процесс обмена данными между сервером и клиентами. В общем случае, когда данные поступают на вход в сокет принимающей стороны, код, который эти данные обрабатывает, не знает, что это за данные, их размер и когда они были посланы. Именно поэтому класс Message служит конвертом для данных. Каждый объект класса Message имеет тип, который определяет, какого вида данные мы пересылаем, указатель размера данных и номер фрейма. Все используемые типы сообщений предопределены: SIGNUP, SIGNUP_ACK, CLI-ENT_STATUS, SERVER_STATUS, ERROR и SIGNOFF. Сервер устанавливает номера фреймов (кадров). Каждый фрейм — это список изменений состояний игры в определенный момент времени. Номера фреймов создаются последовательно, что позволяет обнаруживать потерянные или задержавшиеся данные.
В этом классе особое место занимают два метода: archive() и createFromStream(). Метод archive() выводит содержание данного экземпляра класса в выходной поток (например, в возвращаемый следующим выражением new DataOutputStre-am(socket.getOutputStream())). В то же время метод createFrom Stream() создает новый экземпляр этого класса в соответствии с данными, прочитанными из входного потока (например, new DataInputStream(socket. getInputStream()) ).
Код класса Message: http://java.sun.com/blueprints/code/slugs10/com/sun/j2me/blueprints/slugs/shared/Message.java.html

Интерфейс PlayerActions
Интерфейс PlayerActions позволяет упростить процесс взаимодействия клиента с сервером. Этот интерфейс просто определяет ряд символических констант, которые будут использоваться в коде приложений вместо обычных чисел. Эти символические константы описывают, что игрок делает.
Код интерфейса PlayerActions: http://java.sun.com/blueprints/code/slugs10/com/sun/j2me/blueprints/slugs/shared/PlayerActions.java.html

Интерфейс Server
Server — это небольшой интерфейс, который используется как клиентами, так и сервером для того, чтобы определить, какой из TCP/IP портов будет "слушаться" сервером на предмет входящих соединений клиентов.
Код интерфейса Server: http://java.sun.com/blueprints/code/slugs10/com/sun/j2me/blueprints/slugs/shared/Server.java.html

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

По материлам Kay Neuenhofen
Алексей Литвинюк, litvinuke@tut.by .



Компьютерная газета. Статья была опубликована в номере 10 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета