Пишем Сапёр на Unity. Взаимодействие

Мы продолжаем нашу серию уроков по написанию Сапёра на Unity. В этой части мы реализуем взаимодействие между клетками, сделаем так, чтобы они подсвечивались при наведении мыши, а также добавим возможность отмечать клетки с минами, поставив на них флажки.

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

  1. Пишем Сапёр на Unity. Настройка.
  2. Пишем Сапёр на Unity. Взаимодействие.
  3. Пишем Сапёр на Unity. Обработка конца игры.

Выделение клетки при наведении мыши

Пусть выделение клетки не является обязательным пунктом для нашей игры, но эта возможность будет очень полезной. Чтобы реализовать ее, мы воспользуемся функцией OnMouseOver() — она вызывается автоматически, когда мышь находится на том объекте, где и определен метод. Давайте добавим несколько переменных в скрипт объекта Tile:

public var materialIdle: Material;
public var materialLightup: Material;

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

В то время как основной материал может использовать диффузный (diffuse) шейдер,…

ms_02_09

… освещенный должен использовать зеркальный (specular).

ms_02_10

Во многих играх используется шейдер обводки (rim), но его пока еще нет в Unity.

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

ms_02_11

И не забудем добавить непосредственно в скрипт Tile следующий код:

function OnMouseOver()
{
    renderer.material = materialLightup;
}

А теперь нажмите кнопку Play и убедитесь в том, что клетка подсвечивается по наведению мыши.

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

function OnMouseExit()
{
    renderer.material = materialIdle;
}

И вуаля! Теперь игра выглядит чуть интересней!

Назначение клеткам идентификаторов

Чтобы клетки могли взаимодействовать друг с другом (например, при вычислении количества мин вокруг), назначим каждой из них свой идентификатор. Добавим переменную ID в скрипт Tile. Также нам нужно знать количество клеток в одной строке, а потому добавим еще и переменную tilesPerRow:

public var ID: int;
public var tilesPerRow: int;

Вернемся к моменту создания клетки и немного изменим код:

var newTile = Instantiate(tilePrefab, Vector3(transform.position.x + xOffset, transform.position.y, transform.position.z + zOffset), transform.rotation);
newTile.ID = tilesCreated;
newTile.tilesPerRow = tilesPerRow;

Идентификатором служит число от 0 до количества всех клеток. То есть первая клетка получит идентификатор 0, вторая — 1 и т. д. Вы можете проверить это, кликнув по нужному объекту и взглянув на параметр ID в компоненте сценария:

ms_02_12

Получение соседних клеток

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

public var tileUpper: Tile;
public var tileLower: Tile;
public var tileLeft: Tile;
public var tileRight: Tile;
 
public var tileUpperRight: Tile;
public var tileUpperLeft: Tile;
public var tileLowerRight: Tile;
public var tileLowerLeft: Tile;

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

tileUpper = Grid.tilesAll[ID + tilesPerRow];
tileLower = Grid.tilesAll[ID - tilesPerRow];
tileLeft  = Grid.tilesAll[ID - 1];
tileRight = Grid.tilesAll[ID + 1];
     
tileUpperRight = Grid.tilesAll[ID + tilesPerRow + 1];
tileUpperLeft  = Grid.tilesAll[ID + tilesPerRow - 1];
tileLowerRight = Grid.tilesAll[ID - tilesPerRow + 1];
tileLowerLeft  = Grid.tilesAll[ID - tilesPerRow - 1];

Если мы будем вычислять соседей клетки с идентификатором, равным 3, а в строке находятся по 5 клеток, то получим, что верхняя имеет ID, равный 8 (номер нашей клетки плюс количество клеток в ряду), правый — 4 (номер нашей клетки плюс 1) и т. д.

Но, к сожалению, такой код не совсем корректный, ведь может получиться так, что индекс выйдет за пределы диапазона. Чтобы избежать ошибок, напишем функцию inBounds(), которая и будет проверять, находится ли данный индекс в пределах допустимых значений:

private function inBounds(inputArray: Array, targetID: int): boolean
{
    if(targetID < 0 || targetID >= inputArray.length)
        return false;
    else
        return true;
}

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

if(inBounds(Grid.tilesAll, ID + tilesPerRow))                     tileUpper = Grid.tilesAll[ID + tilesPerRow];
if(inBounds(Grid.tilesAll, ID - tilesPerRow))                     tileLower = Grid.tilesAll[ID - tilesPerRow];
if(inBounds(Grid.tilesAll, ID - 1) &&     ID % tilesPerRow != 0)  tileLeft  = Grid.tilesAll[ID - 1];
if(inBounds(Grid.tilesAll, ID + 1) && (ID+1) % tilesPerRow != 0)  tileRight = Grid.tilesAll[ID + 1];
     
