Главная » Программирование игр под Android » БОЛЬШОЙ ПРЫГУН:  ДВУХМЕРНАЯ ИГРА, НАПИСАННАЯ С ПОМОЩЬЮ OPENGL ES – РАЗРАБОТКА ИГР ДЛЯ ОС ANDROID

0

 

Пришло время использовать все, что вы узнали, для того, чтобы написать игру.

Однако для нашей следующей игры я решил выбрать что-нибудь более простое. Мы реализуем игру в жанре попрыгунчик (jump-em-up), подобную играм Abduction или Doodle Jump. Как и в случае с игрой Мистер Ном, мы начнем с определения игровой механики.

ОСНОВНАЯ ИГРОВАЯ МЕХАНИКА

Я предлагаю вам установить игру Abduction на ваш телефон с ОС Android или просмотреть видеоролики об этой игре в Интернете. Из этого примера мы можем почерпнуть основную игровую механику нашей игры, которая будет называться Большой прыгун. Вот некоторые детали.

Персонаж постоянно прыгает вверх, передвигаясь с платформы на платформу. Игровой мир простирается по вертикали на множество экранов.

Горизонтальным передвижением персонажа можно управлять, наклоняя телефон влево или вправо.

Когда персонаж пересекает одну из боковых границ экрана, он появляется с противоположной стороны экрана.

Платформы могут быть как неподвижные, так и перемещающиеся по горизонтали.

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

На своем пути персонаж может собирать предметы для того, чтобы зарабатывать очки.

Кроме монет персонажу могут встретиться пружины или платформы, позволяющие ему прыгать выше.

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

Игра также заканчивается, когда персонаж падает за нижнюю границу экрана.

На вершине уровня находится какая-либо цель. Когда персонаж достигает ее, начинается новый уровень.

Хотя этот список и длиннее, чем тот, который мы создали для игры Мистер Ном, он не кажется более сложным. На рис. 9.1 показан исходный макет игровых принципов. В этот раз для создания макета я использовал программу Paint.NET. Давайте теперь придумаем сюжет для игры.

Рис. 9.1. Исходный макет, содержащий игровые механики; на нем показаны персонаж, платформы, монеты, отрицательные персонажи и цель, расположенная в верхней части уровня

ПРЕДЫСТОРИЯ И СТИЛЬ

Мы собираемся использовать всю нашу креативность и создать уникальный сюжет для игры.

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

Недуг Боба в конечном итоге начинает приносить ему пользу, и он отправляется в путь за своей возлюбленной, сражаясь со злобной армией белок.

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

ЭКРАНЫ И ПЕРЕХОДЫ

Теперь мы можем определить экраны игры и переходы между ними. Опишем их тем же способом, который использовался для экранов игры Мистер Ном.

У нас будет основной экран с логотипом; пункты меню PLAY (Играть), HIGHSCORES (Рекорды), и HELP (Помощь); а также кнопка, позволяющая включить или выключить звук.

Кроме того, у нас будет основной экран игры, в котором пользователю будет задаваться вопрос о том, готов ли он играть, а также, соответственно, обрабатывать состояния выполнения игры, паузы, окончания игры и перехода на новый уровень. Отличие от игры Мистер Ном только одно – состояние перехода на новый уровень, в которое переключается игра в момент, когда Боб попадает в замок. В этом случае генерируется новый уровень, и Боб снова начинает свой путь с самого низа мира, при этом сохраняя набранные очки.

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

Наконец, в игре будет также экран помощи, на котором будет размещена информация об игровых механиках и цели игры. Кроме того, мы разместим там описание способа управления персонажем. Современные дети не сталкиваются со сложностями, с которыми сталкивались в 1980-х и ранних 1990-х годах мы, когда игры не подсказывали игрокам о том, как в них играть.

Этот список более-менее похож на тот, который мы создали для игры Мистер Ном. На рис. 9.2 показаны все экраны игры, а также переходы между ними. Обратите внимание, на основном экране игры и его подэкранах нет никаких кнопок, кроме кнопки Пауза. Пользователи будут интуитивно трогать экран, когда им зададут вопрос об их готовности начать игру.

Рис. 9.2. Все экраны игры Большой прыгун и переходы между ними

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

ОПРЕДЕЛЕНИЕ ИГРОВОГО МИРА

Мы снова столкнулись с классической проблемой что было раньше, курица или яйцо?. Из предыдущей главы вы узнали, что существует соответствие между единицами измерения игрового мира (например, метрами) и пикселами. Объекты физически определены в пространстве мира. Ограничивающие их фигуры и позиции используют в качестве единицы измерения метры, скорости измеряются в метрах в секунду. Графическое представление объектов определяется в пикселах, поэтому нам придется установить некоторое соответствие между ними и метрами. Эту проблему возможно обойти, если сначала определить разрешение используемых графических ресурсов. Как и в случае с игрой Мистер Ном, мы используем разрешение 320 х 480 пикселов (соотношение сторон равно 1,5). Далее необходимо установить соответствие между пикселями и метрами нашего мира. Макет, изображенный на рис. 9.1, позволяет получить представление о том, сколько места занимают различные объекты, а также их пропорции относительно друг друга. Обычно для двухмерных игр я использую следующий масштаб – 32 пиксела на один метр. Давайте теперь наложим на экран, чьи размеры составляют 320 х 480 пикселов, сетку, каждая клетка которой имеет размеры 32 х 32 пиксела. На рис. 9.3 показан макет с наложенной сеткой.

Рис. 9.3. Макет, на который наложена сетка; каждая клетка имеет размер 32 х 32 пиксела и соответствует площади игрового мира размером 1 х 1 м

Рисунок 9.3, конечно, является несколько ненатуральным. Я разместил объекты на экране таким образом, что они замечательно помещаются в клетки. В реальной игре мы расположим объекты по координатам, содержащим и дробные части.

Итак, что можно узнать из рис. 9.3? Прежде всего стало возможно напрямую определить размеры каждого объекта игрового мира в метрах. Следующие значения можно использовать для создания ограничивающих прямоугольников игровых объектов.

Боб имеет размеры 0,8 х 0,8 м; он не занимает целую клетку.

 Платформа имеет размеры 2 х 0,5 м, занимая две клетки по горизонтали и половину клетки по вертикали.

Монета имеет размеры 0,8 х 0,5 м. Она занимает практически целую клетку по вертикали и половину клетки по горизонтали.

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

Белка имеет размеры 1 х 0,8 м.

Замок – 0,8 х 0,8 м.

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

Из рис. 9.3 можно также определить размеры окна просмотра. Игрок сможет увидеть область игрового мира площадью 10 х 15 м.

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

Вектор ускорения, создаваемый гравитацией, равен (0, -13) м/с2, что несколько больше, чем гравитация на планете Земля, а также значения, использованного нами в примере с пушкой.

Исходный вектор скорости прыжка Боба равен (0,11) м/с. Обратите внимание на то, что этот вектор влияет лишь на передвижение по вертикали. Горизонтальное передвижение будет определяться согласно данным, полученным от акселерометра.

Вектор скорости прыжка Боба будет увеличиваться в 1,5 раза, когда он заденет пружину. Он будет равен (0; 16,5) м/с. Опять же, это значение получено лишь экспериментальным путем.

Скорость передвижения Боба по горизонтали равна 20 м/с. Обратите внимание на то, что она не имеет направления, то есть не является вектором. Далее я объясню, как именно использовать данные, полученные от акселерометра.

Белки будут патрулировать пространство слева направо и наоборот. У них будет постоянная скорость передвижения, равная 3 м/с. Если выразить ее вектором, то она будет равна (-3,0) м/с, когда белка двигается влево, и (3,0) м/с, когда белка двигается вправо.

Так как же Боб будет перемещаться по горизонтали? Скорость передвижения по горизонтали, которую мы определили ранее, на самом деле является максимальной скоростью передвижения по горизонтали. В зависимости от того, насколько игрок наклонит свой телефон, скорость перемещения Боба по горизонтали будет изменяться от 0 (нет наклона) до 20 м/с (максимальный наклон телефона в одну сторону).

Мы будем использовать наклон акселерометра по оси х, поскольку игра будет работать в портретной ориентации. В то время, когда телефон не наклонен, ось сообщит об ускорении, равном 0 м/с2. Когда же телефон максимально наклонен влево, при этом оказавшись в альбомном режиме, ось сообщит об ускорении, равном приблизительно -10 м/с2. В случае когда телефон максимально наклонен вправо, ось сообщит об ускорении, равном приблизительно 10 м/с2. Нам нужно нормализовать показания акселерометра, разделив их на максимальное абсолютное значение (10), а затем умножив их на максимальную скорость перемещения Боба по горизонтали. Поэтому Боб будет передвигаться влево или вправо со скоростью 20 м/с, когда телефон максимально наклонен в одну из сторон. Скорость будет уменьшаться с уменьшением угла наклона телефона. Боб может дважды за секунду пересечь экран, когда телефон максимально наклонен.

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

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

