ActionScript 3: Создание игры «три-в-ряд» Bejeweled (третья часть)

Выбираем, какой драгоценный камень нужно удалить

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

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

Разработка: Чтобы было проще, создадим функцию, которая будет сканировать массив jewels почти так же, как это делают функции rowStreaks и colStreaks. Основное отличие — будем сохранять имена драгоценных камней, которые будут удалены в массиве, для дальнейшего использования.

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

private function removeGems(row:uint,col:uint):void {
var gemsToRemove:Array=[row+"_"+col];
var current:uint=jewels[row][col];
var tmp:int;
if (rowStreak(row,col)>2) {
tmp=col;
while (checkGem(current,row,tmp-1)) {
tmp--;
gemsToRemove.push(row+"_"+tmp);
}
tmp=col;
while (checkGem(current,row,tmp+1)) {
tmp++;
gemsToRemove.push(row+"_"+tmp);
}
}
if (colStreak(row,col)>2) {
tmp=row;
while (checkGem(current,tmp-1,col)) {
tmp--;
gemsToRemove.push(tmp+"_"+col);
}
tmp=row;
while (checkGem(current,tmp+1,col)) {
tmp++;
gemsToRemove.push(tmp+"_"+col);
}
}
trace("Will remove "+gemsToRemove);
}

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

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

Измените функцию onClick следующим образом:

private function onClick(e:MouseEvent):void {
if (mouseX<480&&mouseX>0&&mouseY<480&&mouseY>0) {
var selRow:uint=Math.floor(mouseY/60);
var selCol:uint=Math.floor(mouseX/60);
if (! isAdjacent(selRow,selCol,pickedRow,pickedCol)) {
pickedRow=selRow;
pickedCol=selCol;
selector.x=60*pickedCol;
selector.y=60*pickedRow;
selector.visible=true;
} else {
swapJewelsArray(pickedRow,pickedCol,selRow,selCol);
if (isStreak(pickedRow,pickedCol)||isStreak(selRow,selCol)) {
swapJewelsObject(pickedRow,pickedCol,selRow,selCol);
if (isStreak(pickedRow,pickedCol)) {
removeGems(pickedRow,pickedCol);
}
if (isStreak(selRow,selCol)) {
removeGems(selRow,selCol);
}
} else {
swapJewelsArray(pickedRow,pickedCol,selRow,selCol);
}
pickedRow=-10;
pickedCol=-10;
selector.visible=false;
}
}
}

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

Протестируйте приложение и поменяйте местами два драгоценных камня.

Проверка на удаление линии-комбинации

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

Will remove 0_2, 0_1, 0_0

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

Удаление драгоценных камней

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

Идея: Сканируем массив gemsToRemove и удаляем все драгоценные камни.

Разработка: Удаление драгоценного камня выполняется двумя шагами: удаление драгоценного камня типа DisplayObject из списка отображения (DisplayList) и обновление массива jewels. Это приводит к вопросу: если массив jewels содержит элемент от нуля до шести, чтобы представлять различные типы драгоценных камней, то, как закодировать «пустой» статус? Будем использовать -1, чтобы указать, что нет никакого драгоценного камня в массиве jewels.

Первое, что нужно сделать — это перебрать массив gemsToRemove, и будем делать это, используя метод forEach. Разумеется, метод forEach работает также и с массивами. Добавьте эту строчку в функцию removeGems:

private function removeGems(row:uint,col:uint):void {
...
gemsToRemove.forEach(removeTheGem);
}

Теперь для каждого элемента в массиве gemsToRemove будет выполняться функция removeTheGem. Здесь мы должны удалить его объект типа DisplayObject и устанавить соответствующий элемент на значение -1.

Запишите функцию removeTheGem следующим образом:

private function removeTheGem(element:String,index:int,arr:Array):void
{
with (gemsContainer) {
removeChild(getChildByName(element));
}
var coordinates:Array=element.split("_");
jewels[coordinates[0]][coordinates[1]]=-1;
}

Вы заметили как много аргументов? Это из-за структуры метода forEach.

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

Это означает, что нужно найти способ управлять строкой типа 3_6 так, как мы знаем, что работаем над рядом 3 и столбцом 6.

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

Следуя предыдущему примеру, разбивание строки 3_6 с использованием знака нижнего подчеркивания (_) в качестве аргумента будет создавать массив из двух элементов, содержащих 3 и 6.

При помощи такой линии подчеркивания:

var coordinates:Array=element.split("_");

