.7 Эффекты от использования игровых бонусов
.8 Прорисовка заднего фона
.9 Добавление возможности поставить паузу и кнопку выхода
.10 Внедрение игровых состояний (ждёт выбор элемента 1, ждёт выбор элементов 2, ждёт разрушения игровых объектов, пауза и подобных)
. Доработка остальных деталей игры
.1 Дорисовка графики (где это необходимо)
3.2 Добавление красивых визуальных эффектов к игровым событиям
Изначально для своей игры я решил создать два основных класса, которые
являются каркасом основной игровой части приложения. Первым классом является
игровой объект или gem. Он
представляет из себя небольшой объект минимального размера на игровом поле и
занимает его незначительную часть.
Рисунок 3.2 - Один игровой объект полностью
В нашем случае объектом игровой сетки является простое изображение в форме квадрата определённого цвета с 15 вариантами. Все варианты, которые вы видите на рисунке выше являются настоящей анимацией. Из изображения вырезаются квадраты размером по 85 пикселей, которые потом при быстрой замене текущего изображения в игровом объекте (которым является наш gem), создают непрерывную и плавную его анимацию. Анимация проигрывается не всегда, а только при падении новых игровых объектов на экран и в самом начале игры. Это сделано для того, чтобы снизить нагрузку на глаза игрока и позволить им меньше уставать от игрового процесса.
Для того, чтобы создать хорошую анимацию игрового объекта, были использованы знания программы Photoshop CS6. К каждому отдельному квадрату добавлялись определённые эффекты. Например, тиснение. Этот эффект увеличивался с определённым шагом до квадрата в центре, а затем снова снижался до прежнего уровня, что позволяло нашему игровому объекту вновь принять цвет, которым он обладал в начале игры или в момент первого своего появления на игровом экране.
Класс gem в целом представляет из себя обычное игровое изображение с анимацией, которое находится по определённым x,y координатам. Также класс обладает типом, который определяет его цвет из файла и соответствующие анимации.
В классе gem можно выделить только созданный метод gem:explode(), который занимает очень важное место в игре.gem:explode()
- Проигрываем анимацию исчезновения
tween:to(self.sprite, { alpha=0, time=0.1, easing=ease.sineIn } )
-- Удаляем объект из игры.removeGem(self)
- Добавляем игровые очки за уничтожение.addToRoundScore(gemDestroyedScore, self.type)
- Создаём эффект «взрыва» в указанной точке
explosion.createExplosion(self.sprite.x + gemActualWidth / 2, self.sprite.y + gemActualHeight / 2, self.type)
end
Этот метод удаляет любой созданный игровой объект с экрана и создаёт на
его месте красивый эффект уничтожения. Благодаря ему игровой процесс в самой
игре развивается и действия игрока визуально подчёркиваются приятной графикой.
Также, этот метод отвечает за выпадение бонусов - монеток.
Рисунок 3.3 - Монетка
Большое количество монет позволяет нам использовать больше бонусов, чтобы легче проходить любые уровни. Монетка с определённым шансом (в зависимости от уровня), выпадает после уничтожения любого предмета на экране. Количество монет игрока сохраняется в базе данных, которая используется в игре, и помогает сохранить игровые достижения игрока.
Класс grid - это класс, который представляет из
себя основную игровую сетку, на которой располагаются отдельные gem-элементы. Выглядит это сетка
примерно вот так.