Теперь рассмотрим ресурсы, которые нам понадобятся для создания игры.

СОЗДАНИЕ РЕСУРСОВ

Наша новая игра будет иметь два типа графических ресурсов: элементы пользовательского интерфейса и непосредственно игровые элементы (элементы игрового мира). Начнем с описания элементов пользовательского интерфейса.

Элементы пользовательского интерфейса

Первое, на что следует обратить внимание, – элементы пользовательского интерфейса (кнопки, логотипы и т. д.) не зависят от преобразования единиц измерения из пикселов в метры, которое было определено ранее. Как и в случае с игрой Мистер Ном, следует разработать их так, чтобы они соответствовали конкретному разрешению – в нашем случае 320 х 480 пикселов. Взглянув на рис. 9.2 можно определить, какие именно элементы пользовательского интерфейса будет иметь игра.

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

Рис. 9.4. Различные кнопки, каждая из которых имеет размер 64 х 64 пиксела

Я всегда создаю все графические ресурсы на сетке, ячейки которой имеют размер 32 х 32 или 64 х 64 пиксела. Кнопки, показанные на рис. 9.4, находятся на сетке с ячейками 64 х 64 пиксела. Кнопки, которые располагаются в верхнем ряду сетки, использованы на экране главного меню. Они сигнализируют о том, включен ли звук. Стрелка, расположенная в нижней левой ячейке сетки, используется на нескольких экранах для перехода на следующий экран. Кнопка, находящаяся в правой нижней ячейке сетки, применяется на главном экране игры и позволяет поставить игру на паузу.

Вы можете задаться вопросом, почему нет стрелки, показывающей вправо. Помните, что с помощью нашего замечательного класса Spri tebatcher возможно легко переворачивать картинки, задавая им отрицательные значения ширины и/или высоты.

Мы используем этот трюк на нескольких графических ресурсах для того, чтобы сэкономить немного памяти.

Далее рассмотрим элементы, которые понадобятся на экране главного меню. На нем разместятся логотип, пункты меню и фоновая картинка. Все эти элементы показаны на рис. 9.5.

Рис. 9.5. Фоновое изображение, пункты главного меню и логотип

Фоновое изображение используется на всех экранах, а не только на экране главного меню. Его размеры в точности совпадают с размерами целевого разрешения 320 х 480 пикселов. Пункты главного меню имеют размер 300 х 110 пикселов. Для них был выбран черный фон, поскольку белое на белом выглядит совсем не так хорошо. В реальном изображении фон, конечно же, создан из прозрачных пикселов. Размеры логотипа составляют 274 х 142 пиксела, он имеет несколько прозрачных пикселов по углам.

Далее необходимо создать изображения для экранов помощи. Я поленился и вместо того, чтобы составлять их из нескольких элементов, нарисовал их как полноэкранные изображения размером 320 х 480. Это немного сократит размер кода, отрисовывающего их, и не добавит слишком много объема нашей программе. Вы можете увидеть все экраны помощи на рис. 9.2. Единственный элемент, который мы добавим на эти изображения, – кнопка со стрелкой.

Для экрана рекордов мы повторно используем часть изображения экрана главного меню, в которой есть пункт меню HIGHSCORES (Рекорды). Сами результаты будут отображаться при помощи специального приема, который мы рассмотрим далее в этой главе. Остальная часть экрана будет составлена из фонового изображения и кнопки.

Экран игры имеет еще несколько текстовых элементов пользовательского интерфейса, например метку READY? (Готовы?), пункты меню, появляющегося, когда игра приостановлена (RESUME (Продолжить) и QUIT (Выйти)), а также метку GAME OVER (Игра окончена). На рис. 9.6 они показаны во всей красе.

Рис. 9.6. Метки READY? (Готовы?), RESUME (Продолжить), QUIT (Выйти) и GAME OVER (Игра окончена)

Обработка текста с помощью растровых шрифтов

Итак, как же следует отрисовывать прочие текстовые элементы игрового экрана? Мы будем использовать прием, задействованный при создании игры Мистер Ном для отрисовки результатов. В данном случае нам нужно применить набор не только цифр, но и символов. Используем атлас изображений, в котором каждое под-изображение будет представлять символ (например, О или а). Такой атлас изображений называется растровым шрифтом. На рис. 9.7 показан растровый шрифт, который мы будем применять.

Рис. 9.7. Растровый шрифт

Черный фон и сетка, показанные на рис. 9.7, конечно, не являются частью изображений, входящих в состав растрового шрифта. Использование таких шрифтов – очень старый прием отрисовки текста на экране в игре. Обычно они состоят из изображений, созданных для набора символов ASCII. Один такой символ называется глифом. ASCII – это один из Юникода. Всего в наборе ASCII 128 символов (табл. 9.1).

Таблица 9.1. Символы ASCII и их десятичное, шестнадцатеричное и восьмеричное представления

Из этих 128 символов печатаемыми являются лишь 96 (они имеют номера с 32-го по 126-й). Наш растровый шрифт состоит исключительно из печатаемых символов. Его первый ряд содержит с 32-го по 47-й символы, следующий ряд – с 48-го по 63-й и т. д. Использование ASCII полезно лишь в том случае, когда необходимо отобразить на экране текст, состоящий из символов стандартного латинского алфавита. Существует расширенный формат ASCII, который имеет символы с номерами с 128-го по 255-й. Эти символы используются для кодирования наиболее часто встречающихся символов западных языков, например б или ё. Более объемные наборы символов (например, для китайского или арабского языков) представляются с помощью Юникода и не могут быть закодированы при помощи ASCII. Для нашей игры будет достаточно набора символов ASCII.

Как же отобразить текст с использованием растрового шрифта? Оказывается, это очень просто. Сначала следует создать 96 текстурных регионов, каждый из которых будет указывать на глиф растрового шрифта. Эти регионы возможно хранить в массиве, например, следующим образом:

Строки в языке Java кодируются при помощи 16-битного Юникода. К счастью для нас, символы ASCII, использованные в нашем растровом шрифте имеют одинаковые номера как в ASCII, так и в Юникод. Чтобы получить регион для символа строки на языке Java, необходимо сделать лишь следующее:

Так мы получаем прямой индекс массива текстурных регионов. Мы просто отнимаем номер символа пробела (32) от порядкового номера текущего символа строки. Если индекс меньше нуля или больше 95, то можно определить, что это символ Юникод, не входящий в наш растровый шрифт. Обычно следует просто игнорировать такой символ.

Чтобы отрисовать несколько символов в строке, необходимо знать, как много пространства должно разделять эти символы. Растровый шрифт, показанный на рис. 9.7, является так называемым моноширинным шрифтом. Это значит, что каждый глиф имеет одинаковую ширину. Глифы нашего растрового шрифта имеют размер 16 х 20 пикселов каждый. Для перемещения позиции отрисовки от символа к символу нам следует всего лишь добавить к ней 20 пикселов. Количество пикселов, на которое перемещается позиция отрисовки от символа к символу, называется смещением. В нашем растровом шрифте оно фиксировано, но в общем случае оно является переменной величиной, которая изменяется в зависимости от отрисовываемого символа. Более сложная форма расчета смещения принимает во внимание как отрисовываемый в данный момент символ, так и следующий за ним. Такой прием называется кернингом, вы можете узнать о нем больше в Интернете. Мы будем использовать лишь моноширинные растровые шрифты, поскольку с ними наша задача значительно упрощается.

Итак, как я сгенерировал этот растровый шрифт ASCII? Я использовал один из многих инструментов для генерации атласов текстур, доступных в Сети. Тот, что применил я, называется Bitmap Font Generator. Он выпущен компанией Codehead и распространяется бесплатно. Вы можете выбрать файл шрифта, хранящийся на вашем компьютере, определить высоту шрифта, и генератор создаст изображение, содержащее символы набора ASCII. Этот инструмент имеет также и другие возможности, которые я не могу обсудить в рамках этой и. Советую вам ознакомиться с ними самостоятельно.

Все остальные строки игры мы нарисуем именно таким способом. Далее вы увидите конкретную реализацию класса растрового шрифта. Рассмотрим и остальные ресурсы.

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

Элементы игры

Теперь рассмотрим непосредственно игровые элементы. Все они, как говорилось ранее, соответствуют нашей пиксельной единице измерения. Чтобы максимально упростить их создание, я использовал простой прием – начинал рисование каждого из них с сетки, чьи клетки были размером 32 х 32 пиксела. Все объекты располагались в центре одной или более клеток, поэтому они с легкостью могли соответствовать своим физическим размерам, которые они имеют в нашем мире. Начнем с Боба (рис. 9.8).

Рис. 9.8. Пять анимационных кадров, изображающих Боба

На рис. 9.8 изображены два кадра, на которых Боб подпрыгивает, два кадра, на которых он падает, и один кадр, где он мертв. Полный рисунок имеет размер 160 х 32 пиксела, а каждая анимация – 32 х 32 пиксела. Пикселы фонового изображения прозрачны.

