Кросс-платформенная разработка мобильных игр на Cocos Creator — Урок 5: Генерация параметров платформы

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

Напомню, что на прошлом уроке мы создали ноду platforms и прикрепили к ней скрипт Platforms, который создает внутри этой ноды платформы. В данный момент платформы создаются тремя вызовами метода createPlatform с заранее заданными параметрами. То есть теоретически, при желании сделать заранее заскриптованный уровень, мы могли бы создать некоторый конфиг, содержащий параметры позциций и длин платформ и пройдясь в цикле по всем элементами такого конфига мы могли бы создать целый уровень. Вместо этого мы будем создавать платформы автоматически, так как игровой уровень у нас бесконечный.

Так как метод createPlatform у нас принимает в качестве параметра объект с нужными характеристика платформы, то если мы хотим создавать платформы динамически, то нам нужно каким-то образом сгенерировать нужные значения для каждой новой платформы. Поэтому заведем новый метод под названием generatePlatformData
Этот метод будет возвращать именно такой объект, который вдальнейшем можно будет передать в createPlatform для создания нужной платформы:

   generateRandomData() {
       let data = {
           x: 0,
           y: 0,
           tilesCount: 0
       };

       return data;
   },

Приступим к реализации этого метода. Как мы видим, в данном методе нам потребуется сгенерировать 3 характеристики платформы: координаты x, y новой платформы, а также ее длину.

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

Начнем с генерацией отступов. Создадим свойства

       xOffsetMin: 60,
       xOffsetMax: 200,
       yOffsetMin: -120,
       yOffsetMax: 120,

В объекте properties. Мы завели свойства для максимально допустимых показателей отступа от края предыдущей платформы для координат x и y. Как указать правильные значения и почему я выбрал именно такие цифры в своем коде? Здесь все просто — эти показатели выбираются интуитивно. В том числе поэтому мы заводим данные свойства не константными значениями, а свойствами объекта properties, которые нам станут доступны в редакторе для визуальной настройки. И экспериментируя, изменяя значения и перезапуская игру, проверяя новые показатели, мы со временем сможем подобрать нужные значения. Так, методом проб и ошибок, я со временем пришел к тому, что такие цифры меня вполне устраивают. Вы можете либо использовать мои значения, либо проверить их на собственном опыте и позапускать игру с разными параметрами. Тем более, что изменяя их в редакторе процесс тестирования будет достаточно простым и лишний раз лезть в код не придется.

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

       const xOffset = this.xOffsetMin + Math.random() * (this.xOffsetMax - this.xOffsetMin);
       const yOffset = this.yOffsetMin + Math.random() * (this.yOffsetMax - this.yOffsetMin);

Здесь мы использовали способ получения случайного числа из заданного диапазона при помощи функции Math.random:

const randomRumber = min + Math.random() * (max - min)

Работает это так. Math.random возвращает случайное число между 0 и 1, включая 0 и не включая 1. Представим, что нам нужно сгенерировать случайное число от 1 до 10.
Подставим 1 в качестве min, а 10 в качестве max в формулу и получим:

1 + Math.random() * (10 - 1)

Теперь представим 2 крайних случая. Мы знаем, что Math. random возвращает число от 0 до 1, включая 0, но не включая 1. Значит, самое меньшее возможное число, которое может вернуть Math,random — это 0. Тогда подставим этот крайний случай в формулу мы получим:

1 + 0 * (10 - 1) = 1.

Таким образом в первом крайнем случае при наименьшем возможном значении math.random мы получим 1, что удовлетворяет нашей первоначальной цели получить число от 1 до 10.
Теперь возьмем другой крайний случай и подставим в формулу максимально допустимое возможное значение Math.random: 0.9999(9):

1 + 0.9(9) * (10 - 1) = 1 + 0.9(9) * 9 = 1 + 8.9(9) = 9.(9)

Таким образом, максимум, который мы в теории можем получить — это 9.(9), что меньше 10. И если бы мы работали с целыми числами, что чтобы выполнить исходное условие мы могли бы добавить округление результата Math.random() * (10 — 1) до целого числа. Но в нашем случае у нас нет необходимости генерировать целые числа и мы упростили формулу.

Отступ мы сгенерировали, а как получить край предыдущей платформы, чтобы полученный отступ применить?
Для этого нам необходимо запоминать каждую создаваемую платформу. Перейдем в метод createPlatform и вместо использования локальной переменной ноде поместим только что созданную платформу в свойство this.current:

   createPlatform(data) {
       this.current = cc.instantiate(this.platform);
       this.node.addChild(this.current);
       const platform = this.current.getComponent('Platform');
       platform.init(data.tilesCount, data.x, data.y);
   }

Таким образом у нас есть доступ к свойству this.current из метода generateRandomData
Чтобы получить позицию новой платформы по оси x достаточно прибавить к правому краю прошлой платформы полученное смещение:

       data.x = this.current.node.x + this.current.node.width + xOffset;

Напомню, что на уроке, в котором мы создали компонент Platform, мы принудительно установили для префаба платформы точку отсчета координат в левый нижний угол. Таким образом начало координат для платформы отсчитывается с левого нижнего угла. Это значит, что для получения правого края платформы нам потребуется взять ее координату x, которая обусловлена anchor point и соответствует левому нижнему углу и прибавить к ней всю ширину платформы.