У нас есть значения ряда и столбца драгоценного камня соответственно в coordinates[0] и coordinates[1]. Наконец, мы можем установить соответствующий элемент массива jewels на значение -1 так:

jewels[coordinates[0]][coordinates[1]]=-1;

Проверьте приложение и поменяйте местами два драгоценных камня. Линия-комбинация исчезнет.

Удаление линии-комбинации

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

Делаем так, чтобы драгоценные камни падали

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

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

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

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

Итак, весь код будет вызываться в конце функции removeGems:

private function removeGems(row:uint,col:uint):void {
...
adjustGems();
}

Функция adjustGems будет обрабатывать падение драгоценных камней. Вот как она будет работать:

  • Сканировать массив jewels от столбца до столбца, начиная с самого нижнего ряда.
  • Если найден пустой элемент (-1), то искать выше первый непустой элемент в том же столбце.
  • Если такой непустой элемент найден, то поменять его местами с пустым элементом, который был найден ранее, и отрегулировать позицию этого объекта типа DisplayObject и его имя.

Кажется сложнее, чем есть на самом деле. Давайте проверим функцию adjustGems:

private function adjustGems():void {
for (var j:uint=0; j<8; j++) {
for (var i:uint=7; i>0; i--) {
if (jewels[i][j]==-1) {
for (var k:uint=i; k>0; k--) {
if (jewels[k][j]!=-1) {
break;
}
}
if (jewels[k][j]!=-1) {
trace("moving gem at row "+k+" to row "+i);
jewels[i][j]=jewels[k][j];
jewels[k][j]=-1;
with(gemsContainer.getChildByName(k+"_"+j)){
y=60*i;
name=i+"_"+j;
}
}
}
}
}
}

Сначала обратите внимание, как сканируется массив jewels на поиск пустых элементов.

for (var j:uint=0; j<8; j++) {
for (var i:uint=7; i>0; i--) {
if (jewels[i][j]==-1) { ... }
}
}

Перебираем от столбца к столбцу, начиная с самого верхнего индекса ряда (7) назад к самому нижнему (0). Как только пустой элемент найден, уже есть уверенность в том, что нет пустых элементов в этом столбце с более высоким индексом ряда. То есть, это самый нижний пустой элемент в столбце.

for (var k:uint=i; k>0; k--) {
if (jewels[k][j]!=-1) {
break;
}
}

В это же время ищем первый элемент в том же столбце, с самым наименьшим индексом ряда, который не является пустым (отличен от значения -1). Оператор break гарантирует, что значение k в конце цикла for содержит индекс ряда первого непустого элемента, если такой есть. Это можно было бы проделать и при помощи цикла while, но мне больше нравится идея с тремя циклами for.

if (jewels[k][j]!=-1) { ... }

Теперь проверяем, действительно ли найден непустой элемент, или просто завершился цикл for, так как значение k достигло нуля. Если найден непустой элемент, то меняем местами значения в массиве jewels и корректируем позицию драгоценного камня, чтобы заполнить пустое пространство, которые было найдено.

Давайте разберем это на последовательности изображений:

Проверка игрового поля для заполнения пространства верхними драгоценными камнями

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

Проверьте приложение, создайте несколько линий-комбинаций, горизонтальных или вертикальных.

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

Драгоценные камни перемещаются вниз сразу после удаления линии-комбинации

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

Добавление новых драгоценных камней

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

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

Разработка: Я говорил вам, что это легко, и в этот раз нам просто нужно несколько циклов for. Очевидно, что новые драгоценные камни будут создаваться в самом конце процесса удаления, поэтому мы будем вызывать функцию для замены драгоценных камней в самом конце функции removeGems.

private function removeGems(row:uint,col:uint):void {
...
replaceGems();
}

И функция replaceGems очень простая, так как просто ищет пустые пространства, куда можно добавить новые драгоценные камни.

private function replaceGems():void {
for (var i:int=7; i>=0; i--) {
for (var j:uint=0; j<8; j++) {
if (jewels[i][j]==-1) {
jewels[i][j]=Math.floor(Math.random()*7);
gem=new gem_mc(jewels[i][j],i,j);
gemsContainer.addChild(gem);
}
}
}
}

Хотелось бы обратить ваше внимание на две вещи:

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

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

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

Заполнение пространства новыми драгоценными камнями сверху

Поздравления! Вы только в одном шаге от выполнения прототипа игры.

Создание супер-комбинаций

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

Идея: После удаления или добавления любого драгоценного камня, ищем линии-комбинации.