Боб может находиться в трех состояниях: прыжок, падение и смерть. У нас есть анимационные кадры для каждого из этих состояний. Два кадра прыжка различаются лишь тем, что на одном у Боба торчит чуб. Мы создадим экземпляр класса Animation для каждой из этих трех анимаций Боба и будем использовать их для отрисовки персонажа соответственно его текущему состоянию. У нас не будет дублирующих кадров на тот случай, когда Боб будет двигаться влево. Как и в случае с кнопкой, изображающей стрелку, мы просто зададим отрицательную ширину кадра при вызове метода SpriteBatcher.drawSpriteO, что перевернет изображение Боба по горизонтали.

На рис. 9.9 изображена злобная белка. В этот раз у нас два анимационных кадра – белка будет махать крыльями.

Рис. 9.9. Анимационные кадры, изображающие злобную летающую белку

Изображение на рис. 9.9 имеет размер 64 х 32 пиксела, а каждый кадр – 32 х 32 пиксела.

Анимация монеты, показанная на рис. 9.10, будет особенной. Вместо последовательности кадров 1, 2,3,1 мы используем 1,2,3,2,1. В противном случае монета из полностью повернутого состояния, изображенного на кадре 3, сразу перейдет в полностью развернутое состояние, показанное на кадре 1. Мы можем сохранить немного памяти, повторно использовав второй кадр.

Рис. 9.10. Анимационные кадры, изображающие монету

Изображение на рис. 9.10 имеет размер 96 х 32 пиксела, каждый его кадр – 32 х 32 пиксела.

О пружине, изображенной на рис. 9.11, можно сказать не так уж много. Она просто спокойно располагается в центре изображения.

Рис. 9.11. Пружина; изображение имеет размеры 32 х 32 пиксела

Замок, показанный на рис. 9.12, также не анимирован. По размерам он больше, чем все остальные объекты (64 х 64 пиксела).

Рис. 9.12. Замок

Платформа, приведенная на рис. 9.13 (64 х 64 пиксела), имеет четыре анимационных кадра. В соответствии с нашей игровой механикой некоторые платформы будут рассыпаться, когда Боб заденет их. В таком случае мы воспроизведем полную анимацию платформы. Для неподвижных платформ мы будем использовать только первый кадр.

Рис. 9.13. Анимационные кадры, изображающие платформу

Атлас текстур спешит на помощь

Выше были перечислены все графические активы, которые будут присутствовать в нашей игре. Мы уже говорили о том, что текстурам необходимы определенные значения ширины и высоты. Фоновое изображение и все экраны помощи имеют размер 320 х 480 пикселов. Мы будем хранить их как изображения размером 512×512 пикселов, чтобы загружать их как текстуры. Всего получается 6 текстур.

Нужно ли создавать отдельные текстуры для каждого прочего изображения? Нет. Мы создадим единый атлас текстур. Все прочие элементы отлично помещаются в единый атлас размером 512×512 пикселов, который можно загрузить как единую текстуру – это должно действительно порадовать GPU, ведь в таком случае у нас будет гораздо меньше текстур. На рис. 9.14 показан наш атлас текстур.

Рис. 9.14. Мощный атлас текстур

Изображение на рис. 9.14 имеет размеры 512×512 пикселов. Сетка и границы не являются частью изображения, а фоновые пикселы прозрачны. Это также верно и для черных фоновых пикселов меток пользовательского интерфейса и растрового шрифта. Ячейки сетки имеют размер 32 х 32 пикселов каждая.

Я поместил все изображения атласа в точки, чьи координаты кратны 32. Это упростит создание текстурных регионов.

Музыка и звук

Нам также понадобятся звуковые эффекты и музыка. Поскольку у нашей игры 8-битный ретростиль, мы можем использовать так называемые чип-тюны. Чип-тюнами называются звуковые эффекты и музыка, сгенерированные синтезатором. Наиболее известные чип-тюны были сгенерированы приставками NES, SNES и GameBoy фирмы Nintendo. Для создания звуковых эффектов я использовал инструмент sfxr, созданный Томасом Петерссоном (есть также флэш-версия – as3sfxr). Вы можете найти его по адресу www.superflashbros.net/as3sfxr.

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

С музыкой для игр обычно определиться немного сложнее. В Интернете существует всего несколько сайтов, предоставляющих 8-битные чип-тюны, подходящие для игр вроде Большого прыгуна. Мы будем использовать одну песню, которая называется NewSong, ее написал Геир Тьелта (Geir Tjelta). Ее можно найти на сайте www.freemusicarchive.org. Она находится под лицензией Creative Commons Atribution-NonCommercial-NoDerivatives (также известной как Music Sharing). Это означает, что ее можно применять для некоммерческих проектов, например таких, как наш Большой прыгун с открытым исходным кодом, в том случае если мы укажем, что ее написал Геир, и не изменим оригинальный фрагмент. Когда вы будете искать музыку для игры в Интернете, всегда следите за тем, что вы твердо придерживаетесь этой лицензии. Люди вкладывают много сил в эти песни. Если лицензия вашему проекту не подходит (например, если он коммерческий), то вы не сможете использовать эти песни.

РЕАЛИЗАЦИЯ БОЛЬШОГО ПРЫГУНА – РАЗРАБОТКА ИГР ДЛЯ ОС ANDROID

Реализовать Большого прыгуна будет довольно легко. Мы можем повторно использовать весь фреймворк, а на высоком уровне будем следовать архитектуре, разработанной для игры Мистер Ном. Это означает, что необходимо создать класс для каждого экрана и все эти классы будут реализовывать логику и представление, которые должен иметь данный экран. Кроме того, необходимо также создать стандартные действия перед началом работы над проектом – сделать подходящий файл манифеста, поместить все активы в каталог assets/, определить все необходимые значки приложения и т. д. Начнем с главного класса – Assets.

Класс Assets

В игре Мистер Ном у нас заранее был готов класс Assets, состоявший только из множества ссылок на изображения и звуки (Pixmap и Sound), которые хранились в статических переменных – членах класса. То же самое мы сделаем и для Большого прыгуна, но на этот раз мы добавим немного логики для их загрузки. В листинге 9.1 показан код класса.

Листинг 9.1. Класс Assets.java, в котором хранятся все активы, за исключением текстур экранов помощи.

В этом классе хранятся ссылки на все экземпляры классов Texture, TextureRegi on, Animati on, Musi с и Sound (текстуры, текстурные регионы, анимации, музыка и звуки), которые понадобятся нам во время игры. Единственное, что мы не загружаем здесь, – изображения для экранов помощи.

Метод load, который будет вызываться лишь один раз при запуске игры, отвечает за наполнение всех статических членов класса. Он загружает фоновое изображение и создает для него соответствующий текстурный регион (TextureRegi on). Далее он загружает атлас текстур и создает все необходимые текстурные регионы и анимации (Animation). Сравните код с рис. 9.14 и прочими рисунками предыдущего раздела. Единственное, на что стоит обратить внимание при загрузке графических ресурсов, – создание анимации для монеты. Как мы говорили ранее, мы повторно используем второй кадр в конце анимационной последовательности кадров. Время, через которое кадры сменяют друг друга, – 0,2 секунды.

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

Кроме того, в этом методе загружаются музыка и звуки (экземпляры классов Music и Sound). Как вы можете видеть, мы вновь работаем с нашим старым добрым другом – классом Setti ngs. Мы можем повторно использовать его реализацию, которую мы разработали для игры Мистер Ном, внеся лишь одну небольшую модификацию, о которой вы узнаете через минуту. Обратите внимание, мы зацикливаем воспроизведение композиции и устанавливаем громкость ее звука равной 0,5, поэтому она будет звучать немного тише, чем звуковые эффекты. Музыка начнет проигрываться, если пользователь не отключил предварительно звук. Эта информация будет также храниться в классе Setti ngs, как и в игре Мистер Ном.

Далее рассмотрим таинственный метод, который называется reload. Необходимо помнить, что контекст OpenGL ES будет теряться всякий раз, когда приложение будет ставиться на паузу. При возобновлении приложения следует каждый раз перезагружать текстуры – именно этим и занимается данный метод. Кроме того, возобновляется воспроизведение музыки, если, конечно, звук включен.

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

Теперь взглянем на модифицированный класс Setti ngs.

Класс Settings

В листинге 9.2 показан код несколько измененного класса Setti ngs.

Листинг 9.2. Settings.java, несколько измененный класс Settings, который позаимствован у игры Мистер Ном

Единственное отличие от класса Setti ngs игры Мистер Ном заключается в том, что файл настроек, с которым осуществляется работа, имеет расширение не . mrnom, a. super jumper.

Основная активность

Как главную точку входа в нашу игру следует использовать Activity. Мы назовем ее SuperJumper. В листинге 9.3 показан ее исходный код.

Листинг 9.3. Класс SuperJumper.java, главная точка входа в игру