Для получения позиции y новой платформы начнем с добавления смещения к текущей координате y:

       let y = this.current.node.y + yOffset;

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

       const screenTop = cc.winSize.height / 2;
       const screenBottom = -cc.winSize.height / 2;

Напомню, что отсчет координат у нас по умолчанию начинается из центра экрана. Если мы принудительно не изменили данную установку. А это значит, что если вся высота экрана у нас отражена в параметре cc.winSize.height, то положительное значение половины высоты будет соответствовать верхнему краю, а отрицательное значение половины высоты будет соответствовать нижнему краю экрана.
Теперь, имея координаты, за которые ноде нельзя выходить на экране, обработваем полученную переменную y:

       y = Math.min(y, screenTop - this.current.node.height * 2); // не выше, чем верх экрана
       y = Math.max(y, screenBottom + this.current.node.height); // не ниже, чем низ экрана

Используем Math.min для проверки верхнего положения, т.к. чем выше на экране нода, тем большее ее координата y. Значит, если y больше, чем

screenTop - this.current.node.height * 2

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

И наоборот для нижнего края. Чем ниже нода на экране, тем меньше ее координата y.
Значит, если y будет меньше, чем screenBottom + this.current.node.height
то Math.max выберет последнее в качестве значения y.
В этом случае нам не обязательно использовать двойную высоту платформы, т.к. не критично, если нода будет приклеена к низу экрана.

Остается присвоить полученную y в объект data:

      data.y = y;

Остается сгенерировать нужное число тайлов для новой платформы. Предлагаю выполнить эту задачу в виде челленджа. Используя полученную ранее формулу

const random = min + Math.random() * (max - min)

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

Поставьте видео на паузу и попробуйте решить данную задачу.

Ну а теперь выполним решение вместе!
Используя полученную ранее формулу для генерации случайного числа в заданном диапазоне получает случайное число тайлов для генерации:

       data.tilesCount = this.tilesCountMin + Math.floor(Math.random() * (this.tilesCountMax - this.tilesCountMin));

Добавим поля в объект свойств для визуальной настройки в редакторе:

       tilesCountMin: 2,
       tilesCountMax: 6,

И в данном случае мы уже округляем результат Math.random * (max — min) до нижнего целого числа, чтобы работать с целыми числами, т.к. мы не хотим получить 4.5 тайлов для генерации платформы.

К этому моменту мы научились создавать новые платформы со случайными параметрами, но мы пока не создаем их на самом уровне.

Начнем с того, что удалим все вызовы createPlatform из метода start кроме первого, который оставим отвечать за создание первой и единственной заранее предопределенной платформы в игре, на которой начинает свою игру герой.

Метод createPlatform принимает параметр data для генерации платформы. Случайные параметры для новой платформы мы создаем в методе generateRandomData.

Раз мы договорились оставить возможность создавать платформы с предопределенными параметрами и используем эту возможность для генерации самой первой платформы, то необходимо оставить поддержку аргумента data в методе createPlatform. Но теперь также необходимо обработать случай, при котром параметр data не передается в данный метод, чтобы создать все свойства платформы автоматически.
Сделать это просто и в самом начале метода createPlatform проверим, передано ли значение в параметре data:

   createPlatform(data) {
       if (!data) {
           data = this.generateRandomData();
       }

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

Итак, мы будем вызывать createPlatform без параметра data для автоматический генерации новой платформы только в том случае, если последняя созданная платформа, которая хранится в свойстве this.current, целиком и полностью видна на экране. То есть, если созданная платформа целиком и полностью видна игроку, значит нужно создать новую платформу справа от нее.

Чтобы самостоятельно реализовать данный функционал ответьте на следующий вопрос:

Что значит, что платформа целиком и полностью видна на экране?
Какой предопределенный движком метод компонента Platforms подходит для многократной проверки состояния игры

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

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

Итак, что значит, что платформа целиком видна игрока с технической точки зрения?
Это значит, что координата правого края платформы меньше координаты правого края экрана. Мы знаем, что координата правого края экрана — это cc.winSize.width / 2
Напомню, что отсчет координат по умолчанию в кокосе для всех нод идет из центра ноды. Отсчет координат на канвасе идет из центра канваса.

Отсчет координат на ноде платформы мы изменили вручную. И он идет от нижнего левого угла. А значит, чтобы получить правый угол платформы нужно прибавить к координате платформы ее ширину:
const currentRight = this.current.node.x + this.current.node.width;

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

   update (dt) {
       const currentRight = this.current.node.x + this.current.node.width;

       if (currentRight < cc.winSize.width / 2) {
           this.createPlatform();
       }
   },

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

Прежде, чем протестировать игру, предлагаю сместить героя и первую платформу в левый нижний угол. Зададим герою координаты -160 -110. А первой платформе

       this.createPlatform({
           tilesCount: 4,
           x: -200,
           y: -200
       });

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

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

На следующем уроке мы займемся оптимизацией текущего скрипта. При реализуем переиспользование объектов платформ, чтобы в нашей игре память использовалась более оптимально и не было утечек памяти. А также внимательнее посмотрим на физические тела и продемонстрируем инструменты отладки физических тел в Cocos Creator. До встречи!

Leave a reply:

Your email address will not be published.

Site Footer