пятница, 8 апреля 2011 г.

Пишем match-3

Всем привет. Очень давно не писал. Дело в том что игра практически готова, и сейчас художник, с которым я работаю, дорисовывает последние моменты. Обо всем этом расскажу когда все будет закончено.
А пока попробую свои силы в переводе урока из книги "ActionScript 3.0 Game Programming University". О ней говорилось в прошлом посте. Мне понравился урок по созданию игры на механике match-three. Сам урок я понял с первого раза, а для чего же я сделал перевод? Во-первых, надеюсь это поможет кому-то, кто не силен в инглише. Во-вторых, перевод позволил мне разобрать весь код по косточкам, очень тщательно.

Перед тем как я начну, выдам пару соглашений. Перевод может показаться немного деревянный, но уж примите, такой какой есть. Некоторые слова, выражения могут иметь более подходящие аналоги в русском языке. Если это будет критично, исправлю. В программе используется дополнительный класс PointBurst. Я не буду его сейчас описывать, а скорее всего сделаю это в следующем посте, т.к. это довольно интересный класс. Просто пока будем знать, что этот класс выдает эффект всплывающих очков в определенном месте.
И последнее, я не придумывал ничего нового, а просто сделал перевод, т.е. все благодарности автору книги Gary Rosenzweig.




Поле (board) = игровая доска, где располагаются фишки, визуальное отображение
Фишка (piece) = элемент, который мы комбинируем с другими.
Линия (match) = ряд или колонка, последовательность минимум из 3-х фишек одного типа.
Сетка (grid)=2-мерная матрица, которая в цифровом виде дублирует доску.


Обзор функциональности игры.
Последовательность всех событий в игре включает в себя 12 шагов, где каждый шаг представляет собой отдельную задачу.
1.Создание случайно-генерируемого игрового поля.
Создание поля 8х8 со случайно расположенными фишками, каждая из которых может иметь 7 разных вариантов отображения.
2.Проверка на линии.
Существуют некоторые ограничения на начальное расположение фишек на поле. Во-первых, поле, при старте игры не должно содержать линий.
3.Проверка на возможность первого хода.
Второе ограничение, состоит в том, чтобы дать игроку сделать хотя бы один ход. То есть на поле не должно быть изначально нерешаемой композиции.
4.Игрок выбирает 2 фишки.
Фишки должны находиться рядом друг с другом (по вертикали или по горизонтали) и их обмен местами происходит с целью образовать линию.
5.Фишки меняются местами.
Здесь используем простейшую анимацию.
6.Проверка на линии.
После обмена ищем линии на поле. Если линий не найдено, меняем фишки обратно местами.
7.При нахождении линии вознаграждаем игрока очками.
8.Убираем линии с поля.
9.Сдвигаем верхние фишки на место исчезнувших.
10.Заполняем образовавшиеся пустоты.
11.Снова проверяем на линии. Возвращаемся к пункту 6.
После того как все фишки упали вниз на свободные места, а новые заполнили пустоты надо заново проверить на линии.
12.Проверка на возможность хода.
Перед тем как передать игроку ход, надо убедиться, есть ли на поле возможные ходы.

Наш клип и класс MatchThree
Клип MatchThree.fla очень прост. Кроме шрифта Arial в библиотеке, у нас здесь клип из семи кадров. В слое Color в каждом кадре разный тип фишки. Верхний слой Select используется для обрамления (выделения) выбранной фишки и в последствие будет активироваться свойством visible.




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

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



Настройки игры будут храниться в 5 различных переменных. Во-первых, сетка (grid) будет содержать ссылки на все фишки (Pieces). Сетка представляет собой двумерный массив. Каждый элемент сетки (grid) будет содержать массив из 8 фишек (Pieces). Все это будет выглядеть как матрица, массив 8х8 и к любой фишке мы сможем обратиться через ссылку grid[x][y].
Спрайт GameSprite будет содержать все созданные нами спрайты и мувиклипы. Так мы будем отделять их от любой другой графики уже существующей на сцене.
Переменная firstPiece будет содержать ссылку на первую кликнутую фишку.
Две логические (Boolean) переменные isDropping, isSwapping будут отслеживать, какие фишки нам надо анимировать в данный момент. Переменная gameScore будет хранить очки игрока.



