[Перевод] Создание 3D-шахмат в Unity

image


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

В этом туториале мы создадим 3D-игру в шахматы на Unity. В процессе вы узнаете. как реализовать следующее:

  • Как выбирать перемещаемую фигуру
  • Как определять разрешённые ходы
  • Как менять игроков
  • Как распознавать состояние победы


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

Примечание: вам необходимо знать Unity и язык C#. Если вы хотите повысить свой навык в C#, то можно начать с серии видеокурсов Beginning C# with Unity Screencast.

Приступаем к работе


Скачайте материалы проекта для этого туториала. Чтобы начать работу, откройте заготовку проекта в Unity.

Шахматы часто реализуются в виде простой 2D-игры. Однако в нашей версии в 3D мы будем имитировать игрока, сидящего за столом и играющего со своим другом. Кроме того, 3D — это круто.

Откройте сцену Main из папки Scenes. Вы увидите объект Board, представляющий собой игровую доску, и объект для GameManager. К этим объектам уже прикреплены скрипты.

  • Prefabs: здесь содержатся доска, отдельные фигуры и квадраты-индикаторы для выделения клеток, которые мы будем использовать в процессе выбора хода.
  • Materials: здесь находятся материалы для шахматной доски, фигур и клеток.
  • Scripts: содержит компоненты, которые уже прикреплены к объектам в иерархии.
  • Board: контролирует визуальное отображение фигур. Этот компонент также отвечает за выделение отдельных фигур.
  • Geometry.cs: вспомогательный класс, управляющий преобразованиями между записью рядов/столбцов и точек Vector3.
  • Player.cs: контролирует фигуры игрока, а также взятые игроком фигуры. Кроме того, содержит направление движения фигур, для которых важно направление, например, для пешек.
  • Piece.cs: базовый класс, определяющий перечисления для всех экземпляров фигур. Также содержит логику определения допустимых ходов в игре.
  • GameManager.cs: хранит игровую логику, такую как допустимые ходы, исходное расположение фигур в начале игры и другое. Это синглтон, поэтому другим классам его удобно вызывать.


GameManager содержит 2D-массив pieces, хранящий положения фигур на доске. Изучите AddPiece, PieceAtGrid и GridForPiece, чтобы понять, как он работает.

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

7318b7f44668f2185b87718144046fb5.png


Перемещение фигур


В первую очередь нам нужно определить, какую фигуру двигать.

Определить, на какую клетку игрок навёл мышь, можно с помощью отслеживания лучей/рейкастинга (Raycasting). Если вы не знаете, как работает отслеживание лучей в Unity, то прочитайте наш туториал Введение в скриптинг Unity или популярный туториал об игре Bomberman.

После выбора игроком фигураы мы должны сгенерировать допустимые клетки, на которые может переместиться фигура. Затем нужно выбрать одну из них. Для обработки этого функционала мы добавим два новых скрипта. TileSelector поможет выбрать перемещаемую фигуру, а MoveSelector позволит подобрать место для перемещения.

Оба компонента имеют одинаковые базовые методы:

  • Start: для первоначальной настройки.
  • EnterState: выполняет настройку для текущей активации фигуры.
  • Update: выполняет отслеживание лучей при перемещении мыши.
  • ExitState: сбрасывает текущее состояние и вызывает EnterState следующего состояния.


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

Выбор клетки


Выберите в иерархии Board. Затем нажмите на кнопку Add Component в окне Inspector. Введите в поле TileSelector и нажмите New Script. Наконец, нажмите Create and Add, чтобы прикрепить скрипт.

Примечание: при создании новых скриптов уделите время на то, чтобы переместить их в соответствующую папку, чтобы поддерживать порядок в папке Assets.


Выделение выбранной клетки


Дважды нажмите на TileSelector.cs, чтобы открыть его, и добавьте внутри определения класса следующие переменные:

public GameObject tileHighlightPrefab;

private GameObject tileHighlight;


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

Далее добавим в Start следующие строки:

Vector2Int gridPoint = Geometry.GridPoint(0, 0);
Vector3 point = Geometry.PointFromGrid(gridPoint);
tileHighlight = Instantiate(tileHighlightPrefab, point, Quaternion.identity, gameObject.transform);
tileHighlight.SetActive(false);