Из класса GIGame мы наследуем и реализуем метод getStartScreenC, который возвращает экземпляр MainMenuScreen. Два других метода чуть менее очевидны.

Переопределяем метод onSurfaceCreate, который вызывается каждый раз при очередном создании контекста OpenGL ES. Если этот метод вызывается в первый раз, мы используем метод Assets. 1 oadдля того, чтобы загрузить все активы в первый раз, атакже загрузить настройки из файла, размещенного на карте памяти, если он доступен. В противном случае все, что нам нужно сделать, – перезагрузить текстуры и начать воспроизведение музыки с помощью метода Assets.reloadO. Мы также переопределили метод onPause, чтобы приостанавливать музыку, если она воспроизводится.

Мы выполняем оба эти действия, поэтому нет необходимости повторять их в методах resume и pause( )для экранов игры.

Перед тем как углубиться в реализацию классов экранов, взглянем на наш новый класс Font.

Класс Font

Для отрисовки случайного текста в формате ASCII мы будем использовать растровые шрифты. Принцип действия на высоком уровне уже обсуждался, поэтому просто взгляните на код листинга 9.4.

Листинг 9.4. Font.java, класс отрисовки растровых шрифтов

Этот класс хранит текстуру, содержащую глифы шрифта, ширину и высоту одного глифа, а также массив TextureRegi ons – по одному региону на каждый глиф. Первый элемент массива хранит регион для глифа пробела, следующий хранит регион для глифа восклицательного знака и т. д. Другими словами, первый элемент соответствует ASCII-символу с кодом 32, а последний – символу с кодом 127.

В конструкторе сохраняем конфигурацию растрового шрифта и генерируем регионы для глифов. Параметры offset и offsetY определяют верхний левый угол области текстуры, содержащей растровый шрифт. В созданном нами атласе текстур этот пиксел имеет координаты (224; 0). Параметр glyphsPerRow говорит о том, сколько глифов будет в строке, а параметры glyphWidth и glyphHeight определяют размер одного глифа. Поскольку мы используем моношириный растровый шрифт, этот размер будет одинаковым для всех глифов. Параметр glyphWidth также является значением, на которое мы будем сдвигаться при отрисовке нескольких глифов.

Метод drawText принимает экземпляр SpriteBatcher, строку текста и позиции х и у, откуда следует начинать рисовать текст. Координаты хну определяют центр первого глифа. Мы получаем индекс каждого символа строки, проверяем, имеется ли для него глиф, и, если ответ положительный, отрисовываем его с помощью экземпляра класса SpriteBatcher. Далее увеличиваем координату хна 3Ha4eHHeglyphWidth. После этого мы готовы отрисовывать следующий символ строки.

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

Почему необязательно привязывать ее снова в методе drawText  ? Помните, для OpenGLES гораздо лучше, если состояния меняются лишь незначительно.

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

Экран GL

В примерах, приведенных в двух предыдущих главах, мы всегда получали ссылку на объект класса GLGraphics при помощи преобразования типов. Исправим это с помощью небольшого вспомогательного класса по имени GLScreen, который будет делать за нас всю грязную работу и хранить ссылку на объект GLGraphi cs. В листинге 9.5 показан код этого класса.

Листинг 9.5. Небольшой вспомогательный класс GLScreen.Java

Мы храним экземпляры классов GLGraphics и GLGame. Конечно же, программа выдаст ошибку, если экземпляр класса Game, передаваемый конструктору, не будет иметь тип GLGame. Но теперь можно быть уверенным, что этого не случится.

Экран главного меню

Этот экран возвращается методом SuperJumper ,getStartScreen, пользователь увидит его в первую очередь. Он отрисовывает фоновое изображение и элементы интерфейса, а затем ожидает нажатия одного из элементов. Основываясь на нажатом элементе мы либо изменяем конфигурацию (включение/выключение звука), либо переходим на новый экран. В листинге 9.6 содержится код этого класса.

Листинг 9.6. Класс MainMenuScreen.java: экран главного меню

Этот класс наследует от класса GLScreen, поэтому мы можем получить доступ к экземпляру класса GLGraphics проще.

В этом классе есть несколько членов. Первый – это экземпляр класса Camera2D, который называется gui Cam. Нам также понадобится экземпляр класса Spri teBatcher для отрисовки фонового изображения и элементов пользовательского интерфейса. Чтобы определить нажатие пользователем одного из элементов интерфейса, мы будем применять прямоугольники (Rectangles). Поскольку мы задействуем класс Camera2D, нам также понадобится экземпляр класса Vector2, чтобы преобразовать координаты нажатия в координаты игрового мира.

В конструкторе инициализируются все члены класса. Здесь есть небольшая особенность. Экземпляр класса Camera2D позволяет работать с нашим целевым разрешением 320 х 480 пикселов. Нам нужно установить подходящие значения для ширины и высоты области видимости. Остальное OpenGL ES сделает на ходу. Однако обратите внимание на то, что начало координат по-прежнему находится в левом нижнем углу и ось устремится вверх. Мы будем использовать подобную GUI-камеру на всех экранах, имеющих элементы пользовательского интерфейса, поэтому становится возможно измерять их в пикселах, а не в координатах игрового мира. Конечно, мы немного жульничаем в случае, если разрешение экрана не соответствует целевому, но мы уже применяли этот трюк в игре Мистер Ном, и ничего плохого не произошло. Поэтому координаты Rectangle, которые мы используем для каждого элемента, даны в пикселах.

Далее рассмотрим метод updateO. В цикле мы проходим по всем событиям TouchEvents, которые возвращает экземпляр класса Input, и проверяем их тип. Нам нужно обнаружить прикосновения к экрану. Если такое событие произошло, прежде всего необходимо преобразовать координаты прикосновения в координаты игрового мира. Поскольку камера установлена для работы с целевым разрешением, суть трансформации заключается лишь в простом преобразовании координаты по оси у для экрана размером 320 х 480 пикселов. Для меньших или больших по размеру экранов следует преобразовывать координаты прикосновения для целевого разрешения. Как только координаты прикосновения получены, можно сверить их с координатами расположения прямоугольников, содержащих элементы пользовательского интерфейса. Если таким образом были выбраны пункты меню PLAY (Играть), HIGHSC0RES (Рекорды) или HELP (Помощь), осуществляется переход на соответствующий экран. Если была нажата кнопка звука, изменяются настройки и, соответственно, воспроизведение музыки либо начинается, либо приостанавливается. Обратите также внимание на то, что если нажатие было произведено с помощью метода Assets. pi aySound, проигрывается звук щелчка.

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

Последний метод, делающий что-либо, – pause. Здесь мы убеждаемся в том, что все настройки после манипуляций пользователя со звуком на этом экране сохранены на SD-карту.

Экраны помощи

Всего в игре пять экранов помощи, все они работают по одному принципу: загружают изображение экрана помощи, отрисовывают его и кнопку со стрелкой и ожидают нажатия этой кнопки, чтобы перейти на следующий экран. Единственное, что меняется, – загружаемое ими изображение, а также экран, на который осуществляется переход. По этой причине я приведу код только первого экрана помощи, с которого можно перейти на второй. Файлы, содержащие изображения экранов помощи, называются help., png и т. д. вплоть до hel р5. png. Соответствующие классы называются HelpScreen, Help2Screen и т. д. Последний экран, Help5Screen, переходит обратно к экрану главного меню.

В этом классе также есть несколько членов для хранения экземпляров класса камеры, Spri teBatcher, прямоугольника кнопки со стрелкой, вектора точки прикосновения, Texture и TextureRegion для изображения экрана помощи.

В конструкторе инициализируются все члены класса. Это происходит примерно так же, как и в классе Mai nMenuScreen.

В методе resume  загружается соответствующая текстура экрана помощи и создается соответствующий TextureRegi on для его отрисовки с помощью Spri teBatcher. Загрузка вынесена в этот метод, поскольку контекст OpenGL ES может быть утерян. Текстуры фонового изображения и элементов пользовательского интерфейса, как говорилось ранее, обрабатываются классами Assets и SuperJumper. В работе с ними на других экранах нет необходимости. Вдобавок мы удаляем текстуру изображения экрана помощи в методе pause, чтобы очистить память.

Далее следует метод update О, в котором выполняется простая проверка нажатия кнопки со стрелкой, при котором происходит переход на следующий экран. При нажатии также воспроизводится звук щелчка.

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

Как было отмечено ранее, другие экраны помощи имеют аналогичную реализацию.

 

Экран лучших результатов

Далее в нашем списке следует экран лучших результатов. Для его создания будут использованы несколько меток пользовательского интерфейса главного меню (участок HIGHSCORES (Рекорды)). Лучшие результаты, хранящиеся в экземпляре класса Setti ngs, будут отрисовываться с помощью объекта класса Font, который хранится в экземпляре класса Assets. Конечно, на этом экране также будет кнопка со стрелкой, с помощью которой игрок сможет вернуться в главное меню. В листинге 9.7 показан код класса этого экрана.