Настройка сетки
Первые функции будут определять переменные игры, включая и настройку сетки.
Настройка игровых переменных
Для начала игры необходимо определить, инициализировать все игровые переменные. Начнем с создания сетки (grid), двумерного массива 8х8. Затем используем функцию setUpGrid для заполнения этого массива.

Примечание.
Нет необходимости заполнять все элементы массива пустыми слотами для инициализации. При установке значения для любого элемента массива, все предшествующие элементы заполняются значением undefined. К примеру, в только созданном массиве присваиваем третьему элементу (под индексом [2]) значение «My String». Массив будет иметь длину (length) равную 3, а элементы [0] и [1] получат значения undefined.


Дальше определим переменные isDropping, isSwapping и gameScore. Также установим на событие ENTER_FRAME слушатель для запуска всех передвижений фишек в игре.

Настройка сетки Для создания и инициализации сетки (grid) используем цикл с условием while(true). В цикле создадим элементы сетки. Также создадим спрайт gameSprite, который будет содержать мувиклипы наших фишек. Затем добавим 64 рандомных фишки с помощью функции addPiece. Эту функцию рассмотрим позже, а пока просто будем знать, что она добавляет фишку в сетку и в gameSprite. Далее проверим два условия необходимых для начала игры. Функция lookForMatches возвращает массив найденных линий. Эту функцию мы также рассмотрим попозже. В данный момент нам известно, что функция вернет 0 если на экране нет линий. Оператор continue пропускает оставшуюся часть цикла и возвращает нас к его началу. После этого вызовем функцию lookForPossibles, которая проверит, есть ли на поле возможные ходы. Если ходов нет, функция вернет false, и это будет означать что начать игру нельзя. В случае если мы прошли оба этих условия, оператор break прервет цикл, и мы добавим gameSprite на сцену.

Добавление фишек
Функция addPiece создает рандомную фишку в определенном столбце и колонке, и присваивает ей местоположение на экране.


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


Мувиклип Select находящийся внутри клипа Piece, представляет собой рамку, которая повляется над кликнутой фишкой. Изначально его свойство visible будет false. Клип фишки Piece мы добавляем в gameSprite. Для того чтобы добавить фишку в сетку (grid) используем двойные прямые скобки grid[col][row] = newPiece. На каждую фишку повесим слушатель (click listener). Затем вернем ссылку на фишку (Piece). Мы не будет использовать эту ссылку в функции setUpGrid, а используем ее позже при создании новых фишек, для замены пустот.



Взаимодействие с игроком
При выборе, клике игроком фишки, дальнейшие наши действия зависят от того была это первая или вторая кликнутая фишка. Если это была первая фишка, она выделяется. Если игрок опять кликнет на эту фишку, выделение снимется.



Когда игрок кликает на вторую фишку, отличную от первой мы должны определить, можно ли совершить обмен. Сначала снимем выделение с первой фишки. Затем проверим, являются ли они соседями по вертикали или по горизонтали со второй. В обоих случаях вызовем функцию makeSwap. Она совершит обмен и установит, образовались ли линии. В любом случае, переменная firstPiece обнуляется (null) и становится готова к следующему выбору игрока. Если же фишки не являются соседями, считаем что игрок сбросил свой выбор с первой фишки и начал со второй



Функция makeSwap меняет фишки местами и проверяет, образовались ли на поле линии. Если нет, меняет фишки обратно местами. Если да, и обмен возможен, переменная isSwapping принимает значение truе, что дает сигнал к началу анимации движения фишек.



Чтобы произвести обмен, мы должны сохранить расположение первой фишки во временно хранилище. Далее перемещаем первую фишку на место второй, а вторую на уже сохраненные координаты первой.




