Пишем Match-Three

1
Всем коллегам, здравствуйте.
Я начинающий FlashGame-разработчик. Скажу в самом начале, у меня нет ни одной выпущенной или проданной игры. Но мне понравился урок по созданию игры на механике match-three. Сам урок я понял с первого раза, а для чего же я сделал перевод? Во-первых, надеюсь это поможет кому-то, кто не силен в «инглише». Во-вторых, перевод позволил мне разобрать весь код по косточкам, очень тщательно.

Перед тем как я начну, выдам пару соглашений. Урок взят мной из книги Gary Rosenzweig — «ActionScript 3.0 Game Programming University». Я уже писал в своем личном блоге, что не все наши флеш-разработчики положительно относятся к этой книге. Перевод может показаться немного деревянным и не очень приятным на слух. Некоторые слова, выражения могут иметь более подходящие аналоги в русском языке. Если это будет критично, исправлю. В программе используется дополнительный класс 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.



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

package {
   import flash.display.*;
   import flash.events.*;
   import flash.text.*;
   import flash.utils.Timer;

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


public class MatchThree extends MovieClip {   
   // constants   
   // количество типов фишек   
   static const numPieces:uint = 7;   
   // расстояние между двумя фишками   
   static const spacing:Number = 45;   
   // отступ слева   
   static const offsetX:Number = 120;   
   // отступ сверху   
   static const offsetY:Number = 30; 


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


// игровая сетка и необходимые настройки   
private var grid:Array;   
private var gameSprite:Sprite;   
private var firstPiece:Piece;   
private var isDropping,isSwapping:Boolean;   
private var gameScore:int;   


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

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


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


// инициализация сетки () и старт игры   
public function startMatchThree() {        
   // создание и инициализация сетки (grid)     
   grid = new Array();    
   for(var gridrows:int=0;gridrows<8;gridrows++) {     
      grid.push(new Array());    
   }    
   setUpGrid();    
   isDropping = false;    
   isSwapping = false;    
   gameScore = 0;    
   addEventListener(Event.ENTER_FRAME,movePieces);   
}   


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


public function setUpGrid() {    
   // цикл, пока не создадим играбельную сетку    
   while (true) {     
      // создаем спрайт     
      gameSprite = new Sprite(); 
         
      // добавляем 64 рандомных фишки     
      for(var col:int=0;col<8;col++) {      
         for(var row:int=0;row<8;row++) {       
         addPiece(col,row);      
         }     
      }     


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


      // пробуем снова, если на поле есть линии     
      if (lookForMatches().length != 0) continue; 
         
      // пробуем снова, если на поле нет ни одного хода     
      if (lookForPossibles() == false) continue; 
         
   // нет линий, и есть ход, прерываем цикл     
   break;    
   }         
   // добавляем спрайт    
   addChild(gameSprite);   
}   


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

// создаем рандомную фишку, добавляем ее в спрайт и сетку   
public function addPiece(col,row:int):Piece {    
   var newPiece:Piece = new Piece();    
   newPiece.x = col*spacing+offsetX;    
   newPiece.y = row*spacing+offsetY;  


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

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


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


   newPiece.select.visible = false;    
   gameSprite.addChild(newPiece);    
   grid[col][row] = newPiece;    
   newPiece.addEventListener(MouseEvent.CLICK,clickPiece);    
   return newPiece;   
}   


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


// игрок кликает на фишку   
public function clickPiece(event:MouseEvent) {    
   var piece:Piece = Piece(event.currentTarget);        

   // первая фишка выбрана    
   if (firstPiece == null) {     
      piece.select.visible = true;     
      firstPiece = piece;         

   // повторный клик на первой фишке    
   } else if (firstPiece == piece) {     
      piece.select.visible = false;     
      firstPiece = null;   


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


   // клик на второй фишке    
   } else {     
      firstPiece.select.visible = false; 
         
      // одинаковый ряд, проверяем соседство в колонке     
      if ((firstPiece.row == piece.row) && (Math.abs(firstPiece.col-piece.col) == 1)) {     
         makeSwap(firstPiece,piece);      
         firstPiece = null;           

      // одинаковая колонка, проверяем соседство в ряду     
      } else if ((firstPiece.col == piece.col) && (Math.abs(firstPiece.row-piece.row) == 1)) { 
         makeSwap(firstPiece,piece);                
         firstPiece = null;           

      // нет соседства, скидываем выбор с первой фишки     
      } else {      
         firstPiece = piece;      
         firstPiece.select.visible = true;     
      }    
   }   
} 


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


// начало анимации обмена двух фишек   
public function makeSwap(piece1,piece2:Piece) {    
   swapPieces(piece1,piece2);        

   // проверяем, был ли обмен удачным    
   if (lookForMatches().length == 0) {     
      swapPieces(piece1,piece2);    
   } else {     
      isSwapping = true;    
   }   
}   


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



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

// обмен двух фишек   
public function swapPieces(piece1,piece2:Piece) {    
   // обмениваем значения row и col    
   var tempCol:uint = piece1.col;    
   var tempRow:uint = piece1.row;    
   piece1.col = piece2.col;    
   piece1.row = piece2.row;    
   piece2.col = tempCol;    
   piece2.row = tempRow;        

   // изменяем позицию в сетке (grid)    
   grid[piece1.col][piece1.row] = piece1;    
   grid[piece2.col][piece2.row] = piece2;       
} 


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

Анимация движения фишек
Используем интересный, но не очевидный метод анимации. О каждой фишке нам известно, в каком ряду и колонке она находится, благодаря динамических свойствам 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.


// если какая-то фишка не на своем месте, двигаем ее чуть ближе   
// такое происходит в случае обмена, или падения фишки   
public function movePieces(event:Event) {    
   var madeMove:Boolean = false;    
   for(var row:int=0;row<8;row++) {     
      for(var col:int=0;col<8;col++) {      
         if (grid[col][row] != null) {             

         // смещаем вниз       
         if (grid[col][row].y < grid[col][row].row*spacing+offsetY) {        
            grid[col][row].y += 5;        
            madeMove = true;               

         // смещаем вверх       
         } else if (grid[col][row].y > grid[col][row].row*spacing+offsetY) {        
            grid[col][row].y -= 5;        
            madeMove = true;               
         // смещаем вправо 
      
         } else if (grid[col][row].x < grid[col][row].col*spacing+offsetX) {        
            grid[col][row].x += 5;        
            madeMove = true;          

         // смещаем влево       
         } else if (grid[col][row].x > grid[col][row].col*spacing+offsetX) {        
            grid[col][row].x -= 5;        
            madeMove = true;       
         }      
      }     
   }    
}    


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


   // все падения завершены    
   if (isDropping && !madeMove) {     
      isDropping = false;     
      findAndRemoveMatches();         

      // все обмены завершены    
   } else if (isSwapping && !madeMove) {     
      isSwapping = false;     
      findAndRemoveMatches();    
   }   
}   


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

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

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


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

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

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



public function findAndRemoveMatches() {    
   // формируем список линий    
   var matches:Array = lookForMatches();    
   for(var i:int=0;i<matches.length;i++) {     
      var numPoints:Number = (matches[i].length-1)*50;     
      for(var j:int=0;j<matches[i].length;j++) {      
         if (gameSprite.contains(matches[i][j])) {       
            var pb = new PointBurst(this,numPoints,matches[i][j].x,matches[i][j].y);       
            addScore(numPoints);       
            gameSprite.removeChild(matches[i][j]);       
            grid[matches[i][j].col][matches[i][j].row] = null;       
            affectAbove(matches[i][j]);      
         }     
      }    
   }

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


   // добавляем новую фишку вверху поля    
   addNewPieces(); 
       
   // линий не найдено, проверим на возможность хода    
   if (matches.length == 0) {     
      if (!lookForPossibles()) {      
         endGame();     
      }    
   }   
}  


Функция 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.


//возвращаем массив всех найденных линий   
public function lookForMatches():Array {    
   var matchList:Array = new Array();        

   // поиск горизонтальных линий    
   for (var row:int=0;row<8;row++) {     
      for(var col:int=0;col<6;col++) {      
      var match:Array = getMatchHoriz(col,row);      
      if (match.length > 2) {       
         matchList.push(match);       
         col += match.length-1;      
         }     
      }    
   }  
      
   // поиск вертикальных линий    
   for(col=0;col<8;col++) {     
      for (row=0;row<6;row++) {      
      match = getMatchVert(col,row);      
         if (match.length > 2) {       
         matchList.push(match);       
         row += match.length-1;      
         } 
           
      }    
   }    
   return matchList;   
}  

Функции getMatchHorizon и getMatchVert
Разберем функцию getMatchHorizon. Учитывая переданные в нее колонку и ряд она проверяет следующую фишку на совпадение типов. Если это так, она добавляется в массив. Продолжает двигаться горизонтально, пока не встретит несовпадение. Затем она сообщает, что массив составлен. Он может быть составлен даже из одной фишки, если следующая не совпала. А может вернуть и несколько.

// поиск горизонтальных линий из заданной точки   
public function getMatchHoriz(col,row):Array {    
   var match:Array = new Array(grid[col][row]);    
   for(var i:int=1;col+i<8;i++) {     
      if (grid[col][row].type == grid[col+i][row].type) {      
         match.push(grid[col+i][row]);     
      } else {      
         return match;     
      }    
   }    
   return match;   
} 


Функция getMatchVert практически идентична getMatchHorizon, за исключением того что поиск производится не по рядам а по колонкам.

// поиск вертикальных линий из заданной точки   
public function getMatchVert(bol,row):Array {    
   var match:Array = new Array(grid[col][row]);    
   for(var i:int=1;row+i<8;i++) {     
      if (grid[col][row].type == grid[col][row+i].type) {      
         match.push(grid[col][row+i]);     
      } else {      
         return match;     
      }    
   }    
   return match;   
}   


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


//заставляет все фишки над переданной в функцию двигаться вниз   
public function affectAbove(piece:Piece) {    
   for(var row:int=piece.row-1;row>=0;row--) {     
      if (grid[piece.col][row] != null) {      
         grid[piece.col][row].row++;      
         grid[piece.col][row+1] = grid[piece.col][row];      
         grid[piece.col][row] = null;     
      }    
   }   
}



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


// если в колонке отсутствует фишка, добавляем новую, падающую сверху.   
public function addNewPieces() {    
   for(var col:int=0;col<8;col++) {     
   var missingPieces:int = 0;     
   for(var row:int=7;row>=0;row--) {      
      if (grid[col][row] == null) {       
         var newPiece:Piece = addPiece(col,row);       
         newPiece.y = offsetY-spacing-spacing*missingPieces++;       
         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 производит поиск по всем позициям доски.

// проверка на возможные ходы по составлению линий на поле   
public function lookForPossibles() {    
   for(var col:int=0;col<8;col++) {     
      for(var  row:int=0;row<8;row++) { 
           
         // воможна горизонтальная, две подряд      
         if (matchPattern(col, row, [[1,0]], [[-2,0],[-1,-1],[-1,1],[2,-1],[2,1],[3,0]])) {       
            return true;      
         }            

         // воможна горизонтальная, две по разным сторонам      
         if (matchPattern(col, row, [[2,0]], [[1,-1],[1,1]] ) {       
            return true;      
         }            

         // возможна вертикальная, две подряд      
         if (matchPattern(col, row, [[0,1]], [[0,-2],[-1,-1],[1,-1],[-1,2],[1,2],[0,3]])) {       
            return true;      
         }            

         // воможна вертикальная, две по разным сторонам       
         if (matchPattern(col, row, [[0,2]], [[-1,1],[1,1]])) {       
            return true;      
         }     
      }    
   }        

   // не найдено возможных линий    
   return false;   
}   


Хотя функция matchPattern и будет выполнять важную задачу, сама по себе она не большая. Она получает тип фишки из определенных col и row. Далее она проходит по mustHave списку и проверяет фишки на соответствующих позициях. Если совпадений не найдено, двойная линия не найдена, нет смысла продолжать и функция возвращает false. В противном случае, каждая фишка из needOne проверяется. Если хотя бы одна фишка совпадает по типу, возвращаем true. Если ни одна, возвращаем false.

public function matchPattern(col,row:uint, mustHave, needOne:Array) {    
   var thisType:int = grid[col][row].type;        
   // убедимся, что есть вторая фишка одного типа    

   for(var i:int=0;i<mustHave.length;i++) {     
      if (!matchType(col+mustHave[i][0], row+mustHave[i][1], thisType)) {      
         return false;     
      }    
   }        

   // убедимся,  что третья фишка совпадает по типу с двумя другими    
   for(i=0;i<needOne.length;i++) {     
      if (matchType(col+needOne[i][0], row+needOne[i][1], thisType)) {      
         return true;     
      }    
   }    
   return false;   
}


Все сравнения в matchPattern производятся через matchType. Оформим это отдельной функцией т.к. мы часто будем обращаться к фишкам, которые не в сетке. К примеру, если мы передадим в matchPattern col и row [5][0], то проверять фишки -1 нет смысла, к примеру 4 -1, т.к. они не попадают в сетку.
Функция будет проверять, находится ли фишка на поле, и если да, то будет сравнивать ее тип с требуемым.

public function matchType(col,row,type:int) {    
   // убедимся, что фишка не выходит за пределы поля    
   if ((col < 0) || (col > 7) || (row < 0) || (row > 7)) return false;    
      return (grid[col][row].type == type);   
}   


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

public function addScore(numPoints:int) {    
   gameScore += numPoints;    
   MovieClip(root).scoreDisplay.text = String(gameScore);   
}


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

public function endGame() {    
   // сдвигаем в фон    
   setChildIndex(gameSprite,0);    
   // переходим в экран окончания игры    
   gotoAndStop("gameover");   
}  


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


public function cleanUp() {    
   grid = null;    
   removeChild(gameSprite);    
   gameSprite = null;    
   removeEventListener(Event.ENTER_FRAME,movePieces);   
} 


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

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

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

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

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

Спасибо за подсказки.
  • +36

Комментарии (21)

+5
титанический труд))
0
Надеюсь в хорошем смысле)
На мой взгляд много текста, но и механика не совсем простая.
0
В самом что ни на есть хорошем я думаю! ))
0
Однозначно в хорошем!
Такие статьи за вечер не пишутся :)
0
Точнее не переводятся. Т.к. судя по скринам это перевод туториала с flashgameu.com
0
Если так, то хотелось бы спросить автора: «Где ссылка на оригинал?»
0
Перед тем как я начну, выдам пару соглашений. Урок взят мной из книги Gary Rosenzweig — «ActionScript 3.0 Game Programming University».
0
.....<конец>

Оригинал: <ссылка>
Перевод: Dulea
0
Я считаю, выкладывать ссылку (на торрент или файлообменник) на книгу, которая, как я знаю, есть только в платном варианте это крайняя степень неуважения к автору самой книги. Как если бы кто-то сделал обзор на вашу игру, а ссылку на нее поставил не к вам на портал, а к себе (Не совсем удачный пример, конечно). Я не сноб, и никого не принуждаю покупать книги. Если честно, то не так уж и много их купил в своей жизни. Но не потому что жадный, а потому что не было возможности.
Коллеги, я же дал название книги и автора. А внизу статьи привел ссылку на исходник. На этом же сайте flashgameu можно купить книгу. Чего вам еще надо?! (jokingly)
+1
Извиняюсь, не заметил.
Привык пропускать сторку, начинающуюся на: «Исходники...», сам пишу. =)
0
Полезно =)
Спасибо!
0
Если хотя бы одному человеку это поможет осуществить свою мечту по созданию игры, значит не зря старался. Спасибо.
0
Обалдеть! Спасибо!!!
0
Я свой движек дважды переписывал. Сейчас меня он полностью устраивает.
На самом деле, самое сложное это поиск подсказки. Здесь он реализован неплохо. Я же считал массив сумм фишек в соседних ячейках. Такой метод тоже работает :)
За статью спасибо!
0
Пару мест очень гавено реализованы, а вообще эта статья в свое время сильно помогла.
0
Да уж, работа проделана действительно потрясающая. Спасибо автору, очень грамотно и доступно.
0
Добавил небольшой update.
+1
Нужно еще переделать прослушку энтерфрейма, на событие. А то даже в стоячем состоянии ест память.
0
Спасибо за статью!
0
Камрад, ты сделал большую, отличную и полезную работу!
Спасибо за пост!
  • SeeD
  • SeeD
0
Супер, респект!
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.