Давайте продолжим наш веб-бесконечный раннер и подготовим нашу игровую логику!

⬅️ Урок №1. Создание базовой 3D-сцены с помощью Three.js| TOC | Урок №3: Моделирование нашего космического корабля! ➡️

В первом выпуске этой серии мы рассказали о нашей игре и библиотеке Three.js. Затем мы настроили нашу среду разработки и создали базовую 3D-сцену с зеленым вращающимся кубом.

Сегодня мы сделаем шаг назад и больше сосредоточимся на логике нашей игры. Мы не будем реализовывать что-то совершенно новое, но мы подготовим общую архитектуру нашей игры в специальном классе и получим обзор игровой логики.

Это руководство доступно как в формате видео, так и в текстовом формате — см. ниже :)

Настройка нашего класса

Зачем изолировать код в классе?

На данный момент мы закодировали все в нашем скрипте main.js, в функции window.onload(). Это нормально, если у вас небольшой проект, но по мере усложнения проекта лучше организовать его и разбить на несколько функций и, возможно, на пространства имен.

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

В нашем случае мы хотим изолировать все, что относится к нашей игре Hyperspeed, от основной рутины. Итак, в конце концов, основной скрипт будет знать только то, что ему нужно подготовить 3D-сцену, что ему нужно создать экземпляр игры и инициализировать его и, наконец, что ему нужно определить цикл анимации, который вызывает метод update() в игре. пример.

Но основная процедура будет независима от всех деталей 3D-сцены — они будут обрабатываться самим экземпляром игры.

Это позволит нам легко изменять вещи внутри класса Game, не беспокоясь об основном скрипте, или даже переключать игру на совершенно другую, если она имеет тот же интерфейс, те же точки входа :)

Добавление и импорт нашего нового скрипта

Прежде чем мы начнем кодировать наш JS-класс, давайте создадим скрипт и правильно импортируем его в наш HTML-индекс. Мы просто добавим новый скрипт game.js в нашу папку scripts/, а затем обновим теги импорта index.html файла <head>:

Здесь я сделал две вещи: добавил ссылку на наш новый файл game.js и переместил импорт Three.js так, чтобы он был первым в блоке. Это могло бы избежать некоторых ошибок, потому что это гарантирует, что библиотека загружается перед нашими собственными сценариями, которые ее используют;)

Теперь давайте откроем наш новый скрипт game.js и создадим в нем базовый класс Game:

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

Но они не являются не «настоящими» классами, такими как классы в Java, C++ или C#, например, поэтому они не обладают всеми функциями, такими как инкапсуляция данных, общедоступность, защищенные или частные переменные, средства доступа и еще много чего.

Подготовка 2 основных точек входа

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

Конструктор будет вызываться автоматически, когда мы создадим новый экземпляр этого класса Game в нашем скрипте main.js. Он определит некоторые полезные переменные для экземпляра, подготовит 3D-сцену и привяжет наши обратные вызовы событий:

Метод обновления будет вызываться в нашем цикле анимации. Он будет реализовывать «шаблон обновления игры», который является распространенным шаблоном программирования для видеоигр. Идея этого шаблона заключается в том, что в видеоигре у вас есть множество систем, которые вы хотите регулярно запрашивать или обновлять. Итак, по сути, у вас будет бесконечный цикл, называемый «цикл обновления», который получает некоторую информацию от ваших систем, а затем использует эту информацию для обновления состояния игры.

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

Давайте подробнее рассмотрим, как работают оба эти шага ;)

Добавление обработчиков событий

Обработка событий на самом деле довольно проста в Javascript, потому что у нас есть несколько готовых прослушивателей, к которым мы можем привязать наши функции обратного вызова. Возможно, вы уже слышали о них — это такие события, как «keydown», «keyup», «mousemove» и так далее…

Объявление функций обратного вызова

Здесь мы будем играть с клавишами со стрелками на клавиатуре, поэтому нам нужно прослушивать события «keydown» и «keyup». Эти события изменят курс корабля: они повлияют на нашу скорость по оси X (потому что помните, что мы автоматически движемся по оси Z и не контролируем свое движение в этом направлении, но у нас есть право голоса в том, будет ли корабль идет влево или вправо!).