После обмена мы должны обновить значения в сетке.


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

Анимация движения фишек
Используем интересный, но не очевидный метод анимации. О каждой фишке нам известно, в каком ряду и колонке она находится, благодаря динамических свойствам row и col. А также мы можем узнать ее расположение на экране исходя из свойств x и y. Еще не забудем про константы spacing, offsetX, offsetY. К примеру, фишка в 3 колонке получит значение x = 3*spacing + offset. Что же будет, если фишка переместится в другую колонку? Если мы присвоим фишке значение col равное 4, тогда x = 4*spacing + offset, что находится на 45 пикселей правее. Поэтому заставим фишку двигаться правее, ближе к месту своего назначения. Если делать это каждый кадр, то вскоре фишка встанет на свое новое место назначения и прекратит двигаться (ведь ее значения col и x будут соответствовать друг другу).
Используя такую технику, можно анимировать любую фишку в процессе ее движения к новому месту. Нам даже не придется настраивать анимацию на уровне мувиклипа. Все что нам надо сделать, это изменить свойство col или row фишки (Piece). А функция movePieces уже позаботится об остальном.
Функция movePieces вызывается каждый кадр, мы ведь установили это еще в самом начале класса, с помощью слушателя. Она проверяет все фишки на соответствие значений col и row с x и y.

Примечание.
В функции movePieces мы используем шаг 5 каждый кадр. Это значение всегда должно быть кратно значению spacing. Если бы spacing был равен, к примеру, 48, мы бы использовали 4, 6 или 8.



В начале функции movePieces мы устанавливаем флаг madeMove в false. Затем, в случае любого смещения, сбрасываем его в true. Если же ни в одну сторону смещения не было, madeMove остается равным false. Затем этот флаг сравниваем со свойствами isDropping и isSwapping. Если isDropping true, а madeMove false, значит все падающие фишки встали на место. Самое время проверить поле на линии.
Если же isSwapping true и madeMove false, значит две фишки только что закончили обмен. И в этом случае проверим поле на линии.



Поиск линий
В нашей программе есть две сложных задачи. И первая из них, это поиск линий. Задача их нахождения довольно непроста и не решается простым методом.

Деление задачи на мелкие шаги.
Попробуем разделить задачу на более мелкие подзадачи, и будем делать это до тех пор, пока не сведем подзадачу к простому решению.
Итак, функция findAndRemoveMatches делится на две части. Поиск линий и их удаление. Вторая задача, удаление, простая по своей сути. Удаляем фишки из gameSprite, устанавливаем в сетке на место удаленных фишек нули и вознаграждаем игрока очками.

Примечание
Количество очков зависит от количества фишек в линии. Три фишки означают (3-1)*50 или 100 очков за каждую фишку. Четыре фишки, (4-1)*50 или 150 очков за фишку, минимум 600 очков.


Также после удаления фишек, надо сбросить вниз те, которые были над удаленными. Это тоже довольно непростая задача.
Итак, у нас есть две сложные задачи, найти линии и решить, что делать с фишками над исчезнувшими. Мы возложим эти задачи на функции lookForMatches и affectAbove. Остальное сделаем прямо в функции findAndRemoveMatches.

Функция findAndRemoveMatches
Мы перебираем в цикле все линии и помещаем их в массив. Даем очки за каждую линию. Далее проходим по всем фишкам, которые надо удалить и убираем их.

Примечание
Когда мы берем сложную задачу и возлагаем ее решение на функции, которые мы еще не определили, это называется top-down programming. Вместо того чтобы думать и ломать голову как искать линии, мы переложим это на функцию lookForMatches. То есть выстраиваем нашу программу сверху вниз, заботясь о том как все выглядит в целом, а функции на которые мы перекладываем задачи, рассматриваем позже.


