Найти в Дзене
ZDG

Игра в Юнити-кубики #3. Стратегии рядов

Все материалы по Юнити

Предыдущие части: Структура блока, Игра в кубики

Исходный код:

GitHub - nandakoryaaa/blockjumper

Вслед за структурой блока идёт структура ряда.

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

У ряда есть ряд (хаха) задач.

  1. Передвинуть блоки
  2. Удалить блок в начале
  3. Добавить блок в конец
  4. Заполнить весь ряд блоками до конца (например, в начале игры, когда нужно сразу сделать заполненные ряды)
  5. Ответить на вопрос, требуется ли заполнение (влезает ли в конец ряда ещё один блок)

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

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

Поэтому я решил использовать шаблон проектирования Стратегия.

У каждого ряда будет своя стратегия движения. Всего стратегий будет три: Левая, Правая и Статичная. Но этот список в случае необходимости можно будет расширить.

Методы стратегии

Начнём с метода Update(), кторый передвигает блоки. Давайте посмотрим на примере стратегии, которая двигает блоки влево (класс RowStrategyLeft):

Мы перебираем в цикле все рабочие блоки, двигаясь от row.head к row.tail. Мы не используем в цикле i++, так как head и tail находятся в кольцевом буфере и могут перепрыгивать на начало. Поэтому используется метод row.Next(i), который корректно считает следующее значение i.

Далее нужно отнять от x-координаты блока скорость движения ряда row.speed, то есть блок должен сместиться влево. Однако делается это не очень просто. Нельзя просто вычесть число из координаты блока. Блок нужно именно переместить на новую позицию с помощью метода MovePosition(). Почему – я описывал в материале про физику:

Передать в MovePosition() требуется 3-мерный вектор. Поэтому такой вектор я сделал заранее в виде переменной _v и сразу присвоил его z-координате z-координату ряда, т.к. она уже меняться не будет. Ну и далее позиция вектора x вычисляется как позиция x блока минус скорость движения, и затем эта новая позиция передаётся в MovePosition().

Далее мы смотрим, не ушёл ли первый блок за край экрана. Для этого берётся блок в позиции head, так как при движении влево он и есть первый, и проверяется его видимость методом IsVisible(). Как работает этот метод, обсудим позже, а пока достаточно того, что он возвращает true или false.

Если блок невидим, то он удаляется из ряда. Как говорилось ранее, ничего удалять не надо, просто head смещается на 1 элемент вправо и теперь первый блок – тот, который был за первым.

А бывший первый блок деактивируется методом Deactivate(). Метод просто сообщает движку Unity, что данный GameObject не нужно никак обрабатывать, что экономит процессорное время:

-2

Наконец, после каждого смещения блоков влево в конце ряда образуется свободное пространство. Это пространство надо заполнить новыми блоками с помощью метода row.Fill() у ряда:

-3

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

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

Поcмотрим опять же стратегию левого движения, метод canFill():

-4

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

Иначе мы берём последний блок в ряду, то есть самый правый. Сейчас я заметил, что берётся блок не по индексу tail, а по индексу tail-1, а точнее, учитывая природу кольцевого буфера, row.Prev(tail) – этот метод ряда корректно считает предыдущий индекс. Это говорит о том, что tail показывает не на последний блок, а на следующий свободный блок. Почему я сделал так, уже не помню, но наверно была причина.

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

И как он добавляется:

-5

Сначала инициализируется блок по индексу row.tail (да, так как tail показывает на следующий свободный блок).

Затем этому блоку нужно назначить правильное смещение внутри ряда. Если ряд пустой (tail == head), то блок ставится в его начало. А началом у данной стратегии считается левая граница ряда.

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

После добавления нового блока в индекс tail мы сдвигаем tail вправо с помощью row.Next(tail), и теперь tail снова показывает на следующий свободный блок.

Собственно, вот базовый набор методов стратегии.

Чтобы понять, почему всё это пришлось делать, можно посмотреть код стратегии движения вправо (RowStrategyRight) и сравнить, где что изменилось.

Далее всё относительно просто. Я создам 5 рядов, и у каждого ряда будет своя стратегия. Вот начало класса Game, который собственно игра:

-6

Обратите внимание, что у самого первого ряда использована стратегия RowStrategyStatic, т.к. в начале игры он стоит на месте. У этой стратегии очень простые методы, которые практически ничего не делают, но она решает проблему количества блоков (ровно 1) и их размещения (посередине) приятным универсальным способом наряду с другими стратегиями.

Далее нужно ввести в дело игрока и заставить его прыгать и падать.