Главная » Программирование игр под Android » ТРЮКИ ПРИ РАЗРАБОТКЕ 20-ИГР  – РАЗРАБОТКА ИГР ДЛЯ ОС ANDROID

0

Мы узнали, что OpenGL ES предлагает множество функций для графического 20-программирования. Среди них – вращение, масштабирование и автоматическое растяжение конуса отображения до размеров области просмотра. OpenGL ES также позволяет работать быстрее, чем при использовании Canvas.

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

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

ПЕРЕД СТАРТОМ

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

Наш демонстрационный проект, который мы рассмотрим для начала, называется GameDev2DStarter и содержит список тестов для запуска. Мы можем повторно использовать код GLBasicsStarter и просто заменить названия классов в тестах. Кроме того, понадобится добавить все тесты в файл манифеста в виде элементов <activity>.

Каждый из этих тестов – это опять-таки экземпляр интерфейса Game, а сама логика тестов реализована в виде Screen, содержащегося в Game реализации теста. Я продемонстрирую лишь те части Screen, которые важны в конкретном примере, чтобы вы лучше понимали процесс. Что касается названий, мы снова используем XXXTest и XXXScreen для реализаций GLGame и Screen каждого теста.

Теперь поговорим о векторах.

СНАЧАЛА БЫЛ ВЕКТОР

Мы говорили, что не надо путать векторы и позиции. Это не совсем так, поскольку мы можем (и будем) представлять местоположение в пространстве с помощью вектора. Вектор можно интерпретировать по-разному.

Позиция. Мы уже использовали такую интерпретацию для кодирования координат объектов относительно начала координат.

Скорость и ускорение. Об этих физических величинах мы поговорим в следующем разделе. Хотя в обыденном понимании скорость и ускорение – это скалярные величины, в 2D или 3D они выражаются в виде векторов. Они кодируют не только скорость объекта (например, машины, едущей со скоростью 100 км/ч), но также направление, в котором движется объект. Обратите внимание, что такая интерпретация не означает, что вектор находится в начале системы координат. Это логично, поскольку скорость и направление движения не зависят от местоположения объекта. Представьте себе машину, едущую на северо-запад по прямой трассе со скоростью 100 км/ч. Если скорость и направление не будут меняться, вектор скорости также останется неизменным.

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

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

Рис. 8.1. Базовые интерпретации вектора

Еще одна деталь, опущенная на рис. 8.1, – информация о том, в каких единицах измеряются компоненты вектора. Это всегда следует проверять. Например, скорость

Боба может измеряться в метрах в секунду, и он будет передвигаться со скоростью 2 м влево, 3 м вверх за одну секунду. Это касается и позиций, и расстояний, которые также могут быть выражены в метрах. Направление Боба – это особый случай, поскольку это безразмерная величина. Без размерность удобна, если нужно определить общее направление объекта, держа физические величины направления отдельно. Такая же операция применима и для скорости Боба: можно сохранить направление его скорости в качестве вектора направления, а скорость сохранить как скалярное значение. Для этого вектор направления должен иметь длину 1, однако мы обсудим это позже.

Работа с векторами

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

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

Чтобы получить конечный вектор, нужно сложить компоненты. Попробуйте это сделать с векторами на рис. 8.1. Допустим, местоположение Бобар = (3; 2), а его скорость v = (-2; 3). Мы перемещаемся в новое место р = (3 + -2; 2 + 3) = (1,5). Не обращайте внимания на апостроф после р, он просто показывает, что у нас есть новый вектор р. Конечно, это маленькая операция целесообразна лишь тогда, когда местоположение и скорость измеряются в одних и тех же единицах. В этом случае допустим, что местоположение измеряется в метрах (м), а скорость – в метрах в секунду (м/с).

Естественно, мы можем также вычитать векторы:

Опять-таки мы просто комбинируем компоненты двух векторов. Обратите внимание на то, что порядок, в котором мы вычитаем один вектор из другого, весьма важен. Возьмите, к примеру, рис. 8.1. Зеленый Боб (на крайнем правом рисунке сверху) находится на pg = (1; 4), а красный Боб (на крайнем правом рисунке снизу) – на рг = (6; 1), где pg и рг означают местоположения зеленого и красного соответственно. Когда мы вычитаем вектор расстояния красного Боба от зеленого Боба, выполняются следующие вычисления:

Вот что странно. Этот вектор фактически направлен от красного Боба к зеленому. Чтобы получить вектор направления от зеленого Боба к красному, нужно поменять порядок вычитания:

Если мы хотим найти вектор расстояния между точками, мы используем следующую общую формулу:

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

Мы также можем умножать вектор на скаляр:

Мы умножаем каждый компонент вектора на скаляр. Это позволяет нам изменять длину вектора. Возьмите в качестве примера вектор направления из рис. 8.1. Он определен как d = (0; -1). Если мы умножим его на скаляр, равный 2, мы вдвое увеличим его длину: d х s = (0; -1 х 2) = (0; -2). Естественно, мы можем таким путем и уменьшить его длину, используя значения скаляра меньше единицы, например d, умноженное на s = 0,5, приводит к созданию нового вектора d = (0; -0,5).

Говоря о длине, мы также можем подсчитать длину вектора (в тех единицах, в которых она измеряется):

Запись  а  просто означает, что мы говорим о длине вектора. Если вы не прогуляли уроки линейной алгебры в школе, вам должна быть известна формула длины вектора. Это теорема Пифагора, примененная к 2Э-вектору. Компоненты х и у вектора образуют две стороны треугольника, третья сторона – это длина вектора (рис. 8.2).

Рис. 8.2. Пифагору бы понравились векторы

Длина вектора всегда больше нуля, как и квадратный корень. Если применить это к вектору расстояния между красным и зеленым Боба, то выясним, как далеко они находятся друг от друга (если оба местоположения даны в метрах).

Обратите внимание, что если бы отнимали рд – рг, получили бы такой же результат, поскольку длина не зависит от направления вектора. Однако отсюда мы можем вынести еще один урок: когда мы умножаем вектор на скаляр, длина вектора изменяется соответственно. Вектор d= (0; -1) с начальной длиной в 1 единицу, умноженный на 2,5, даст нам новый вектор с длиной 2,5 единицы.

Мы уже обсуждали, что, как правило, направление векторов является безразмерной величиной. Мы можем сделать эту величину и размерной, умножив вектор на скаляр – например, чтобы получить вектор скорости v, мы можем умножить вектор направления d = (0; 1) на константу скорости s = 100 м/с: v = (0 х 100; 1 х 100) = (0; 100). Поэтому удобно, чтобы длина векторов была равна 1. Векторы, чья длина равна 1, называются единичными (unit vectors). Мы можем превратить любой вектор в единичный, разделив каждый его компонент на его длину:

Запомните, что  d  означает только длину вектора d. Допустим, нам нужен вектор направления, который указывает ровно на северо-восток: d = (1; 1). Нам может казаться, что этот вектор уже единичный, поскольку оба его компонента равны 1, правильно? Нет, неправильно:

Мы можем легко это исправить, превратив вектор в единичный.

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

Немного тригонометрии

Давайте на минутку обратимся к тригонометрии. В тригонометрии есть две основополагающие функции: косинус и синус. Каждая из них принимает один аргумент: угол. Мы привыкли измерять углы в градусах (например, 45° или 360°). Однако в большинстве математических библиотек тригонометрические функции измеряются в радианах. Можно легко перевести градусы в радианы с помощью следующего уравнения:

Здесь используется к, суперконстанта, которая примерно равна 3,14159265. к радиан равно 180°.

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

Рис. 8.3. Косинус и синус создают единичный вектор с его конечной точкой, лежащей на единичном круге

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

Мы можем поступить иначе: подсчитать угол вектора относительно оси х.

Функция atan2 – это искусственная конструкция. Она использует функцию арктангенса (что является обратной функцией тангенса, еще одной фундаментальной функцией в тригонометрии), чтобы создать угол от -180° до 180° (или от -pi до pi, если угол измеряется в радианах). Это все достаточно сложно и не относится к теме нашей дискуссии, у- и х-компоненты нашего вектора являются аргументами функции. Обратите внимание, что вектор необязательно должен быть единичным, чтобы функция atan2 работала. Кроме того, заметьте, что г/-компонент, как правило, дается первым, а х-компонент – вторым, однако это зависит от математической библиотеки, которую мы используем. Это один из наиболее частых источников ошибок.

Рассмотрим несколько примеров. Имея вектор v = (cos(97°); sin(97°)), мы получим результат atan2(sin(97°); cos(97°)), равный 97°. Отлично, это было достаточно просто. Используя вектор v = (1; -1), мы получаем atan2(-l; 1) = -45°. Так что если -компонент вектора отрицательный, мы получим отрицательный угол, равный от 0° до -180°. Мы можем исправить это, прибавив 360° (или 2pi), если результат atan2 отрицательный. В предыдущем примере мы тогда получили бы 315°.

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

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

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