Функция findAndRemoveMatches выполняет две задачи. Первая, addNewPieces заполняет недостающее количество фишек в столбце. Затем вызываем lookForPossibles чтобы убедиться, что у игрока еще есть ходы на поле. Она нужна только в том случае, если новых линий больше не найдено. Это происходит если findAndRemoveMatches была вызвана после того как упали новые фишки, а линий не было найдено.

Функция lookForMatches
Цель функции создать массив из найденных линий. Определяем линии более чем из 2-х фишек. Для этого делаем обход в цикле, сначала по рядам, потом по колонкам. Проверяем отрезок из первых 6 фишек в каждом ряду. Из 7 и 8 проверять нет смысла, так как они не смогут образовать линии больше чем из 2-х фишек.
Функции getMatchHoriz и getMatchVert определяют длину линии от начала передаваемого в них элемента. К примеру, если элемент [3][6] фишка типа 4, [4][6] тоже фишка типа 4, а [5][6] фишка типа 1, вызов getMatchHoriz(3,6) вернет 2, поскольку найдена линия из 2 фишек.
Если линия найдена, эффективно будет пропустить пару циклов и перескочить на пару шагов вперед. К примеру, у нас есть линия в [2][1],[2][2],[2][3] и [2][4], мы просто проверяем [2][1], возвращаем результат 4 и пропускаем [2][2],[2][3], [2][4] чтобы сразу начать с [2][5].
При каждой линии найденной с помощью getMatchHorizon или getMatchVert они возвращают массив содержащий каждую фишку в линии. Эти найденные массивы мы добавляем в массив matches в функции lookForMatches.


Функции getMatchHorizon и getMatchVert
Разберем функцию getMatchHorizon. Учитывая переданные в нее колонку и ряд она проверяет следующую фишку на совпадение типов. Если это так, она добавляется в массив. Продолжает двигаться горизонтально, пока не встретит несовпадение. Затем она сообщает, что массив составлен. Он может быть составлен даже из одной фишки, если следующая не совпала. А может вернуть и несколько.
Функция getMatchVert практически идентична getMatchHorizon, за исключением того что поиск производится не по рядам а по колонкам. Функция affectAbove Рассмотрим affectAbove. Мы передаем в нее фишку, и ожидаем когда она скажет всем фишкам над собой, что можно сдвинуться на шаг вниз. В цикле просматриваем фишки в колонке над текущей. К примеру, если текущая [5][6], то проверяем [5][5], [5][4], [5][3], [5][2], [5][1], [5][0] именно в таком порядке. Значение row этих фишек увеличивается на 1. Кроме того они передают в сетку новые данные о своем местоположении. Помним, что с функцией movePieces нам не надо беспокоиться об анимации. Мы просто сообщаем фишке новое место расположения.

Функция addNewPieces
Следующая функция, которую мы должны написать это addNewPieces. Она проверяется все пустые (null) ячейки в сетке и заполняет их новыми фишками. Хотя значения col и row и получают свое конечное значение, y получает значение сверху экрана, поэтому фишки падают вниз. Переменная isDropping принимает true, что указывает на анимацию падающей фишки.



Поиск возможных ходов
Поиск возможных линий не намного проще поиска линий.
Самый простой способ, это перебрать доску, делая обмен для каждой фишки. [0][0] с [1][0], затем [1][0] с [2][0] и т.д. При каждом обмене ищем линии, и при нахождении первой же прекращаем поиск и возвращаем true. Такой brute-force подход будет работать, но будет очень уж медленным , тем более на старых машинах. Существует более эффективный способ.
Какие варианты у нас могут быть для составления линии? Обычно это две фишки одного типа в ряду. Третья же фишка отличается типом, но может быть обменена на любую из трех в свободных направлениях. Либо же две фишки одного типа разделенные между собой одной фишкой другого типа, и теперь может произойти обмен в 2 направлениях.
Рисунок показывает нам два этих случая разбитых на 6 шаблонов.