Игровая сетка представляет из себя сложную структуру, в которой описывается по сути весь основной игровой алгоритм. В сетке есть методы, которые позволяют отслеживать нажатия на любые игровые элементы, которые были в неё добавлены. Специальные методы в сетке конвертируют координаты при нажатии, получая игровой объект, который по ним находится и переводят их в номер, под которым он хранится в таблице - специальной структуре языка Lua, которая здесь способна заменять массивы и является в нашей игровой сессии основой хранения игровых данных.
Получаем x,y координаты сетки по нажатию на экран
function grid:screenToGrid(x, y)grid_x = math.floor((x - self.offsetX) / gem.gemActualWidth + 0.001)grid_y = math.floor((y - self.offsetY) / gem.gemActualWidth + 0.001)grid_x + 1, grid_y + 1
end
Получаем игровой объект, который находится по координатам x,y
function grid:getGem(x, y)grid_x, grid_y = self:screenToGrid(x, y)self.gemsGrid[grid_x][grid_y]
end
Кроме всего, что мы перечислили, именно сетка отвечает за игровые бонусы. Особый её метод grid:useBonus(bonus_num), где bonus_num - номер активированного в игре бонуса (1 из 6 видов). Помогает легко найти все игровые объекты в ряду, столбце, или вообще перебрать всю сетку полностью на игровые объекты, чтобы определить - какие из них требуется уничтожить.
Основной метод сетки - grid:regenerate()
Он позволяет генерировать новую случайную сетку игровых объектов с любыми типами (цветами), игровых объектов. При этом в самом конце сетка проверяется заранее на любые готовые совпадения игровых элементов и перестраивает её так, чтобы их не осталось (иначе часть задачи игрока была бы выполнена сразу после запуска игры).
-- Создаём новую сетку элементовgrid:regenerate()gem_index = 1
-- Делаем сетку и заполняем случайными элементами
for x = 1, self.width doy = 1, self.height do
- Пустые элементы удаляем(self.gemsGrid[x][y] == nil) then.gemsGrid[x][y] = self.matchedGems[gem_index].matchedGems[gem_index] = nil_index = gem_index + 1
- Перезагружаем координаты сетки.gemsGrid[x][y]:setType(math.random(self.maxGemTypes)).gemsGrid[x][y].sprite.x = (x - 1) * gem.gemActualWidth + self.offsetX.gemsGrid[x][y].sprite.y = (y - 1) * gem.gemActualHeight + self.offsetY.gemsGrid[x][y].sprite.alpha = 1
end
- Удаляем все 3+ сгенерированные совпадения
self:fix()
end
Однако, для игры недостаточно создать просто такую структуру. Она должна быть обязательно подкреплена игровой логикой, которая по нашим правилам должна пересчитывать все +3 совпадения в игровой сетки. Есть специальный метод grid:checkMatches() он перебирает всю сетку на совпадения три и больше в ряд/столбец различных игровых элементов и заносит их в особую группу для дальнейшего удаления. Затем по логике игры уже добавляются новые на место удалённых элементов и игра продолжается.
function grid:checkMatches()match = false
-- Очищаем лист наших элементов для удаления
for i in pairs(self.matchedGems) do[i] = nil
- Проверяем столбцы матчаx = 1, self.width docount = 0focusox, oyy = 1, self.height dogem = self.gemsGrid[x][y]current = gem.type(count == 0) then= current= 1= x= y(current == focus) then= count + 1(current ~= focus or y == self.height) then
-- Удаляем совпадения в колонках(count >= 3) then
match = truei = 1, count do
- Добавляем элементы в удалялку:addGemToMatches(self.gemsGrid[ox][oy])= oy + 1= current= x= y= 1
end
- Проверяем строки на 3 и более
for y = 1, self.height docount = 0focusox, oyx = 1, self.width dogem = self.gemsGrid[x][y]current = gem.type(count == 0) then= current= 1= x= y(current == focus) then= count + 1(current ~= focus or x == self.width) then
-- Заносим в удалялку элементы(count >= 3) then
match = truei = 1, count do:addGemToMatches(self.gemsGrid[ox][oy])= ox + 1= current= x= y= 1
- Удаляем элементы(#self.matchedGems > 0) then:playSound("audio/gem_destroyed.raw")i,g in ipairs(self.matchedGems) do
--Вызываем метод их взрыва:explode()match
После того, как в сетке были описаны все необходимые в процессе игры методы и мелочи, оставалось только добавить дополнительный класс селектора, который помогал бы в управлении игрой пользователю. Подробно рассматривать данный класс не стоит. Так, как в его функции входит простая «отметка» того элемента на игровой сетки, на который нажал игрок. После этого, уже относительно этой метки можно обменивать выбранный элемент с любыми, что находятся выше-ниже, или левее-правее, но не более, чем на 1 клетку от него.
Если благодаря перестановки элементов будет найдено 3+ совпадение, то игра позволит окончательно сдвинуть эти элементы местами и поменять их. Если же такого не будет, то игра просто проиграет красивую анимацию и вернёт всё на свои места.
Специально для удобного управления игрой были добавлены игровые состояния - переменные, которые определяют, какие действия пользователя сейчас возможны, а какие нет. Стоит рассмотреть их подробнее.
- Список игровых состояний= { = 0, = 1,
waitingSecondGem = 2, = 3, = 4, = 5,
}- пауза. Игра приостановлена. Нельзя выбирать какие-либо элементы. Управления игрока самим процессом игры полностью отключено. Можно только нажать на кнопку выхода, которая появляется в верхней части экрана.
WaitingFirstGem - игра ждёт, пока мы выберем один из игровых элементов на экране нажатием.
WaitingSecondGem - мы уже выбрали первый элемент для перестановки и игра ждёт второй, после нажатия на который будет произведён обмен и возможно, игра перейдёт в следующие состояния.
Tninking - игра анализирует результаты наших действий, удаляет блоки и добавляет в игровую сетку новые. Управление у игрока полностью отключено в этот момент и он не может ничего сделать, кроме поставить паузу.
GamsFalling - стадия, когда игра добавляет в игровую сетку новые элементы, так-же не позволяя игроку вмешиваться. В конце этой стадии вновь происходит обдумывание получившихся результатов и игра может перейти снова в стадию thinking. Так будет до тех пор, пока сетка полностью не будет очищена от всех три в ряд совпадений.
RoundOver - последняя стадия, которая заканчивает игру. Забирает управление, отключает возможность паузы. Проигрывает красивый эффект и закрывает уровень.
Благодаря добавлению в файл game.lua специальных вставок с проверкой состояний игры, удалось достичь высокого её качества, запрета определённых игровых действий в нужные моменты и придать чёткости игровым правилам.
В целом, интерфейс всех игровых сцен аналогичен. К определённым спрайтам привязываются определённые проверки на их нажатие. Любой эвент выглядит и описывается так, как показано ниже и представляет из себя функцию. Например, вот полное описание эвента на выход из игры в главном меню.
При создании сцены изначально мы добавляем эвент на «прикосновение» к элементу (кнопке выхода), после нажатия на которую запускается функция exitButtonTouched
Привязываем эвент к нашей кнопке выхода из игры:addEventListener("touch", exitButtonTouched)
Функция выхода из игры и остановки движка
function exitGame(event):quit()
- Эвент нажата кнопка выходаexitButtonTouched(event)(event.phase=="ended") then:to(exitText, { alpha=0.2, time=0.5 } ):to(exitText, { alpha=0.2, delay=0.25, time=0.75, easing=ease.expIn} )
- Играем анимацию, после которой запускаем функцию exitGame
tween:to(exitButton, { alpha=0.2, delay=0.25, time=1.25, easing=ease.expIn, onComplete=exitGame } )
end
Компонент движка tween позволяем нам создавать разные анимации с игровыми объектами, как уже рассказывалось ранее. Благодаря ему без добавления специальных эффектов можно изменять за время прозрачность, перемещать объект в любые координаты экрана плавно с течением времени, растягивать по любым осям и много чего ещё.
В целом, весь пользовательский интерфейс построен на эвентах, привязанных к определённым объектам на экране. Давайте рассмотрим все игровые сцены, как они выглядят и что на них изображено.
Первая сцена - главное меню.
Рисунок 3.5 - Главное меню игры
В главном меню с точки зрения интерфейса пользователя будут только 2 элемента. В конечной версии приложения галочка и крест снизу будут убраны потому, что отвечают за читы для быстрого открытия всех уровней приложения. И удобной демонстрации всех возможностей игры без траты времени на прохождение. Остальные же кнопки играть и выход будут оставлены в игре. Первая из них переводит игрока на экран выбора уровней, а вторая закрывает игру.
Вторая сцена - экран выбора уровней.
Рисунок 3.6 - Экран выбора уровней
На экране выбора уровней есть много активных элементов, которые построены по аналогиями с кнопками из главного меню. Имеется 6 миров-категорий, в каждой из которых содержится по 5 уровней. Если уровни открыты, то сверху будет зелёный цвет. Если уровень недоступен, то будет подсвечен красным. При нажатии на любой из этих уровней запускается игра с теми параметрами, которые соответствуют этому уровню. При завершении любого уровня, открывается следующий для прохождения. В каждом из миров меняются цвета игровых элементов и задачи становятся сложнее, но и монет, которые может найти игрок становится больше.
Третья сцена в игре игровая.
Рисунок 3.7 - Экран игровой сцены
Интерфейс игровой части строился по аналогии с теми приложениями, которые мы уже рассматривали в самом начале. Все игровые события так-же сделаны аналогично. Нажатием на монетку можно поставить паузу и вызвать кнопку выхода из игры. Все игровые элементы реагируют на нажатия. Необходимо достигнуть нужное для завершения уровня количество очков. Задний фон для красоты постоянно меняет цвет. Также за монетки можно использовать бонусы, которые находятся в верхней части экрана. Активируются они по нажатию. В самом верху будет появится шкала прогресса, которая поможет понять, насколько вы близки к завершению уровня. Также каждые 10 сек. Игра выбирает случайный тип игрового объекта, за уничтожение таких блоков вы получаете двойные очки и быстрее достигаете игровую цель.
Отладка большинства компонентов и функций игрового приложения проходила сразу в процессе их разработки.
В начале игры у игрока нет никаких монет на бонусы и все уровни, кроме первого, являются закрытыми и поиграть в них невозможно. Сделав специальные команды на обновления данных в хранилище и проверки доступности любых уровней, тщательно протестировав полученные результаты. Мы убедились в том, что база данных полностью соответствует нашим требованиям. Игровые данные сохраняются и могут по мере необходимости вновь загружаться для использования. После перезагрузки, перезапуска приложения, ничего не меняется и всё, чего добился игрок остаётся.
Все функции по работе с базой данных и игровыми сохранениями находятся в файле database.lua.
Так, как моя игра включает в себя огромное количество различных файлов, тестировать пришлось множество модулей и классов, которые являлись важными составляющими игры.
В первую очередь тестирование проводилось в двух самых важных классах игры - классе gem (игрового объекта) и классе grid (игровая сетка). В этих классах было множество различных методов от создания отдельных элементов на экране, до создания целых сеток размером с половину экрана, которая заполняется случайными игровыми объектами. При разработке данных классов было потрачено больше всего сил и времени, поэтому на окончательном этапе тестирования ошибок в них не обнаружилось. Все классы работают корректно, все методы класса вызываются правильно. Данные были точны. Все три в ряд совпадения, найденные в игровой сетке учитывались.
Когда основные два класса были просмотрены, было проведено тестирование вспомогательных классов selector (выбиратель игровых элементов) и класса explosion (взрыватель). Так, как эти классы гораздо меньше тех, что мы рассмотрели ранее. Их тестирование прошло быстро. Ошибок не обнаружилось. Селектор правильно запоминал выбранные элементы в процессе игры и в игровой логике всегда точно определялось, в какие стороны игрок может переставить текущий элемент.