Листинг 9.7. Класс HighscoresScreen.java: экран лучших результатов

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

В конструкторе мы как обычно инициализируем все члены класса и вычисляем значение параметра xOffset. Это можно осуществить, оценив размер самой длинной из пяти строк, созданных нами для пяти лучших результатов. Поскольку мы используем моноширинный растровый шрифт, можно с легкостью посчитать количество пикселов, необходимых для одной строки текста, умножив количество символов на ширину глифа. Такой способ, конечно, не годится для непечатаемых символов или для символов, находящихся за пределами таблицы ASCII. Поскольку мы уверены, что не будем использовать такие символы, можно обойтись подобными простыми вычислениями. Потом в последней строке конструктора из 160 (горизонтальный центр экрана с целевым разрешением 320 х 480 пикселов) вычитается половина значения ширины самой длинной строки и подстраивается это значение далее путем вычитания половины ширины глифа. Это необходимо, поскольку метод Font. drawTextC) работает с центром глифа вместо одной из крайних точек.

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

В этом классе метод рresent  также довольно прямолинеен. Он очищает экран, устанавливает значения матриц, отрисовывает фоновое изображение и все метки главного меню, относящиеся в лучшим результатам. Далее с использованием рассчитанного в конструкторе значения xOffset отрисовываются пять строк, содержащих лучшие результаты. Теперь мы можем заметить, почему класс Font не выполняет никаких привязок текстур: мы можем объединить пять вызовов метода Font drawText. Конечно, необходимо убедиться в том, что экземпляр класса SpriteBatcher может принять необходимое для отрисовки наших текстов количество спрайтов (или в нашем случае – глифов). Убедиться в этом можно при его создании в конструкторе, где в качестве максимального значения устанавливается 100 спрайтов (глифов).

Теперь взглянем на классы эмуляции игрового мира.

Классы эмуляции игрового мира

Перед тем как мы сможем погрузиться в игровые экраны, нам необходимо создать классы эмуляции игрового мира. Как и в случае с игрой Мистер Ном, в игре будет класс для каждого игрового объекта и связывающий их все суперкласс по имени World, который объединяет все мелкие детали и оживляет игровой мир. Нам понадобятся классы для следующих объектов: Боб;  белки;  пружины;  монеты; платформы.

Боб, белки и платформы могут передвигаться, поэтому их классы будут основаны на классе DynamicGameObject, который мы создали ранее. Пружины и монеты являются статическими, поэтому они будут наследовать от класса GameObject. Приведем основные задачи для классов эмуляции игрового мира: хранение позиции, скорости и фигуры, ограничивающей объект; хранение состояния и времени нахождения в этом состоянии (время состояния), если это необходимо; предоставление метода update О, который будет изменять объект в соответствии в его поведением; предоставление методов, изменяющих состояние объекта (например, сообщающих Бобу о том, что он погиб или коснулся пружины).

Класс World будет отслеживать многочисленные экземпляры этих игровых объектов, обновлять их каждый кадр, определять столкновения между игровыми объектами и Бобом, а также генерировать последствия таких столкновений (например, позволять Бобу погибнуть, подобрать монету и т. д.). Рассмотрим каждый класс: от самого простого до самого сложного.

Класс пружины

Начнем с рассмотрения кода класса пружины, приведенного в листинге 9.8. Листинг 9.8. Spring.java, класс пружины

Класс Spri ng наследует от класса GameObject: нам необходимо знать только позицию и ограничивающую фигуру пружины, поскольку она неподвижна.

Далее мы определяем две константы, которые доступны всем: ширину и высоту пружины в метрах. Эти значения уже были определены заранее, поэтому нам остается просто повторно использовать их в этом классе.

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

Класс монеты

Далее рассмотрим класс монеты (листинг 9.9). Листинг 9.9. Coin.java, класс монеты

Класс Coin выглядит практически так же, как и класс Spring, он имеет только одно отличие: необходимо отслеживать продолжительность жизни монеты. Эта информация потребуется далее, когда нам понадобится отрисовать монету, используя экземпляр класса Animation. Мы делали то же самое для пещерного человека, героя последнего примера. Этот прием мы используем для всех классов эмуляции игрового мира. Основываясь на состоянии и времени, на протяжении которого объект находится в этом состоянии, мы можем выбрать анимацию, а также текущий кадр этой анимации для отрисовки. У монеты есть всего одно состояние, поэтому необходимо отслеживать лишь время нахождения в этом состоянии. Для этого у класса есть метод update , увеличивающий значение времени состояния каждый раз, когда ему передается параметр deltaTime.

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

Класс замка

Далее рассмотрим класс замка, находящегося на вершине нашего игрового мира (листинг 9.10).

Этот класс не очень сложен. Нам нужно сохранить позицию замка и его границы. Размер замка определяется константами CASTLE WIDTH и CASTLEJHEIGHT, для которых использованы значения, рассмотренные нами ранее.

Класс белки

Теперь рассмотрим класс Squi rrel (листинг 9.11). Листинг 9.11. Squirrel.java, класс белки.

Белки – перемещающиеся объекты, поэтому их класс наследует от класса Dynami cGameObject, что позволит нам задать векторы скорости и ускорения. Первое, что нам необходимо сделать, – задать размеры белки, а также ее скорость. Поскольку белки анимированы, необходимо также отслеживать время, которое белка находилась в определенном состоянии. У белки есть только одно состояние, как и у монеты: перемещение по горизонтали. Направление ее перемещения может быть определено с помощью компоненты вектора скорости по оси х, поэтому нет необходимости хранить время нахождения белки в этом состоянии. В конструкторе мы, конечно же, вызываем конструктор суперкласса, в который передается исходная позиция белки. Мы также устанавливаем вектор скорости белки равным (SQUIRREL VELOCITY; 0). Все белки сначала будут передвигаться вправо.

Метод update  обновляет позицию и ограничивающую фигуру белки, основываясь на скорости и прошедшем времени. Это стандартный этап интеграции Эйлера, который мы обсуждали и активно использовали в предыдущей главе. Необходимо также проверять столкновение белки с левым или правым краями мира. Если сто-кновение произошло, просто меняем значение вектора скорости на противоположный, и белка начинает двигаться в другом направлении. Ширина нашего мира ограничена 10 м, как мы уже говорили ранее. Последнее, что нам необходимо сделать, – обновить время состояния на значение deltaTime, что позволит определить, какую из двух анимаций следует использовать для отрисовки белки в дальнейшем.

Класс платформы

Код класса PIatform приведен в листинге 9.12. Листинг 9.12. Platform.java, класс платформы

Конечно, платформы – объекты несколько более сложные, нежели белки. Рассмотрим константы, определенные в классе. Первые две константы задают ширину и высоту платформы, значения которых мы рассмотрели ранее. У каждой платформы есть свой тип, она может быть неподвижной или движущейся. Тип платформы описывается константами PLATFORM TYPE STATIC и РLATF0RM TYРЕ М0VING. Платформа также может быть в одном из двух состояний: либо в нормальном состоянии (тогда она может быть статической или передвигающейся), либо она может рассыпаться. Эти состояния зашифрованы в константах PLATFORM STATE NORMAL и PLATFORM STATE PULVERIZING.

Рассыпание платформы – это процесс, ограниченный во времени. Поэтому необходимо определить время, требуемое для полного уничтожения платформы. Будем считать его равным 0,8 секунды. Это значение можно легко получить, приняв в расчет количество кадров анимации и продолжительность каждого кадра – это одна из небольших особенностей, не вписывающихся в шаблон MVC, которые следует учитывать, если вы пытаетесь следовать этому шаблону. Наконец, мы задаем скорость движущихся платформ равной 2 м/с, это значение также обсуждалось ранее. Передвигающаяся платформа ведет себя точно так же, как и белки, – перемещается в одном направлении до тех пор, пока не коснется края экрана, в этом случае она просто меняет направление.

Чтобы хранить тип, состояние и время, проведенное в определенном состоянии, нам понадобятся три члена класса. Они инициализируются в конструкторе, основываясь на типе платформы, который передается в конструктор как параметр (как и позиция центра платформы).

Метод update  перемещает платформу и проверяет, не вышла ли она за пределы игрового мира. В этом случае метод изменяет вектор ее ускорения. Точно такие же действия производил метод Squi rrel. update. В конце этого метода также обновляется время состояния.

Последний метод этого класса называется pulverize. Он переключает состояние платформы из PLATFORM STATE N0RMAL в Р LATFORM STATE PUL V ERI ZING, а также сбрасывает время состояния и скорость. Это означает, что платформа перестает перемещаться. Данный метод вызывается в том случае, если класс World определяет столкновение Боба и платформы и определяет, что платформа должна рассыпаться, основываясь на сгенерированном случайном числе. Чуть позже мы поговорим об этом более подробно.

 

Класс Боба