Теперь зная что есть всего несколько шаблонов, которые мы должны найти, мы можем по принципу top-down программирования начать с использования функции lookForMatches, а о функции поиска шаблонов позаботимся потом.
Взглянув на рисунок увидим две черные фишки, входящие в линию и 3 фишки которые возможно могут быть такого же типа. Обозначим крайне левую черную фишку как [0][0]. Видим что фишка [1][0] такого же типа. Осталось найти такую же фишку на позиции [-1][-1], [-2][0] или [-1][1]. А также с другой стороны [2][-1], [2][1] и [3][0]. Итак, мы должны найти в 6 позициях хотя бы одну совпадающую по типу фишку.




При вызове функции мы будем передавать в нее массив двух фишек совпадающих по типу, и массив фишек окружающих третью, из которых хотя бы одна должна совпасть. Это будет выглядеть примерно так.
matchPattern(col, row, [[1, 0]], [[-2, 0],[-1, -1],[-1, 1],[2, -1],[2, 1],[3, 0]])
Также нам нужна аналогичная функция для других примеров шаблонов на рис. 8,9. Они оба вертикальны.
Функция lookForPossibles производит поиск по всем позициям доски.
Хотя функция matchPattern и будет выполнять важную задачу, сама по себе она не большая. Она получает тип фишки из определенных col и row. Далее она проходит по mustHave списку и проверяет фишки на соответствующих позициях. Если совпадений не найдено, двойная линия не найдена, нет смысла продолжать и функция возвращает false. В противном случае, каждая фишка из needOne проверяется. Если хотя бы одна фишка совпадает по типу, возвращаем true. Если ни одна, возвращаем false. Все сравнения в matchPattern производятся через matchType. Оформим это отдельной функцией т.к. мы часто будем обращаться к фишкам, которые не в сетке. К примеру, если мы передадим в matchPattern col и row [5][0], то проверять фишки -1 нет смысла, к примеру 4 -1 , т.к. они не попадают в сетку. Функция будет проверять, находится ли фишка на поле, и если да, то будет сравнивать ее тип с требуемым.

Счет и окончание игры
В функции findAndRemoveMatches мы вызывали addScore для добавления игроку очков. Эта простая функция суммирует очки и передает необходимые данные в текстовое поле на экране.


Если на поле больше нет возможных ходов, функция endGame переносит нас на таймлайне в кадр gameover. А также использует swapChildIndex чтобы задвинуть gameSprite в фон, таким образом все спрайты кадра gameover окажутся над игровым полем.
Мы делаем это для того чтобы не удалять игровое поле после окончания игры, а оставить его игроку на рассмотрение.


Мы уберем сетку, когда игрок будет готов двигаться дальше. Для этого вызовем функцию cleanup.



На таймлайне функция cleanUp привязывается к кнопке playAgain и запускается, перед тем как начать новую игру.

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

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

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

UPD. Небольшое дополнение.
Как еще можно улучшить игру. Ну во-первых подсказки. Все наверно замечали подсказки в играх на такой механике. Когда игрок долгое время ничего не делает, начинают подмигивать две фишки, с которыми можно произвести обмен. Все это можно сделать с помощью функции lookForPossibles. А как, останется в качестве домашнего задания.
Второе, бонусные фишки. Всегда можно включить в нашу флешку еще один слой Bonus такого же типа как Select. И приладить к фишке свойство bonus. А дальше я думаю понятно как использовать клик на этой фишке и дополнительные очки.
Теперь важное замечание, подсказанное из комментариев. Об этом в книге нигде не говорится, но этот момент лучше не упускать.
1. В функции setUpGrid мы в цикле создаем начальное поле игры. И каждый цикл добавляем фишки, вне зависимости от того, было поле создано играбельным или нет.
2. В функции addPiece мы на каждую фишку вешаем слушатель (addEventListener). А при ее (фишки) удалении не снимаем его (removeEventListener).
Что мы должны отсюда вынести? Такие недоработки рано или поздно приведут в утечкам памяти. Какие исправления мы можем внести в наш код?
1. Добавлять фишки тогда, когда перед нами будет играбельное поле.
2. Использовать флаг weakReference.
Пример: object.addEventListener(Event.CLICK,handleClick false,0,true);
При удалении фишки рекомендуется использовать removeEventListener.