if(inBounds(Grid.tilesAll, ID + tilesPerRow + 1) && (ID+1) % tilesPerRow != 0) tileUpperRight = Grid.tilesAll[ID + tilesPerRow + 1];
if(inBounds(Grid.tilesAll, ID + tilesPerRow - 1) &&     ID % tilesPerRow != 0) tileUpperLeft  = Grid.tilesAll[ID + tilesPerRow - 1];
if(inBounds(Grid.tilesAll, ID - tilesPerRow + 1) && (ID+1) % tilesPerRow != 0) tileLowerRight = Grid.tilesAll[ID - tilesPerRow + 1];
if(inBounds(Grid.tilesAll, ID - tilesPerRow - 1) &&     ID % tilesPerRow != 0) tileLowerLeft  = Grid.tilesAll[ID - tilesPerRow - 1];

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

Вот пример поиска соседей у клетки под номером 7 при условии, что поле содержит 4 клетки в строке:

ms_02_16

Обратите внимание, что у такой клетки (которая находится на правом краю поля), нет соседей справа вообще.

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

public var adjacentTiles: Array = new Array();

А затем добавим всех существующих соседей в этот массив:

if(tileUpper)      adjacentTiles.Push(tileUpper);
if(tileLower)      adjacentTiles.Push(tileLower);
if(tileLeft)       adjacentTiles.Push(tileLeft);
if(tileRight)      adjacentTiles.Push(tileRight);
if(tileUpperRight) adjacentTiles.Push(tileUpperRight);
if(tileUpperLeft)  adjacentTiles.Push(tileUpperLeft);
if(tileLowerRight) adjacentTiles.Push(tileLowerRight);
if(tileLowerLeft)  adjacentTiles.Push(tileLowerLeft);

Подсчет мин

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

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

public var adjacentMines: int = 0;

Чтобы посчитать их, мы пробежимся по ранее созданному массиву и для каждой клетки проверим значение переменной isMine. Как вы помните, именно она и отвечает за то, скрывается ли под клеткой мина или нет. Если значение равно true, то мы увеличиваем счетчик:

function CountMines()
{
    adjacentMines = 0;
     
    for each(var currentTile: Tile in adjacentTiles)
        if(currentTile.isMined)
            adjacentMines += 1;
     
    displayText.text = adjacentMines.ToString();
     
    if(adjacentMines <= 0)
        displayText.text = "";
}

Если мин нет, то строка остается пустой.

Состояния клеток

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

public var state: String = "idle";

Добавление флагов

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

В исходных файлах вы найдете необходимый ресурс. Поставьте сам флажок на клетку:

ms_02_17

Для получения доступа к флажку нам потребуется новая переменная:

public var displayFlag: GameObject;

Добавьте флажок в слот Display Flag, а в методе start() пропишите:

displayFlag.renderer.enabled = false;
displayText.renderer.enabled = false;

Этот код отключит сам флажок и текст в начале игры.

Добавление и удаление флага

Напишем новую функцию, которая будет обрабатывать и размещение, и удаление флажка:

function SetFlag()
{
    if(state == "idle")
    {
        state = "flagged";
        displayFlag.renderer.enabled = true;
    }
    else if(state == "flagged")
    {
        state = "idle";
        displayFlag.renderer.enabled = false;
    }
}

Не забудем изменить функцию OnMouseOver():

function OnMouseOver()
{
    if(state == "idle")
    {
        renderer.material = materialLightup;
     
        if (Input.GetMouseButtonDown(1))
            SetFlag();
    }
    else if(state == "flagged")
    {
        renderer.material = materialLightup;
     
        if (Input.GetMouseButtonDown(1))
            SetFlag();
    }
}

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

При нажатии на кнопку мыши под номером 1 (это правая кнопка) будет вызываться метод SetFlag(), который и поставит/удалит флаг в зависимости от текущего состояния клетки. Вы можете протестировать работу в этой демо-игре.

Вывод

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

В следующий раз мы реализуем «раскрытие» клетки, добавим простой интерфейс и доведем игру до ума. Оставайтесь с нами!

Перевод статьи «Build a Grid-Based Puzzle Game Like Minesweeper in Unity: Interaction»