Сначала нам необходимо поговорить о самом Бобе. Код, реализующий его класс, приведен в листинге 9.13.

Листинг 9.13. Класс Bob.java

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

Конечно же, необходимо также хранить состояние Боба и время, которое он в нем находится.

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

Метод update  начинается с обновления позиции Боба и ограничивающей его фигуры, основываясь на гравитации и его текущей скорости. Обратите внимание на то, что скорость высчитывается с учетом гравитации и передвижения Боба как по горизонтали, так и по вертикали с помощью прыжков. Две следующие условные конструкции устанавливают состояние Боба равным либо BOBSTATEJUMPING, либо BOB STATE FALLING и сбрасывают его время состояния. Все эти действия производятся на основании компоненты у его скорости. Если она больше нуля – значит Боб прыгает, если меньше – падает. Однако эти действия выполняются лишь в случае, если Боб жив, но находится в неправильном состоянии. Если бы эти действия производились всегда, время состояния было бы равно нулю, что неблагоприятно сказалось бы на анимации Боба в дальнейшем. Этот метод также перемещает Боба с одного конца в другой, если он покинул границы игрового мира слева или справа. Наконец, в этом методе снова обновляется член класса stateTime.

Когда же Боб отделяет свою скорость от гравитации? Для этого служат другие методы.

Метод hitSqui rrel  вызывается классом World, если Боб прикоснется к белке. Если этого произошло, Боб перестает передвигаться и входит в состояние B0B STATE  HIT. С этого момента на Боба действует лишь гравитация, игрок более не может управлять персонажем, а сам Боб не взаимодействует с платформами. Подобное поведение демонстрируется в игре Супер Марио: если к Марио прикоснется враг, Марио просто упадет вниз.

Метод hitPl atformC также вызывается классом World. Это делается в том случае, если Боб прикоснется к платформе при падении вниз. Если это произошло, скорость Боба по оси у становится равной BOBJUMPVELOCITY, а также соответственно устанавливаются его состояние и время состояния. С этого момента Боб будет двигаться вверх, пока гравитация снова не победит, заставив его опять падать.

Последний метод, hi tSpring, вызывается классом World, если Боб прикоснется к пружине. Он делает то же самое, что и метод hi tPl atformO, но с одним различием: скорость Боба устанавливается равной значению BOB JUMP VELOCITY, умноженному на 1,5. Это означает, что Боб при касании пружины подпрыгнет немного выше по сравнению с результатом касания платформы.

Класс World

Последний класс, который нам необходимо обсудить, – World. Он немного больше по размеру, чем предыдущие классы, поэтому стоит разбить его на несколько частей. В листинге 9.14 показана первая часть его кода.

Листинг 9.14. Фрагменты KHaccaWorld.Java: константы, члены класса и инициализация.

Первое, что мы определяем в этом классе, – интерфейс, который называется World Listener. Вы спросите, для чего он нужен? Он необходим для решения небольшой проблемы, которую ставит перед нами MVC: когда следует проигрывать звуковые эффекты? Для решения этой проблемы можно просто вызывать метод Assets. pi aySound из соответствующих классов эмуляции, но такой подход не совсем корректен с точки зрения проектирования. Вместо этого мы позволим пользователю класса World зарегистрировать Worl dLi stener, который будет вызываться, когда Боб прыгает с платформы или пружины, касается белки или подбирает монету. Далее зарегистрируем слушателя, который будет воспроизводить соответствующие этим событиям звуки, что позволит освободить классы эмуляции от прямых зависимостей, связанных с отрисовкой и воспроизведением аудио.

После этого необходимо определить несколько констант. Константы WORLD WIDTH и WORLDHEIGHT задают вертикальные и горизонтальные границы игрового мира. Помните, что область видимости в нашей игре имеет площадь 10 х 15 м. Основываясь на константах, представленных здесь, игровой мир будет простираться на 20 областей видимости или экранов по вертикали. Опять же, к этому значению я пришел после тестирования. Мы вернемся к этому моменту, когда будем обсуждать генерацию уровней. Камера игрового мира также может быть в трех состояниях: перемещаться, ожидать начала уровня или находиться в неподвижном состоянии, если игра закончилась (в это время Боб падает очень далеко, за пределы области видимости). Здесь также определяется как константа вектор гравитационного ускорения.

Здесь перечислены все переменные – члены класса Worl d. Этот класс следит за Бобом, всеми платформами, пружинами, белками, монетами и замком. Он также имеет ссылку на интерфейс Worl dLi stener и экземпляр класса Random, который будет использоваться для генерации случайных чисел для различных нужд. Последние три члена отслеживают самую большую координату по оси у, на которой был Боб, состояние камеры игрового мира и количество набранных игроком очков.

В конструкторе инициализируются все члены класса, а также сохраняется экземпляр класса Worl dLi stener, переданный как параметр. Боб помещается по центру оси х немного выше нулевой координаты по оси у (в точке (5; 1)). Остальная часть кода говорит сама за себя, с одним лишь исключением: метод generateLe vel  не так прозрачен.

Генерация игрового мира

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

Листинг 9.15. Фрагменты класса World.Java: метод generateLevel

Опишем основную идею алгоритма простыми словами.

1. Начинаем с низшей точки игрового мира у = 0.

2. Если вершина мира еще не достигнута, выполняем следующее:

1) создаем платформу, неподвижную или двигающуюся в текущей позиции по оси у и случайной позиции по оси х,

2) генерируем случайное число между 0 и 1 и, если оно больше 0,9 и платформа неподвижна, создаем на этой платформе пружину;

3) если мы уже достигли высоты хотя бы трети мира, генерируем случайное число. Если оно больше 0,8, создаем белку и помещаем ее со случайным смещением по оси у от позиции платформы;

4) генерируем случайное число и, если оно больше 0,6, создаем монету и помещаем ее со случайным смещением по оси у от позиции платформы;

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

3. Помещаем в последней позиции по оси у замок, расположенный посередине осих.

Изюминка этой процедуры – это способ увеличения позиции у в пункте 5 шага 2. Необходимо быть уверенным в том, что каждая последующая платформа может быть достигнута Бобом после прыжка с текущей платформы. Боб может прыгать так высоко, как ему может позволить гравитация. Начальная скорость его прыжка м/с по вертикали. Высоту прыжка Боба можно рассчитать по следующей формуле:

Высота = скорость х скорость (2   гравитация) = 11   

11 / (2 х  13) = 4.6

Это означает, что если между платформами будет расстояние, равное по вертикали 4,6, Боб все еще сможет до них допрыгнуть. Чтобы убедиться, что Боб может достичь любой платформы, используем значение, которое немного меньше максимальной высоты его прыжка. Это гарантирует, что Боб сможет допрыгнуть с одной платформы на другую. Горизонтальное размещение платформы также случайно. Основываясь на том, что скорость горизонтального перемещения Боба равна 20 м/с, мы можем быть более чем уверены в том, что персонаж сможет достичь любой платформы как по вертикали, так и по горизонтали.

Другие объекты создаются случайно. Метод Random.nextFloat возвращает случайное число между 0 и 1 при всех своих вызовах. Каждое число может выпасть с одинаковой вероятностью. Белки создаются, только когда генерируется случайное число больше 0,8. Это означает, что новая белка появится с вероятностью 20 % (1 – 0,8). Эти слова также верны и для всех остальных объектов, создаваемых на основе случайного числа. Изменив эти значения, можно создать в игровом мире большее или меньшее количество объектов.

Обновление игрового мира

После генерации игрового мира мы можем обновлять его объекты и проверять наличие столкновений. В листинге 9.16 показаны методы класса World, используемые для обновления игрового мира.

Листинг 9.16. Фрагменты класса World.java: методы, необходимые для обновления игрового мира.

Метод update О в дальнейшем будет вызываться для игрового экрана. В качестве параметров он получает промежуток времени, прошедший с предыдущего вызова, а также показания акселерометра по оси х. Этот метод ответственен за вызов других методов обновления, а также за проверку столкновений и завершения игры. У нас есть метод обновления для каждого типа объектов игрового мира.

Метод updateBob отвечает за обновление состояния Боба. Первое, что он делает, – проверяет, не касается ли Боб нижней границы игрового мира, в этом случае он должен прыгнуть. Это означает, что в начале каждого уровня Боб подпрыгивает от основания игрового мира. Как только самая нижняя платформа выпадает из поля зрения, эта проверка, конечно же, больше не срабатывает. Далее обновляется горизонтальная скорость Боба, основываясь на показаниях акселерометра, которые метод получает в качестве аргумента. Как мы говорили ранее, показания акселерометра приводятся от вида [-10; 10] к виду [-1; 1] (от полного наклона влево до полного наклона вправо), а затем умножаются на стандартную скорость горизонтального перемещения Боба. Далее этот метод приказывает Бобу обновить свое состояние с помощью вызова Bob.update. Последнее, что происходит в этом методе, – сохранение максимального значения высоты, которого сумел достичь Боб. В дальнейшем оно понадобится для определения, упал Боб или нет.