Спасибо за подсказки.

14 комментариев:

  1. Вопрос к опытным ребятам. Есть смысл разместить перевод на flashgameblogs ?

    ОтветитьУдалить
  2. Не удержался и опубликовал.

    ОтветитьУдалить
  3. Здравствуйте, не смотря на то, что это перевод, хотелось бы чтобы вы исправили (или хотя бы указали в примечании) ошибку,а именно в методе findAndRemoveMathces() при удалении Piece вы не убираете у него EventListener, поэтому он никогда не будет освобожден сборщиком мусора и рано или поздно оно отжрет всю память. То же самое произойдет и при начальном создании сетки когда либо есть линии либо нет ходов.
    Как вариант, хотя бы укажите ему weakReference.
    Да и еще, хотя это уже больше относится к оптимизации, если у вас при начальной генерации будут линии или не будет ходов, память все равно будет забиваться фишками (мувиклип которых вобще говоря довольно тяжелый) поэтому лучше было бы сначала проверить расположение, а потом каждой фишке уже добавить соответствующий мувиклип.
    И еще раз простите за занудство, и спасибо за хорошую статью +)

    ОтветитьУдалить
    Ответы
    1. Я не хочу надоедать, но вот просто недавно сел самостоятельно программировать и это оказалось куда сложнее чем я думал. На примере match 3 решил научиться, раз самая простая (сказали) и вот возникла проблема, не могу правильно программный текст вставить, что бы он относился к объекту, вы мне не поможете. Потому, что в роди бы все верно, но все не так-чайник.

      Удалить
    2. Для начинающих в сети очень много учебников и уроков. Лучше начинать с простого. Поэтому сразу мач-3 не советую.

      Насчет текста, тут совсем не сложно, главное понять самому.
      Ищи информацию на следующих сайтах
      flashgamedev.ru
      Здесь никогда не откажут в помощи.

      Справка от Adobe сильнейшая штука, я ее раньше боялся, думал все запутанно, и сухим текстом, но в ней можно найти ответы практически на все вопросы
      http://help.adobe.com/ru_RU/ActionScript/3.0_ProgrammingAS3/

      ПОсмотри у меня в ранних статьях есть ссылки на учебники.

      Ну и конечно google.

      Удалить
  4. Я уже приступил к написанию своего match-3 ;)

    ОтветитьУдалить
  5. Вот и правильно. У тебя там еще арканоид на подходе.

    ОтветитьУдалить
  6. static const numPieces:uint = 7;
    ...

    newPiece.type = Math.ceil(Math.random()*7);


    ??????????????????????????????????????????

    ОтветитьУдалить
  7. Да, есть такое дело. Спасибо, что заметили. Это конечно не критично, но довольно важное замечание.

    ОтветитьУдалить
  8. "ActionScript 3.0 Game Programming University"
    Блин, не знал о такой книге, спасибо.
    Было бы хорошо сделать какой то привью, или демку выполненной работы, чтобы показать всю механику, как например у Эмануель Феронато, написали статейку, и в конце - что должно получится :)
    Насчет алгоритма "Матча три" - интересный, и очень простой, но не сталкивался с подобной реализацией :)

    ОтветитьУдалить
  9. Можно было, просто не разбирался до сих пор как в блогспот флешку добавлять. А если есть желание, можно зайти на flashgameu.com там есть исходники к каждой главе, ну а там уже скомпилировать дело нехитрое.

    ОтветитьУдалить
  10. Константы нужно капслоком писать.

    ОтветитьУдалить
  11. Ой, вот что я упустил. То-то я думаю игрушка моя не продается.

    ОтветитьУдалить
  12. добрый вечер! Подскажите пожалуйста
    код подсказки

    ОтветитьУдалить