Реализация класса Vector

Мы хотим создать удобный в использовании векторный класс для 20-векторов. Назовем его Vector2. Он должен состоять из двух элементов, для записи х- и  у -компонентов вектора. Дополнительно он должен иметь несколько удобных методов, которые позволят нам выполнять следующие операции: складывать и вычитать векторы; умножать векторные компоненты на скаляр; измерять длину векторов; нормализировать векторы; вычислять угол между вектором и осью х; поворачивать вектор.

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

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

Листинг 8.1. Vector2.java: реализация функционала 2D-вектора

Мы перемещаем этот класс в пакет com. badl ogic. androi dgames. framework .math, где будем также хранить все математические классы.

Начинаем с описания двух статических констант: T0 RADI ANS и TODEGREES. Чтобы перевести угол, данный в радианах, нужно просто умножить его на TODEGREES; чтобы перевести в радианы угол, указанный в градусах, мы умножаем его на TO RADIANS. Вы можете перепроверить это, посмотрев на два предыдущих описанных равенства, которые предназначены для перевода из градусов в радианы и обратно. Эта небольшая уловка позволяет обойтись без деления, тем самым немного ускорив работу.

Затем опишем два элемента х и у, которые хранят компоненты вектора и еще пару конструкторов, – ничего сложного:

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

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

Методы add и sub бывают двух разновидностей: в одном случае они работают с двумя аргументами в виде чисел с плавающими точками, в другом – используют еще один экземпляр класса Vector2. Все четыре метода возвращают ссылку на этот вектор, чтобы мы могли создать цепь операций.

Метод mul  просто умножает компоненты вектора х и г на данную скалярную величину и снова возвращается к вектору для цепи.

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

Math, предлагаемого Java SE. Это специальный класс Android API, который работает, он немного быстрее, чем его аналог Math.

Метод nor  нормализирует вектор до единичной длины. Мы используем метод en  внутрисистемно, чтобы сначала подсчитать длину. Если она равна нулю, мы выходим из операции раньше, чтобы избежать деления на ноль. В другом случае мы делим каждый компонент вектора на его длину, чтобы получить единичный вектор. Для объединения действий в цепочку снова возвращаем ссылку на вектор.

Метод angl е вычисляет угол между вектором и осью х, используя метод atan2, что мы и обсудили выше. Нам необходимо применить метод Math .atan2, поскольку у класса FastMath нет этого метода. Возвращенный угол дается в радианах, так что мы переводим его в градусы, умножая его на TODEGREES. Если угол меньше нуля, прибавляем к нему 360°, чтобы он имел значение от 0° до 360°.

Метод rotate просто поворачивает вектор вокруг начала координат на данный угол. Поскольку методы FloatMath.cos и FloatMath.sinO требуют указания угла в радианах, переводим сначала градусы в радианы. Затем используем ранее описанные равенства, чтобы подсчитать новые компоненты вектора хну, и наконец возвращаем вектор для добавления в цепь.

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

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

Простой пример использования

Вот мой вариант простого теста.

Мы создадим своего рода пушку, представленную треугольником, который имеет фиксированное местоположение в мире. Центр треугольника будет находиться в координатах (2,4; 0,5).

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

Конус отображения показывает зону нашего мира между (0; 0) и (4,8; 3,2). Мы не работаем с пиксельными координатами, но вместо этого описываем свою систему координат, где одна единица равна одному метру. Мы также будем работать в альбомной ориентации.

Существует несколько нюансов, заслуживающих особого внимания. Мы уже знаем, как описать треугольник в пространстве модели: для этого можно использовать экземпляр класса Vertices. По умолчанию наша пушка должна указывать направо под углом 0°. На рис. 8.4 показана пушка-треугольник в пространстве модели.

Когда мы визуализируем этот треугольник, мы просто используем gl Transl atef , чтобы передвинуть его на его место в мире, а именно на (2,4; 0,5).