Далее обновляются все платформы в методе updatePl atforms . Мы проходим по списку платформ и вызываем для каждой из них метод update , передавая в него текущее значение параметра deltaTime. Если платформа уже рассыпается, проверяем, как долго проходит этот процесс. Если платформа находится в состоянии PLATFORM STATE PULVERIZING время, большее, чем значение PLATFORM PULVERIZE TIME, она просто удаляется из списка платформ.

Метод updateSqui rrel s  обновляет каждый экземпляр класса Squi rrel с помощью его метода updateO, передавая ему в качестве аргумента время, прошедшее с последнего вызова метода. То же самое делается и для метода updateCoins.

Определение столкновений и реакция на них

Если вы снова взглянете на оригинальный метод World.updateO, то увидите, что далее он определяет столкновения между Бобом и всеми прочими объектами игрового мира. Это делается только в том случае, если Боб не находится в состоянии BOBSTATEHIT, в котором он просто продолжает лететь вниз под воздействием гравитации. Взглянем на код методов, определяющих столкновения (листинг 9.17).

Листинг 9.17. Фрагменты loiaccaWorld.Java: методы, определяющие столкновения

Метод checkCol lisions также является суперметодом, который просто вызывает другие методы, определяющие столкновения. Боб может прикоснуться к следующим объектам игрового мира: платформы, белки, монеты, пружины и замок. Для каждого типа объектов существует отдельный метод. Следует помнить, что этот суперметод вызывается после того, как обновились позиции и ограничивающие фигуры всех объектов игрового мира. Такую ситуацию можно рассматривать как фотоснимок состояния игрового мира в заданный момент. Все, что остается сделать, – просмотреть это неподвижное изображение и проверить, не пересекаются ли какие-либо объекты. Далее мы можем начать действовать и убедиться, что пересекшиеся объекты отреагируют подобающим образом, изменив свои состояния, позиции, скорости и т. д.

Метод checkPl atf ormCol1isions  проверяет пересечение Боба со всеми платформами игрового мира. Мы быстро выходим из этого метода, если Боб сейчас движется вверх. В этом случае Боб может проходить сквозь платформы снизу вверх. Для Большого прыгуна такое поведение подходит хорошо; в играх вроде Супер Марио нам скорее понадобится, чтобы Боб падал вниз при ударе о блок снизу. Далее проходим по всем платформам в цикле и проверяем, находится ли над текущей платформой главный герой. Если да, проверяем, пересекается ли ограничивающий его прямоугольник с ограничивающим прямоугольником платформы. В этом случае мы указываем Бобу, что он задел платформу, вызовом метода Bob. hi tPl atform. Если вернуться назад к данному методу, мы увидим, что это спровоцирует прыжок и соответственно изменит состояние Боба. Далее вызывается метод Worl dLi stener. jump, информирующий слушателя о том, что Боб только что начал прыгать. Это будет использовано для того, чтобы слушатель воспроизвел соответствующий звуковой эффект. Последнее, что делает данный метод, – генерация случайного числа. Если оно больше 0,5, мы приказываем платформе рассыпаться. Она просуществует еще PLATFORM PULVERI ZE TIME секунд (0,8), а затем будет удалена методом updatePI atf onus , показанным ранее. Когда мы будем отрисовывать эту платформу, используем ее время состояния для того, чтобы определить, какой именно кадр анимации платформы следует отобразить.

Метод checkSqui rrel Col1isionsO проверяет пересечение прямоугольника, ограничивающего Боба, с прямоугольниками, которые ограничивают каждую белку. Если Боб коснется белки, мы прикажем ему перейти в состояние BOB STATE HIT, что заставит его упасть, и игрок никак не сможет управлять им. Мы также извещаем об этом WorldListener, чтобы он проиграл соответствующий звуковой эффект.

Метод checkltemCollisionsO проверяет, не столкнулся ли Боб с какой-либо монетой или пружиной. Если Боб коснулся какой-либо монеты, она удаляется из игрового мира. Далее метод сообщает слушателю о том, что была подобрана монета, и увеличивает текущее количество очков игрока на COINSC0RE. Если Боб падает, мы также осуществляем проверку касания какой-либо пружины. Если оно произошло, мы сообщаем Бобу об этом, из-за чего он подпрыгнет выше, чем обычно. Об этом событии также информируется слушатель.

Последний метод проверяет столкновение Боба и замка. Если Боб касается замка, состояние мира принимает значение WORLDSTATENEXTLEVEL, сигнализируя всем внешним сущностям (таким как игровой экран) о том, что следует перейти на следующий уровень, который также будет представлять собой случайно сгенерированный экземпляр класса World. Игра окончена, дружище.

Код последнего метода класса World, который вызывается в последней строке метода World.update, приведен в листинге 9.18.

Листинг 9.18. Окончание класса World.java: метод, проверяющий, не закончилась ли игра

Вспомните, как мы определили состояние окончания игры: Боб должен покинуть нижнюю часть области видимости. Область видимости, конечно же, управляется экземпляром класса Camera2D, который имеет позицию. Координата этой позиции по оси у всегда равна наибольшей координате, которую сумел достичь Боб. Камера будет следовать за главным героем только в том случае, если он будет двигаться вверх. Поскольку мы хотим разделить код отрисовки и эмуляции, нам не нужна ссылка на камеру. Поэтому мы лишь отслеживаем наибольшую координату по оси у, достигнутую Бобом, в методе updateBob и храним ее значение в переменной heightSoFar. Мы знаем, что область видимости имеет высоту 15 м. Мы также знаем, что если координата Боба по оси у стала меньше, чем значение выражения heightSoFar – 7.5, то он покинул область видимости снизу и должен быть признан мертвым. Конечно, этот метод немного ненадежен, поскольку он основан на предположении, что высота области видимости всегда будет равна 15 м, а камера всегда будет находиться в высшей точке, которую сумел достичь Боб. Если бы мы разрешили масштабирование или использовали другой способ перемещения камеры, такой метод стал бы неверным. Вместо того чтобы все излишне усложнять, давайте просто оставим все как есть. Вы будете часто встречать подобные решения при разработке игр, поскольку очень трудно постоянно принимать решения, прозрачные с точки зрения инженерии программного обеспечения (о чем свидетельствует злоупотребление нами членами класса, имеющими тип public или package private).

Возможно, вы заинтересовались, почему до сих пор не был использован класс Spati al HashGrid. Я объясню причину через мгновение. Сначала закончим написание нашей игры, реализовав класс GameScreen.

Игровой экран

Мы практически закончили написание игры Большой прыгун. Последнее, что осталось реализовать, – игровой экран, который позволит игроку увидеть игровой мир и взаимодействовать с ним. Игровой экран состоит из пяти подэкранов, что показано на рис. 9.2. Имеется экран готовности, экран нормальной работы игры, экран перехода на следующий уровень, экран окончания игры и экран паузы. Игровой экран игры Мистер Ном был похож на этот, в нем не было лишь экрана перехода на новый уровень, поскольку в игре был всего один уровень. Мы будем использовать тот же подход, что и для игры Мистер Ном: у нас будут отдельные методы обновления и отображения для каждого подэкрана, которые обновляют и отрисовывают игровой мир, а также элементы пользовательского интерфейса, являющиеся частью подэкранов. Поскольку код класса игрового экрана несколько больше кода прочих классов, разобьем его на несколько листингов. В листинге 9.19 показана первая часть кода игрового экрана.

Листинг 9.19. Фрагменты класса GameScreen.Java: члены класса и конструктор

Класс начинается с описания нескольких констант, определяющих пять состояний, в которых может находиться экран. Далее заданы члены класса. Есть камера для отрисовки элементов пользовательского интерфейса, а также вектор, что позволит преобразовывать координаты прикосновения к координатам игрового мира (как и на других экранах, к области видимости в 320 х 480 единиц, нашему целевому разрешению). В классе есть также SpriteBatcher, экземпляры классов World и Worl dLi stener. Класс Worl dRenderer мы рассмотрим чуть позже. Он просто принимает экземпляр класса Worl d и отрисовывает его. Обратите внимание на то, что его конструктор принимает ссылку на экземпляры классов SpriteBatcher и World в качестве параметров. Это означает, что для отрисовки элементов пользовательского интерфейса и самого игрового мира используются одинаковые экземпляры класса SpriteBatcher. Остальные члены класса – это прямоугольники (объекты класса Rectangles) для различных элементов пользовательского интерфейса (например, для пунктов меню RESUME (Продолжить) и QUIT (Выйти), присутствующих на подэкране паузы) и два члена, которые отслеживают текущий результат игрока. Мы не хотим создавать новую строку каждый кадр, поэтому упростим работу сборщику мусора.

В конструкторе инициализируются все переменные – члены класса. Единственное, что здесь представляет интерес, – реализация Worl dLi stener как анонимного внутреннего класса. Он регистрируется экземпляром класса World и воспроизводит звуковые эффекты соответственно происходящим событиям.

