Пишем сапёр на Unity. Настройка

Логические игры часто включают в себя клеточные поля, при этом клетки имеют определенные свойства и модели поведения. В этой серии уроков мы покажем вам, как создать простую версию классической игры Сапёр. Прилагаем список статей:

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

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

  1. Настройка проекта.
  2. Механика мяча и платформы.
  3. Поведение блоков, префабы и дизайн уровней.
  4. Добавление звуков и новых уровней.

Правила игры

Цель игры — найти все мины и не взорваться. Мины спрятаны в клетках, а сами поля бывают разных размеров от 9×9 (легкий уровень) до 16×30 (сложный уровень). Впрочем, никто не запрещает использовать любой понравившийся размер поля.

При нажатии на клетку вы «раскрываете» её. Если там мина — вы проиграли. Если же мины есть в рядом стоящих клетках, то на месте клетки, на которую вы нажали, появляется число, означающее количество мин вокруг нее. А если вокруг безопасно, то все близлежащие «безопасные» клетки раскроются.

Если вы уверены, что в какой-то клетке находится мина, смело жмите на нее правой кнопкой — клетка отметится флажком и вы не сможете «раскрыть» клетку, пока не снимите флажок. Это делается для того, чтобы вы случайно не открыли клетку, в которой точно находится мина.

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

Базовая клетка

Создайте новый проект Unity, добавьте на сцену кубик (Cube) и назовите его Tile. Перетащите его в папку с проектом для того, чтобы превратить его в префаб. Кубик пока ничего не умеет, но мы воспользуемся им, чтобы построить игровое поле, а затем добавим ему новых возможностей.

Генератор клеток

Создайте новый пустой объект и назовите его Grid. Так же как и кубик, сделайте его префабом (перетащив в папку с проектом). Этот объект — наш будущий генератор клеток, который и будет создавать игровое поле.

Создайте новый сценарий (в качестве языка программирования будем на этот раз использовать JavaScript) и также назовите его Grid. Прикрепите его к нашему генератору и пропишите в скрипте:

public var tilePrefab: GameObject;
public var numberOfTiles: int = 10;
public var distanceBetweenTiles: float = 1.0;
 
function Start()
{
    CreateTiles();
}
 
function CreateTiles()
{
 
}

Сохраните скрипт и перетащите наш кубик Tile в поле Tile Prefab компонента Script объекта Grid. У вас должно получится вот так:

ms_04

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

В настоящий момент генератор клеток ничего не делает. Давайте добавим несколько строчек кода в метод CreateTiles:

var xOffset: float = 0.0;
 
for(var tilesCreated: int = 0; tilesCreated < numberOfTiles; tilesCreated += 1)
{
    xOffset += distanceBetweenTiles;
    Instantiate(tilePrefab, Vector3(transform.position.x + xOffset, transform.position.y, transform.position.z), transform.rotation);
}

Если вы нажмете на кнопку Play, то увидите нечто подобное:

ms_01

Функция CreateTiles создает копии префаба кубика (столько раз, сколько мы задали) и помещает их в линию, где расстояние между кубиками равно distanceBetweenTiles. Попробуйте подобрать оптимальное расстояние, чтобы будущее поле выглядело красиво.

Но для Сапёра нам нужно поле в виде сетки, а не линии. Чтобы достичь этого, добавьте в сценарий объекта Grid новую переменную, которая будет отвечать за количество кубиков в строке:

public var tilesPerRow: int = 4;

И перепишем метод CreateTiles:

function CreateTiles()
{
    var xOffset: float = 0.0;
    var zOffset: float = 0.0;
 
    for(var tilesCreated: int = 0; tilesCreated < numberOfTiles; tilesCreated += 1)
    {
        xOffset += distanceBetweenTiles;
         
        if(tilesCreated % tilesPerRow == 0)
        {
            zOffset += distanceBetweenTiles;
            xOffset = 0;
        }
     
        Instantiate(tilePrefab, Vector3(transform.position.x + xOffset, transform.position.y, transform.position.z + zOffset), transform.rotation);
    }
}

Запустив игру, вы увидите такую картину:

ms_02

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

Добавление мин

Теперь, когда мы создали основу, давайте поработаем с минами. Создайте новый сценарий, назовите его Tile и прикрепите его к префабу Tile. Добавьте строчку с объявлением переменной:

public var isMined: boolean = false;

Этот параметр нам и скажет, есть ли в клетке мина. Далее нам нужно позволить генератору создавать новый объект, который мы только что создали. Для этого измените тип переменной tilePrefab с GameObject на Tile в скрипте объекта Grid:

public var tilePrefab: Tile;

А теперь добавим новые переменные:

static var tilesAll: Tile[];
static var tilesMined: Array;
static var tilesUnmined: Array;

И не забудем про инициализацию:

tilesAll = new Tile[numberOfTiles];
tilesMined = new Array();
tilesUnmined = new Array();

И немного изменим команду Instantiate:

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

А в конце сценария выполним метод AssignMines. Вот так будет выглядеть измененный метод CreateTiles:

function CreateTiles()
{
    tilesAll = new Tile[numberOfTiles];
    tilesMined = new Array();
    tilesUnmined = new Array();
     
    var xOffset: float = 0.0;
    var zOffset: float = 0.0;
     
    for(var tilesCreated: int = 0; tilesCreated < numberOfTiles; tilesCreated += 1)
    {
        xOffset += distanceBetweenTiles;
     
        if(tilesCreated % tilesPerRow == 0)
        {
            zOffset += distanceBetweenTiles;
            xOffset = 0;
        }
     
        var newTile = Instantiate(tilePrefab, Vector3(transform.position.x + xOffset, transform.position.y, transform.position.z + zOffset), transform.rotation);
        tilesAll[tilesCreated] = newTile;
    }
     
    AssignMines();
}

Метод AssignMines случайным образом задаст некоторым клеткам мины:

function AssignMines()
{
    tilesUnmined = tilesAll;
 
    for(var minesAssigned: int = 0; minesAssigned < numberOfMines; minesAssigned += 1)
    {
        var currentTile: Tile = tilesUnmined[Random.Range(0, tilesUnmined.length)];
         
        tilesMined.Push(currentTile);
        tilesUnmined.Remove(currentTile);
         
        currentTile.GetComponent(Tile).isMined = true;
    }
}

Как работает функция AssignMines? Дело в том, что все созданные клетки в методе CreateTiles помещаются в массив tilesAll. И уже в методе AssignMines они копируются в массив tilesUnmined. Далее случайным образом отбирается numberOfMines плиток. Параметр isMined отобранных плиток устанавливается в true, а они сами удаляются из массива tilesUnmined и помещаются в tilesMined.

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

Дизайн плиток

Сейчас наши клетки выглядят как кубики (по сути это и есть кубики). Давайте изменим их внешний вид.

В исходных файлах вы найдете 3D модель puzzleObjects.fbx. Скопируйте файл в папку с проектом для дальнейшего использования (убедитесь в том, что он импортируется с размером, установленным в 1):

ms_02_01

Перейдите в настройки префаба Tile и поменяйте значение поля Mesh Filter на tileImproved.

ms_02_02

И здесь же сбросьте значения компонента Box Collider, нажав на него правой кнопкой мыши и выбрав Reset.

ms_02_03

И наконец, присвойте объекту новый материал, чтобы он не имел стандартный белый цвет.

ms_02_04

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

Добавление чисел

Для того, чтобы показывать количество мин вокруг открывшейся клетки, воспользуемся 3D текстом. Создайте, выбрав GameObject -> Create Other -> 3D Text, и добавьте его к объекту Tile. Вот, как это должно выглядеть:

ms_02_05

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

ms_02_06

ms_02_07

Теперь мы должны получить доступ к тексту из кода. Добавьте следующую строчку в скрипт объекта Tile:

public var displayText: TextMesh;

Перетащите объект текста в новое поле компонента Script объекта Tile:

ms_02_14

Вывод

И на этом наша статья подходит к концу. Мы создали функциональную основу для игры в жанре головоломок. Эта основа может быть использована вами не только для Сапёра.

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

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