Start получает исходные строку и столбец для выделенной клетки, превращает их в точку и создаёт из префаба игровой объект. Этот объект изначально деактивирован, поэтому не будет виден, пока не понадобится.

Примечание: полезно ссылаться на координаты по строке и столбцу, которые принимают вид Vector2Int и на которые мы ссылаемся как на GridPoint. Vector2Int имеет два целочисленных значения: x и y. Когда нам нужно расположить в сцене объект, нам нужна точка Vector3. Vector3 имеет три значения с плавающей запятой: x, y и z.

Geometry.cs — это вспомогательные методы для следующих преобразований:

  • GridPoint(int col, int row): даёт нам GridPoint для заданного столбца и строки.
  • PointFromGrid(Vector2Int gridPoint): преобразует GridPoint в настоящую точку сцены Vector3.
  • GridFromPoint(Vector3 point): даёт нам GridPoint для значения x и z этой 3D-точки, а значение y игнорируется.


Далее мы добавим EnterState:

public void EnterState()
{
    enabled = true;
}


Это позволяет снова включать компонент, когда настаёт время для выбора другой фигуры.

Далее добавим в Update следующее:

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
    Vector3 point = hit.point;
    Vector2Int gridPoint = Geometry.GridFromPoint(point);

    tileHighlight.SetActive(true);
    tileHighlight.transform.position =
        Geometry.PointFromGrid(gridPoint);
}
else
{
    tileHighlight.SetActive(false);
}


Здесь мы создаём луч Ray из камеры, проходящий через указатель мыши и дальше, в бесконечность.

Physics.Raycast проверяет, пересекается ли этот луч с какими-нибудь физическими коллайдерами системы. Так как доска является единственным объектом с коллайдером, нам не нужно волноваться, что фигуры будут перекрывать друг друга.

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

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

Наконец, выберите в иерархии Board и нажмите в окне Project на Prefabs. Затем перетащите префаб Selection-Yellow в слот Tile Highlight Prefab компонента Tile Selector доски.

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

0dca4f70b8d7cf4afcff932b866d7364.gif


Выбор фигуры


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

if (Input.GetMouseButtonDown(0))
{
    GameObject selectedPiece = 
        GameManager.instance.PieceAtGrid(gridPoint);
    if(GameManager.instance.DoesPieceBelongToCurrentPlayer(selectedPiece))
    {
        GameManager.instance.SelectPiece(selectedPiece);
    // Опорная точка 1: сюда мы позже добавим вызов ExitState
    }
}


Если нажата клавиша мыши, то GameManager передаёт нам фигуру в текущей позиции. Нам нужно проверять, принадлежит ли эта фигура текущему игроку, потому что игроки не должны иметь возможность перемещать фигуры противника.

Примечание: в подобных сложных играх полезно бывает чётко определить границы ответственности компонентов. Board имеет дело только с отображением и выделением фигур. GameManager отслеживает значения GridPoint позиций фигур. Также он содержит вспомогательные методы, отвечающие на вопросы о том, где находятся фигуры и какому игроку они принадлежат.


Запустите режим Play и выберите фигуру.

d8dd84bd792e88bc3bd88e84c8214772.png


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

Выбор точки перемещения


На этом этапе TileSelector выполнил всю свою работу. Настало время для другого компонента: MoveSelector.

Этот компонент похож на TileSelector. Как и раньше, выберите в иерархии объект Board, добавьте ему новый компонент и назовите его MoveSelector.

Передача управления


Первое, чего нам нужно достичь — научиться передавать управление от TileSelector компоненту MoveSelector. Для этого можно использовать ExitState. Добавьте в TileSelector.cs следующий метод:

private void ExitState(GameObject movingPiece)
{
    this.enabled = false;
    tileHighlight.SetActive(false);
    MoveSelector move = GetComponent();
    move.EnterState(movingPiece);
}


Он скрывает оверлей клетки и отключает компонент TileSelector. В Unity нельзя вызывать метод Update отключенных компонентов. Так как мы хотим сейчас вызвать метод Update нового компонента, то благодаря отключению старого компонента он не будет нам мешать.

Вызовем этот метод, добавив в Update сразу после Опорной точки 1 следующую строку:

ExitState(selectedPiece);


Теперь откроем MoveSelector и добавим вверху класса эти переменные экземпляра:

public GameObject moveLocationPrefab;
public GameObject tileHighlightPrefab;
public GameObject attackLocationPrefab;

private GameObject tileHighlight;
private GameObject movingPiece;


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

Затем добавим в Start следующий код настройки:

this.enabled = false;
tileHighlight = Instantiate(tileHighlightPrefab, Geometry.PointFromGrid(new Vector2Int(0, 0)),
    Quaternion.identity, gameObject.transform);
tileHighlight.SetActive(false);


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

Перемещаем фигуру


Далее добавляем метод EnterState:

public void EnterState(GameObject piece)
{
    movingPiece = piece;
    this.enabled = true;
}


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

Добавим следующие строки в метод Update компонента MoveSelector:

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
    Vector3 point = hit.point;
    Vector2Int gridPoint = Geometry.GridFromPoint(point);

    tileHighlight.SetActive(true);
    tileHighlight.transform.position = Geometry.PointFromGrid(gridPoint);
    if (Input.GetMouseButtonDown(0))
    {
        // Опорная точка 2: проверка допустимой позиции хода
        if (GameManager.instance.PieceAtGrid(gridPoint) == null)
        {
            GameManager.instance.Move(movingPiece, gridPoint);
        }
        // Опорная точка 3: здесь позже будет код взятия вражеской фигуры
        ExitState();
    }
}
else
{
    tileHighlight.SetActive(false);
}


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

Наконец, добавим метод ExitState, чтобы сбросить всё и подготовиться к следующему ходу:

private void ExitState()
{
    this.enabled = false;
    tileHighlight.SetActive(false);
    GameManager.instance.DeselectPiece(movingPiece);
    movingPiece = null;
    TileSelector selector = GetComponent();
    selector.EnterState();
}


Мы отключаем этот компонент и скрываем оверлей выделения клетки. Так как фигура перемещена, мы можем очистить это значение и попросить GameManager снять выделение с фигуры. Затем мы вызываем EnterState из TileSelector, чтобы начать процесс с самого начала.

Выберите в редакторе Board и перетащите префабы оверлея клетки из папки префабов в слоты MoveSelector:

  • Move Location Prefab должен быть Selection-Blue
  • Tile Highlight Prefab должен быть Selection-Yellow.
  • Attack Location Prefab должен быть Selection-Red.


c124aa319f84abc033536e688dd91dc5.png


Цвета можно изменять при помощи настройки материалов.

Запустите режим Play и попробуйте перемещать фигуры.

cd426085563d1e0795363f9262969aed.png


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

Определяем разрешённые ходы


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

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

Нам нужно ответить ещё на один вопрос: где мы должны генерировать список ходов?

Логично было бы генерировать их в EnterState компонента MoveSelector. Здесь мы генерируем клетки оверлеев, показывающие, куда может ходить игрок, поэтому это разумнее всего.

Генерируем список допустимых клеток


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

Этот отфильтрованный список передаётся обратно в MoveSelector, который выделяет допустимые ходы и ожидает выбора игрока.

Простейшим ходом обладает пешка, так что логичнее начать с неё.

Откройте Pawn.cs в Pieces, и измените MoveLocations так, чтобы он выглядел следующим образом:

public override List MoveLocations(Vector2Int gridPoint) 
{
    var locations = new List();

    int forwardDirection = GameManager.instance.currentPlayer.forward;
    Vector2Int forward = new Vector2Int(gridPoint.x, gridPoint.y + forwardDirection);
    if (GameManager.instance.PieceAtGrid(forward) == false)
    {
        locations.Add(forward);
    }

    Vector2Int forwardRight = new Vector2Int(gridPoint.x + 1, gridPoint.y + forwardDirection);
    if (GameManager.instance.PieceAtGrid(forwardRight))
    {
        locations.Add(forwardRight);
    }

    Vector2Int forwardLeft = new Vector2Int(gridPoint.x - 1, gridPoint.y + forwardDirection);
    if (GameManager.instance.PieceAtGrid(forwardLeft))
    {
        locations.Add(forwardLeft);
    }

    return locations;
}


Здесь мы выполняем несколько действий:

Сначала этот код создаёт пустой список для записи позиций. Затем он создаёт позицию, представляющую собой клетку на один шаг вперёд.