Разработка: Драгоценные камни добавляются функцией replaceGems, и удаляем их функцией adjustGems.

Мы просто должны включить выражение if в обе функции, чтобы узнать является ли последний добавленный/скорректированный драгоценный камень частью линии-комбинации.

Измените функцию replaceGems следующим образом:

private function replaceGems():void {
for (var i:int=7; i>=0; i--) {
for (var j:uint=0; j<8; j++) {
if (jewels[i][j]==-1) {
jewels[i][j]=Math.floor(Math.random()*7);
gem=new gem_mc(jewels[i][j],i,j);
gemsContainer.addChild(gem);
if (isStreak(i,j)) {
trace("COMBO");
removeGems(i,j);
}
}
}
}
}

и точно таким же образом, вот так должна выглядеть функция adjustGems:

private function adjustGems():void {
for (var j:uint=0; j<8; j++) {
for (var i:uint=7; i>0; i--) {
if (jewels[i][j]==-1) {
for (var k:uint=i; k>0; k--) {
if (jewels[k][j]!=-1) {
break;
}
}
if (jewels[k][j]!=-1) {
jewels[i][j]=jewels[k][j];
jewels[k][j]=-1;
with(gemsContainer.getChildByName(k+"_"+j)){
y=60*i;
name=i+"_"+j;
}
if (isStreak(i,j)) {
trace("COMBO");
removeGems(i,j);
}
}
}
}
}
}

Протестируйте игру, если повезет, то иногда вы должны будете увидеть текст:

COMBO

появляющийся в окне Output. Теперь, что-то для ленивых игроков.

Получение подсказок

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

Идея: У нас достаточно пространства справа игрового поля, поэтому игра будет давать подсказку, когда вы нажимаете справа игрового поля. Как можно выдавать подсказку?

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

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

Измените функцию onClick следующим образом:

private function onClick(e:MouseEvent):void {
if (mouseX<480&&mouseX>0&&mouseY<480&&mouseY>0) {
...
} else {
for (var i:uint=0; i<8; i++) {
for (var j:uint=0; j<8; j++) {
if (i<7) {
swapJewelsArray(i,j,i+1,j);
if (isStreak(i,j)||isStreak(i+1,j)) {
trace(i+","+j+" -> "+(i+1)+","+j);
}
swapJewelsArray(i,j,i+1,j);
}
if (j<7) {
swapJewelsArray(i,j,i,j+1);
if (isStreak(i,j)||isStreak(i,j+1)) {
trace(i+","+j+" -> "+i+","+(j+1));
}
swapJewelsArray(i,j,i,j+1);
}
}
}
}
}

Весь код очень интуитивно понятный, в любом случае мы увидим, как это работает:

for (var i:uint=0; i<8; i++) {
for (var j:uint=0; j<8; j++) {
…
}
}

Это классическая пара циклов for для перебора массива.

if (i<7) { ... }

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

swapJewelsArray(i,j,i+1,j);

Свопинг драгоценных камней

if (isStreak(i,j)||isStreak(i+1,j)) { ... }

Проверяем, если хотя бы один из поменянных местами драгоценных камней формирует линию-комбинацию

trace(i+","+j+" -> "+(i+1)+","+j);

пишем подсказку в окне Output

swapJewelsArray(i,j,i+1,j);

Меняем местами назад драгоценные камни.

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

Протестируйте приложение и в такой ситуации, как показано ниже:

Проверка доступных ходов для подсказок игроку

вы получите такие подсказки:

2,6 -> 3,6
4,5 -> 4,6
5,1 -> 6,1
5,2 -> 6,2
6,2 -> 6,3
6,5 -> 7,5

Это все и единственные возможные ходы. Наслаждайтесь игрой «Bejeweled».

Подведение итогов

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

Куда двигаться дальше

В этой третьей части урока по созданию игры «три-в-ряд» Bejeweled на ActionScript 3 из книги «Flash game development by example» Emanuele Feronato мы рассмотрели главные принципы игры. Понятно, что наиболее интересная недостающая особенность это плавное движение. Чтобы его добавить, вам нужно будет использовать слушатель событий ENTER_FRAME, потому что это самый простой способ управлять движением и исчезновением драгоценных камней. Возможно вы попробуете реализовать это сами при помощи tween-анимации. Используя концепции, изученные в этом уроке и коде, вы сможете создать свою идеальную игру три в ряд Bejeweled.

Автор перевода: Jean Winters

Комментировать

Почта не публикуется.Обязательные поля отмечены *