Мы также хотим повернуть пушку таким образом, чтобы ее конец указывал в сторону точки, где произошло касание. Для этого нам необходимо выяснить, где в нашем мире произошло последнее событие касания. Методы GLGame .getInput. getTouchX и getTouchY вернут точку касания в экранных координатах с началом в верхнем левом углу. Кроме того, напоминаю, что экземпляр класса Input не преобразует координаты событий в фиксированную систему координат, как это было в Мистере Номе. Вместо этого мы получаем координаты (479; 319), когда касаемся нижнего правого угла экрана (ландшафтная ориентация) на Her, и (799; 479) на Nexus One. Нам нужно перевести эти координаты касания в координаты нашего мира. Мы уже делали это в обработчике касаний в Мистере Номе и основанном на Canvas фреймворке игры. Единственная разница состоит в том, что наша система координат немного меньше, а ось у направлена вверх. Вот псевдокод, показывающий, как мы можем достичь перевода в общем случае, что представляет собой практически то же самое:

Рис. 8.4. Пушка-треугольник в пространстве модели

Мы нормализуем координаты касания до диапазона (0; 1), деля их на разрешение экрана. В случае с у- координатой вычитаем нормализованную у-координату события касания от 1, чтобы изменить по модулю направление по оси у. Осталось масштабировать координаты х- и у- координат относительно высоты и ширины конуса отображения – в нашем случае это 4,8 и 3,2. Из координат world X и world Y можно создать величину Vector2, где будет сохранена позиция точки касания в системе координат игрового мира.

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

Необходимо создать вектор расстояния между центром пушки (2,4; 0,5) и точкой касания (помните, что нам надо вычесть координаты центра пушки из координат точки касания, а не наоборот). Когда мы получим этот вектор расстояния, можем вычислить угол с помощью метода Vector2. angl е. Этот угол затем можно использован для поворота нашей модели с помощью gl Rotatef .

Запрограммируем это. В листинге 8.2 показан соответствующий фрагмент нашего класса CannonScreen, входящего в класс CannonTest.

Рис. 8.5. Пушка в состоянии, задаваемом по умолчанию, указывает направо (угол = 0°), точка касания и угол, на который нужно повернуть пушку. Прямоугольник – это область мира, которая будет показана в конусе отображения на экране: диапазон от (0; 0) до (4,8; 3,2)

Листинг 8.2. Отрывок из CannonTest.java; прикосновение к экрану повернет пушку

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

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

В конструкторе выбираем экземпляр класса GLGraphics и создаем треугольник в соответствии с рис. 8.4.

Далее следует метод update. Мы просто перебираем в цикле все TouchEvent и подсчитываем угол пушки. Это делается в несколько шагов. Сначала преобразуем координаты событий касания в точки координатной системы мира, как обсуждали это выше. Сохраняем координаты событий касания в координатной системе мира в элементе touchPoi nt. Затем вычитаем положение пушки из вектора touch Poi nt, в результате получаем вектор, показанный на рис. 8.5. Затем вычисляем угол между этим вектором и осью х. Вот и все.

Метод presentC занят все той же скучной работой, что и раньше. Мы задаем область просмотра, очищаем экран, устанавливаем матрицу ортогональной проекции, используя ширину и высоту конуса отображения, и сообщаем OpenGL ES, что все последующие операции матрицы будут применяться к модельно-видовой матрице. Мы также загружаем единичную матрицу в модельно-видовую матрицу, чтобы очистить ее. Затем умножаем (единичную) модельно-видовую матрицу на матрицу трансляции, которая перенесет вершины нашего треугольника из пространства модели в пространство мира. Также вызываем gl Rotatef  с углом, который мы подсчитали в методе updateO, чтобы повернуть наш треугольник в пространстве модели, перед тем как переместить его в пространство мира. Не забывайте, что преобразования применяются в обратном порядке – последнее указанное изменение будет выполнено первым. Наконец, привязываем вершины треугольника, визуализируем его и отвязываем.

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

Рис. 8.6. Треугольник-пушка, реагирующий на событие касания в верхнем левом углу

Обратите внимание: неважно, визуализируем мы на месте пушки треугольник или прямоугольную текстуру, ассоциированную с изображением пушки, – для OpenGL ES это не играет роли. Мы также снова используем матричные операции в методе present.

Дело в том, что таким образом гораздо проще следить за состояниями OpenGL ES. Достаточно часто мы будем использовать несколько конусов отображения в одном вызове present  (например, расчет мира в метрах для визуализации нашего мира и еще один расчет мира в пикселах для визуализации элементов пользовательского интерфейса). Это не так сильно влияет на быстродействие программы, как факторы, так что вполне можно пользоваться этими методами постоянно. Просто запомните, что мы можем при необходимости оптимизировать этот функционал.

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

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

По теме:

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