Так как белые и чёрные пешки двигаются в разных направлениях, объект Player содержит значение, определяющее направление движения пешки. Для первого игрока это значение равно +1, для противника оно равно -1.

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

В случае клеток взятия мы тоже должны проверить, есть ли в этой позиции фигура. Если есть, то мы можем взять её.

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

В скрипте GameManager.cs добавьте этот метод сразу после метода Move:

public List MovesForPiece(GameObject pieceObject)
{
    Piece piece = pieceObject.GetComponent();
    Vector2Int gridPoint = GridForPiece(pieceObject);
    var locations = piece.MoveLocations(gridPoint);

    // Отфильтровываем позиции за пределами доски
    locations.RemoveAll(tile => tile.x < 0 || tile.x > 7
        || tile.y < 0 || tile.y > 7);

    // Отфильтровываем позиции с фигурами игрока
    locations.RemoveAll(tile => FriendlyPieceAt(tile));

    return locations;
}


Здесь мы получаем компонент Piece игровой фигуры, а также её текущую позицию.

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

RemoveAll — это полезная функция, использующая выражение callback (механизма обратного вызова). Этот метод просматривает каждое значение в списке, передавай его в выражение как tile. Если это выражение равно true, то значение удаляется из списка.

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

В верхней части класса скрипта MoveSelector.cs добавьте следующие переменные экземпляра:

private List moveLocations;
private List locationHighlights;


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

Добавьте в конец метода EnterState следующие строки:

moveLocations = GameManager.instance.MovesForPiece(movingPiece);
locationHighlights = new List();

foreach (Vector2Int loc in moveLocations)
{
    GameObject highlight;
    if (GameManager.instance.PieceAtGrid(loc))
    {
        highlight = Instantiate(attackLocationPrefab, Geometry.PointFromGrid(loc),
            Quaternion.identity, gameObject.transform);
    } 
    else 
    {
        highlight = Instantiate(moveLocationPrefab, Geometry.PointFromGrid(loc),
            Quaternion.identity, gameObject.transform);
    }
    locationHighlights.Add(highlight);
}


Эта часть выполняет несколько действий:

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

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

Выполнение хода


Добавьте этот код под Опорной точкой 2, внутри конструкции if, проверяющей кнопку мыши:

if (!moveLocations.Contains(gridPoint))
{
    return;
}


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

Наконец, добавим в MoveSelector.cs код в конец ExitState:

foreach (GameObject highlight in locationHighlights)
{
    Destroy(highlight);
}


На этом этапе игрок выбрал ход, поэтому мы можем удалить все объекты оверлеев.

c3554ce7bb3b1bcdd3672d905a468144.png


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

Следующий игрок


Если может двигаться только одна сторона, то это не очень похоже на игру. Пока нам это исправить!

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

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

Само переключение реализовать достаточно просто. В GameManager есть переменные для текущего и другого игроков, поэтому нам просто нужно менять местами эти значения.

Сложность заключается в том, где нам вызывать замену?

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

Добавим следующий метод в конец класса скрипта GameManager.cs:

public void NextPlayer()
{
    Player tempPlayer = currentPlayer;
    currentPlayer = otherPlayer;
    otherPlayer = tempPlayer;
}


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

Перейдём к MoveSelector.cs и добавим в ExitState следующий код, прямо перед вызовом EnterState:

GameManager.instance.NextPlayer();


Вот и всё! ExitState и EnterState уже сами позаботятся о собственной очистке.

Запустите режим Play и вы увидите, что теперь фигуры движутся у обеих сторон. Мы уже становимся ближе к настоящей игре.

55cb21ced578c9d3a5ce0dbf939893a5.gif


Взятие фигур


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

public void CapturePieceAt(Vector2Int gridPoint)
{
    GameObject pieceToCapture = PieceAtGrid(gridPoint);
    currentPlayer.capturedPieces.Add(pieceToCapture);
    pieces[gridPoint.x, gridPoint.y] = null;
    Destroy(pieceToCapture);
}


Здесь GameManager проверяет, какая фигура находится в целевой позиции. Эта фигура добавляется в список взятых фигур для текущего игрока. Затем она удаляется из записи клеток доски GameManager, а GameObject уничтожается, что удаляет её из сцены.

Чтобы взять фигуру, нужно встать поверх неё. Поэтому код для вызова этого действия должен находиться в MoveSelector.cs.