Мы можем объявить две наши функции обратного вызова прямо сейчас, функции _keydown() и _keyup().

Примечание: в этом руководстве я буду придерживаться своих старых привычек C# и добавлять к этим функциям префикс подчеркивания, чтобы помнить, что они являются вспомогательными функциями, которые являются своего рода «личными» для класса Game… даже несмотря на то, что « настоящие» классы в Javascript, поэтому они не будут инкапсулированы на самом деле ;)

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

Нашей функции _keyup() событие не понадобится; это в основном сбросит нас в режим «холостого хода», начальное состояние, в котором мы просто вечно летим по сетке вдоль оси Z без скорости по оси X.

Привязка обратных вызовов к событиям

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

Чтобы привязать функцию к событию, мы используем встроенный метод document.addEventListener() JS. Это просто вызовет функцию обратного вызова всякий раз, когда документ получает это событие. В нашем случае мы хотим, например, прослушивать событие «keydown», поэтому вы можете подумать о написании привязки следующим образом:

document.addEventListener('keydown', this._keydown);

Проблема в том, что если вы это сделаете, это сработает, но позже у вас возникнут проблемы, если вы попытаетесь использовать ключевое слово this внутри функции _keydown(). Это связано с тем, что ключевое слово this в Javascript является контекстным, а это означает, что его значение зависит от того, откуда была вызвана функция и как она была упакована при вызове. Если мы просто поместим функцию обратного вызова в метод document.addEventListener(), то ключевое слово this потеряет всякий смысл внутри функции _keydown().

Чтобы исправить это, мы должны добавить немного в конце, используя метод bind(), который передает правильное значение для ключевого слова this в функцию обратного вызова:

document.addEventListener('keydown', this._keydown.bind(this));

Благодаря этому изменению мы теперь будем получать наш экземпляр игры внутри вашей функции _keydown() при ее запуске, и поэтому мы сможем использовать ключевое слово this и заставить его ссылаться на экземпляр, как и ожидалось :)

Мы можем сделать то же самое для событий «keyup», что в конечном итоге дает следующий код:

И, собственно, обработка нашего события завершена! Нам не нужно ничего добавлять в метод update(). Благодаря встроенным методам Javascript для привязки обратных вызовов к событиям довольно легко реагировать на события клавиатуры;)

Определение шагов обновления

Давайте теперь перейдем ко второй части нашего метода update() и немного подумаем о том, что мы хотим сделать, чтобы пересчитать наше игровое состояние. Вопрос в том, что наш экземпляр игры должен регулярно обновлять? Какие различные проверки и процессы нам нужно запустить, чтобы реагировать на действия игрока и перенастраивать игровые данные?

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

Автодвижение корабля

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

Вот только… мы на самом деле не будем двигаться :)

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

Мы сделаем это в функции _updateGrid(), которую мы можем вызвать в нашем методе update():

Проверка на коллизии

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

Итак, у нас будет метод _checkCollisions(), который обрабатывает как препятствия, так и бонусы и сравнивает наше текущее расстояние до различных объектов на уровне с заданным порогом:

Обновление информационной панели

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

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

Давайте добавим метод _updateInfoPanel() для вызова из нашей функции update():

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

  • во-первых, есть метод _gameOver(), который подготовит «конечное состояние» — он покажет некоторые элементы пользовательского интерфейса и сбросит состояние экземпляра для новой игры.
  • а затем есть функция _initializeScene(), которая будет вызываться в конструкторе и подготавливать 3D-сцену: в следующем эпизоде ​​мы увидим, как перенести эту логику из main.js script в наш класс Game.

В целом, это дает нам следующий код для нашего полного класса Game:

Вывод

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

В следующем эпизоде ​​мы перенесем логику инициализации сцены из основной процедуры в наш класс Game и создадим более сложный трехмерный объект: наш маленький космический корабль!

⬅️ Урок №1. Создание базовой 3D-сцены с помощью Three.js| TOC | Урок №3: Моделирование нашего космического корабля! ➡️

Если вам понравилась эта статья, вы можете найти другие записи в блоге о технологиях, искусственном интеллекте и программировании на мой сайт :)