Обновление игрового экрана

Далее рассмотрим методы обновления, которые позволят убедиться в том, что любой ввод данных пользователем будет корректно обработан, и при необходимости обновят экземпляр класса World (листинг 9.20).

Листинг 9.20. Фрагменты класса GameScreen.java: методы обновления игрового экрана

Метод GLScreen.update также является суперметодом, вызывающим другие методы обновления в зависимости от текущего состояния экрана. Обратите внимание на то, что мы ограничиваем параметр del taTi me значением, равным 0,1 секунды. Зачем это делается?

В нашей игре также может возникнуть эта проблема, если она будет запущена на устройстве с ОС Android 1.5. В любой момент игра может прерваться сборщиком мусора на несколько сотен миллисекунд. Это отразится и на параметре del taTi те, что заставит Боба телепортировать из одной точки в другую вместо плавного продвижения. Боб будет проходить сквозь платформы, даже не пересекаясь с ними, поскольку будет перемещаться на большое расстояние за один кадр. Ограничив это время чувствительным максимальным значением, равным 0,1 секунды, мы можем скомпенсировать этот эффект.

Метод updateReady  вызывается на подэкране паузы. Все, что он делает, – ожидает прикосновения к экрану, после чего игровой экран перейдет в состояние GAME RUNNING.

Сначала метод updateRunni ng проверяет, прикоснулся ли пользователь к кнопке Пауза в правом верхнем углу экрана. Если это так, игра переходит в состояние GAME PAUSED. В противном случае экземпляр класса World обновляется, используя текущее значение параметра deltaTime и показания акселерометра, отвечающие за перемещение Боба по горизонтали. После этого также необходимо проверить, нужно ли обновить строки, содержащие количество очков пользователя. Выполняется также проверка на то, достиг ли Боб замка. В этом случае переходим к состоянию GAME NEXT LEVEL, в котором на экран будет выведено сообщение, аналогичное показанному на рис. 9.2. Далее экран будет ожидать нажатия экрана, чтобы сгенерировать новый уровень. Если игра закончилась, выводим на экран строку, содержащую количество очков пользователя. Она будет иметь вид или score: fscore, или new hi ghscore: #score в зависимости от того, превосходит ли текущее количество очков результаты, показанные ранее. Далее итоговый результат игрока добавляется в настройки и сохраняется на карте памяти. В дополнение игровой экран переходит в состояние GAME 0VER.

В методе updatePaused проверяется, нажал ли пользователь пункты меню RESUME (Продолжить) или QUIT (Выйти), а также выполняется соответствующая реакция.

Метод updateLevel End  ожидает прикосновения пользователя к экрану. Если оно произошло, создаются новые экземпляры классов World и Worl dRenderer. Кроме того, в этом методе экземпляру класса World указывается количество очков, набранных пользователем на протяжении игры. Игровой экран переходит в состояние GAME READY, в котором он также ожидает прикосновения игрока к экрану.

Метод updateGameOver также ожидает нажатия экрана. Если оно произошло, мы просто переходим к главному меню игры, что показано на рис. 9.2.

Отрисовка игрового экрана

После всех этих обновлений игровой экран должен отрисовать себя с помощью вызова метода GameScreen. present О. Взглянем на код этого метода, приведенный в листинге 9.21.

Листинг 9.21. Фрагменты класса GameScreen.java: метод отрисовки игрового экрана

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

Метод presentReady  отображает кнопку паузы в верхнем правом углу экрана,  а также строку, содержащую количество очков, набранное игроком, в левом верхнем углу экрана.

Метод presentRunning просто отрисовывает кнопку паузы и текущее количество очков игрока.

Метод presentPaused отображает элементы пользовательского интерфейса меню паузы и количество очков игрока.

Метод presentLevelEndO отрисовывает строку THE PRINCESS IS … (Принцесса…) в верхней части экрана и строку IN ANOTHER CASTLE. (В другом замке.) в нижней части экрана, как показано на рис. 9.2. Выполняются также некоторые вычисления для того, чтобы выровнять эти строки по центру.

Метод presentGameOver отображает элементы пользовательского интерфейса, необходимые на экране конца игры, и количество очков игрока. Помните, что экран результатов устанавливается методом updateRunn i ng  и его строки имеют вид либо score: score, либо new highscore: value.

Последние штрихи

Класс игрового экрана практически готов. Остальная часть кода приведена в листинге 9.22.

Листинг 9.22. Окончание класса GameScreen.java: методы pauseO, resumeO и disposeO

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

Класс World Renderer

Этот класс не должен вас удивить. Он просто использует экземпляр класса Spri teBatcher, который передается ему в конструктор для того, чтобы отрисовать игровой мир. В листинге 9.23 приведено начало его кода.

Листинг 9.23. Фрагменты класса WorldRenderer.Java: константы, члены класса и конструктор

Как обычно, начинаем с определения некоторых констант. В этом случае это ширина и высота области видимости, значения которых мы определили равными 10 и 15 м. У нас также есть несколько других членов – экземпляры классов GLGraphics, Camera2D, а также ссылка на экземпляр класса SpriteBatcher, которую конструктор получает от класса игрового экрана.

Конструктор в качестве параметров принимает экземпляры классов GLGraphi cs, Spri teBatcher и World. Следует соответственно инициализировать все члены класса. В листинге 9.24 приведен код отрисовки игрового мира.

Листинг 9.24. Окончание класса WorldRenderer.Java: код отрисовки игрового мира

Метод render  разбивает отрисовку на два пакета: один для фонового изображения, а другой – для всех объектов игрового мира. Он также обновляет позицию камеры, основываясь на текущей координате Боба по вертикали. Если он находится выше координаты камеры по оси у, камера соответственно сдвигается. Обратите внимание на то, что использована камера, которая работает в единицах измерения игрового мира. Мы только единожды устанавливаем матрицы для фонового изображения и для объектов.

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

Метод renderObjects  отвечает за отрисовку второго пакета. В этот раз мы будем использовать смешивание, поскольку все наши объекты имеют прозрачные фоновые пикселы. Все объекты отрисовываются в одном пакете. Оглядываясь на конструктор класса GameScreen, можно увидеть, что экземпляр класса SpriteBatcher, который мы используем, может работать с 1000 спрайтов в одном пакете – этого более чем достаточно для игрового мира. Для каждого объекта существует свой метод, отрисовывающий его.

Метод renderBobC отвечает за отрисовку Боба. Основываясь на состоянии Боба и времени состояния, мы выбираем один кадр из пяти (см. рис. 9.8). Основываясь на компоненте скорости Боба по оси х, мы также определяем, в какую сторону будет повернут Боб. Основываясь на этом, мы повернем в соответствующий текстурный регион. Помните, на имеющихся у нас кадрах Боб повернут только вправо. Обратите также внимание на то, что мы не используем параметры B0B W I DTH или BOB HEIGHT для того, чтобы определить размеры прямоугольника, который мы рисуем для Боба. Эти параметры предназначены для ограничивающих фигур, они необязательно пригодятся для прямоугольников, которые мы рисуем сейчас. Вместо этого мы применяем выбранный масштаб – 1х1мк32х32 пиксела. Это же мы повторим для всех спрайтов; мы будем использовать прямоугольник 1×1 (Боб, монеты, белки, пружины), прямоугольник 2 х 0,5 (платформы) или прямоугольник 2×2 (замок).

Метод renderPl atforms проходит в цикле по всем платформам игрового мира и выбирает текстурный регион, основываясь на состоянии платформы. Платформа может рассыпаться или не рассыпаться. В последнем случае мы просто будем использовать первый кадр, а в первом нам придется получать кадр анимации рассыпания платформы, основываясь на времени состояния платформы.

Метод render Items  отрисовывает пружины и монеты. Для пружин используется один текстурный регион, определенный в активах программы. Для монет мы снова выбираем кадр анимации, основываясь на времени состояния монеты.

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

Последний метод называется renderCastle. Он просто отрисовывает замок используя текстурный регион, определенный в классе Assets.

Это было довольно просто, не так ли? У нас есть только два пакета для отрисовки: один для фонового изображения и один для всех игровых объектов. Учитывая все вышеописанное, понятно, что мы отрисовываем третий пакет для всех элементов пользовательского интерфейса. Всего приходится три раза изменять текстуры и три раза загружать новые вершины в GPU. Теоретически мы могли бы объединить пакеты пользовательского интерфейса и игровых объектов, но это было бы обременительно, а код стал бы неэлегантным. В соответствии с нашим руководством по оптимизации, нашему приложению следует иметь мгновенную отрисовку. Проверим, так ли это.

Мы практически закончили. В нашу вторую игру – Большой прыгун – уже можно играть.

Источник: Mario Zechner / Марио Цехнер, «Программирование игр под Android», пер. Егор Сидорович, Евгений Зазноба, Издательство «Питер»

По теме:

  • Комментарии