В методе Update найдите комментарий Опорная точка 3 и замените его следующей конструкцией:

else
{
    GameManager.instance.CapturePieceAt(gridPoint);
    GameManager.instance.Move(movingPiece, gridPoint);
}


Предыдущая конструкция if проверяла, есть ли фигура в целевой позиции. Так как на этапе генерации ходов фигуры игрока были отфильтрованы, то на содержащей фигуру клетке должна быть фигура противника.

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

Нажмите на Play и перемещайте пешки, пока не сможете взять одну из них.

240e788e7e92611d6e970589bb8202a3.gif


Я ферзь, ты взял мою пешку — готовься к смерти!

Завершение игры


Шахматная партия заканчивается, когда игрок берёт короля противника. При взятии фигуры мы проверяем, король ли это. Если да, то игра окончена.

Но как нам остановить игру? Одним из способов является удаление с доски скриптов TileSelector и MoveSelector.

В методе CapturePieceAt скрипта GameManager.cs добавьте следующие строки перед удалением взятой фигуры:

if (pieceToCapture.GetComponent().type == PieceType.King)
{
    Debug.Log(currentPlayer.name + " wins!");
    Destroy(board.GetComponent());
    Destroy(board.GetComponent());
}


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

Destroy применяется не только для классов GameObject; можно использовать его и для удаления прикреплённого к объекту компонента.

Нажмите на Play. Переместите пешку и возьмите короля противника. Вы увидите, что в консоль Unity будет выведено сообщение о победе.

В качестве дополнительного задания можно добавить элементы UI для отображения сообщения «Game Over» или переход к экрану меню.

74fdb9b82772e8b5025201265dddc382.png


Теперь настало время достать серьёзное оружие и привести в движение более сильные фигуры!

Особые ходы


Piece и его отдельные подклассы — это отличный инструмент для инкапсуляции особых правил перемещения.

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

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

Ходы на несколько клеток


Более сложным случаем являются фигуры, которые могут двигаться на несколько клеток в одном направлении, а именно слон, ладья и ферзь. Слона показать проще, поэтому давайте начнём с него.

У Piece есть предварительно подготовленные списки направлений, в которых могут двигаться слон и ладья из начальной точки. Это все направления из текущей позиции фигуры.

Откройте Bishop.cs и замените MoveLocations следующим кодом:

public override List MoveLocations(Vector2Int gridPoint)
{
    List locations = new List();

    foreach (Vector2Int dir in BishopDirections)
    {
        for (int i = 1; i < 8; i++)
        {
            Vector2Int nextGridPoint = new Vector2Int(gridPoint.x + i * dir.x, gridPoint.y + i * dir.y);
            locations.Add(nextGridPoint);
            if (GameManager.instance.PieceAtGrid(nextGridPoint))
            {
                break;
            }
        }
    }

    return locations;
}


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

На каждом этапе мы сгенерируем GridPoint для позиции и добавим его в список. Затем проверим, есть ли в этой позиции фигура. Если есть, то прекращаем внутренний цикл, чтобы пойти в следующем направлении.

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

Примечание: если вам нужно отличать направление вперёд от направления назад, или влево от вправо, то нужно учитывать, что чёрные и белые фигуры движутся в противоположных направлениях.


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

Вот и всё! Запустите режим Play и попробуйте сыграть.

85bebee2a3152455c6f880f25c411415.png


Двигаем ферзя


Ферзь — самая сильная фигура, поэтому лучше всего будет закончить на нём.

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

В Queen.cs замените MoveLocations следующим кодом:

public override List MoveLocations(Vector2Int gridPoint)
{
    List locations = new List();
    List directions = new List(BishopDirections);
    directions.AddRange(RookDirections);

    foreach (Vector2Int dir in directions)
    {
        for (int i = 1; i < 8; i++)
        {
            Vector2Int nextGridPoint = new Vector2Int(gridPoint.x + i * dir.x, gridPoint.y + i * dir.y);
            locations.Add(nextGridPoint);
            if (GameManager.instance.PieceAtGrid(nextGridPoint))
            {
                break;
            }
        }
    }

    return locations;
}


Единственное, что здесь отличается — это превращение массива направлений в List.

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

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

525a282da93c7c161363b33ad39d143a.gif


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


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

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

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

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

© Habrahabr.ru