paint-brush
Создание собственного 3D-шутера с использованием стека React и Three.js — Часть 1к@varlab
911 чтения
911 чтения

Создание собственного 3D-шутера с использованием стека React и Three.js — Часть 1

к Ivan Zhukov17m2023/10/21
Read on Terminal Reader

Слишком долго; Читать

В эпоху активного развития веб-технологий и интерактивных приложений 3D-графика становится все более актуальной и востребованной. Но как создать 3D-приложение, не потеряв преимуществ веб-разработки? В этой статье мы рассмотрим, как объединить мощь Three.js с гибкостью React, чтобы создать собственную игру прямо в браузере. Эта статья познакомит вас с библиотекой React Three Fiber и научит создавать интерактивные 3D-игры.
featured image - Создание собственного 3D-шутера с использованием стека React и Three.js — Часть 1
Ivan Zhukov HackerNoon profile picture
0-item
1-item

В современной веб-разработке границы между классическими и веб-приложениями стираются с каждым днем. Сегодня мы можем создавать не только интерактивные сайты, но и полноценные игры прямо в браузере. Одним из инструментов, который делает это возможным, является библиотека React Three Fiber — мощный инструмент для создания 3D-графики на основе Three.js с использованием технологии React .

О стеке React Three Fiber

React Three Fiber — это оболочка над Three.js , которая использует структуру и принципы React для создания 3D-графики в Интернете. Этот стек позволяет разработчикам объединить мощь Three.js с удобством и гибкостью React , делая процесс создания приложения более интуитивным и организованным.


В основе React Three Fiber лежит идея о том, что все, что вы создаете в сцене, является компонентом React . Это позволяет разработчикам применять знакомые шаблоны и методологии.

Одним из главных преимуществ React Three Fiber является простота интеграции с экосистемой React . Любые другие инструменты React по-прежнему можно легко интегрировать при использовании этой библиотеки.

Актуальность веб-разработчика игр

За последние годы Web-GameDev претерпела серьезные изменения: от простых 2D Flash- игр до сложных 3D-проектов, сравнимых с настольными приложениями. Этот рост популярности и возможностей делает Web-GameDev областью, которую нельзя игнорировать.


Одним из главных преимуществ веб-игр является их доступность. Игрокам не нужно скачивать и устанавливать какое-либо дополнительное программное обеспечение – достаточно перейти по ссылке в своем браузере. Это упрощает распространение и продвижение игр, делая их доступными для широкой аудитории по всему миру.


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

Производительность игры в современных браузерах

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


Одним из ключевых инструментов, который способствовал развитию браузерных игр, является WebGL . Этот стандарт позволил разработчикам использовать аппаратное ускорение графики, что значительно улучшило производительность 3D-игр. Вместе с другими веб-API WebGL открывает новые возможности для создания впечатляющих веб-приложений прямо в браузере.


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

На вашу отметку!

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


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


Финальная демо-версия


Репозиторий на GitHub


Теперь давайте начнем!

Настройка проекта и установка пакетов

Прежде всего, нам понадобится шаблон проекта React . Итак, начнем с его установки.


 npm create vite@latest


  • выберите библиотеку React ;
  • выберите JavaScript .


Установите дополнительные пакеты npm.


 npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js


Затем удалите из нашего проекта все ненужное.


Код раздела

Настройка отображения холста

В файле main.jsx добавьте элемент div, который будет отображаться на странице в качестве области видимости. Вставьте компонент Canvas и установите поле зрения камеры. Внутри компонента Canvas поместите компонент App .


main.jsx


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


index.css


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


Приложение.jsx


Отображение неба в сцене


Код раздела

Поверхность пола

Давайте создадим компонент Ground и поместим его в компонент App .


Приложение.jsx


В Ground создайте элемент плоской поверхности. По оси Y переместите его вниз так, чтобы эта плоскость оказалась в поле зрения камеры. А также переверните плоскость по оси X, чтобы сделать ее горизонтальной.


Ground.jsx


Несмотря на то, что в качестве цвета материала мы указали серый, плоскость кажется полностью черной.


Квартира на месте происшествия


Код раздела

Основное освещение

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


Приложение.jsx


Освещенный самолет


Код раздела

Текстура поверхности пола

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

В папку с ресурсами добавьте изображение PNG с текстурой.


Добавлена текстура


Чтобы загрузить текстуру на сцену, воспользуемся хуком useTexture из пакета @react-three/drei . А в качестве параметра для хука мы передадим импортированное в файл изображение текстуры. Установите повторение изображения по горизонтальным осям.


Ground.jsx


Текстура на плоскости


Код раздела

Движение камеры

С помощью компонента PointerLockControls из пакета @react-three/drei зафиксируйте курсор на экране так, чтобы он не перемещался при перемещении мыши, а менял положение камеры на сцене.


Приложение.jsx


Демонстрация движения камеры


Давайте внесем небольшие изменения в компонент «Земля» .


Ground.jsx


Код раздела

Добавляем физику

Для наглядности добавим в сцену простой куб.


 <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> 


Куб на сцене


Сейчас он просто висит в космосе.


Используйте компонент «Физика» из пакета @react-three/rapier, чтобы добавить «физику» в сцену. В качестве параметра настраиваем гравитационное поле, где задаем гравитационные силы по осям.


 <Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>


Однако наш куб находится внутри физической составляющей, но с ним ничего не происходит. Чтобы куб вел себя как настоящий физический объект, нам нужно обернуть его в компонент RigidBody из пакета @react-three/rapier .


Приложение.jsx


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


Падение куба


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


Код раздела

Пол как физический объект

Давайте вернемся к компоненту Ground и добавим компонент RigidBody в качестве оболочки над поверхностью пола.


Ground.jsx


Теперь при падении куб остается на полу, как настоящий физический объект.


Падающий куб в самолет


Код раздела

Подчинение персонажа законам физики

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


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


Игрок.jsx


Поместите компонент Player внутри компонента Physics.


Приложение.jsx


Теперь на сцене появился наш персонаж.


Персонаж в форме капсулы


Код раздела

Перемещение персонажа – создание крючка

Управляться персонажем будет с помощью клавиш WASD , а прыгать — с помощью клавиши «Пробел» .

С помощью собственного React-хука мы реализуем логику перемещения персонажа.


Давайте создадим файлooks.js и добавим туда новую функцию usePersonControls .


Давайте определим объект в формате {"код ключа": "действие, которое необходимо выполнить"}. Затем добавьте обработчики событий для нажатия и отпускания клавиш клавиатуры. При срабатывании обработчиков мы определим текущие выполняемые действия и обновим их активное состояние. В конечном результате хук вернет объект в формате {"действие в процессе": "статус"}.


крючки.js


Код раздела

Перемещение персонажа — реализация хука

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


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


Игрок.jsx


Чтобы обновить положение персонажа, давайте воспользуемся фреймом , предоставляемым пакетом @react-three/fiber . Этот хук работает аналогично requestAnimationFrame и выполняет тело функции примерно 60 раз в секунду.


Игрок.jsx


Объяснение кода:

1. const playerRef = useRef(); Создайте ссылку для объекта игрока. Эта ссылка позволит напрямую взаимодействовать с объектом игрока на сцене.

2. const {вперед, назад, влево, вправо, прыжок} = usePersonControls(); При использовании перехватчика возвращается объект с логическими значениями, указывающими, какие кнопки управления в данный момент нажаты игроком.

3. useFrame((state) => { ... }); Крючок вызывается в каждом кадре анимации. Внутри этого хука обновляются положение и линейная скорость игрока.

4. возврат if (!playerRef.current); Проверяет наличие объекта игрока. Если объект игрока отсутствует, функция остановит выполнение, чтобы избежать ошибок.

5. константная скорость = playerRef.current.linvel(); Получите текущую линейную скорость игрока.

6. frontVector.set(0, 0, назад-вперед); Установите вектор движения вперед/назад в зависимости от нажатых кнопок.

7.sideVector.set(слева-справа, 0, 0); Установите вектор движения влево/вправо.

8. направление.subVectors(frontVector,sideVector).normalize().multiplyScalar(MOVE_SPEED); Вычислите конечный вектор движения игрока, вычитая векторы движения, нормируя результат (так, чтобы длина вектора была равна 1) и умножая на константу скорости движения.

9. playerRef.current.wakeUp(); «Буждает» объект игрока, чтобы убедиться, что он реагирует на изменения. Если не использовать этот метод, через некоторое время объект «уснет» и не будет реагировать на изменение положения.

10. playerRef.current.setLinvel({ x: Direction.x, y: Velocity.y, Z: Direction.z }); Установите новую линейную скорость игрока на основе рассчитанного направления движения и сохраните текущую вертикальную скорость (чтобы не влиять на прыжки или падения).


В результате при нажатии клавиш WASD персонаж начинал перемещаться по сцене. Он также может взаимодействовать с кубом, поскольку они оба являются физическими объектами.


Движение персонажа


Код раздела

Перемещение персонажа - прыжок

Для реализации прыжка воспользуемся функционалом из пакетов @dimforge/rapier3d-compat и @react-three/rapier . В этом примере давайте проверим, что персонаж находится на земле и нажата клавиша прыжка. В данном случае мы задаем направление и силу ускорения персонажа по оси Y.


Для Player мы добавим массу и вращение блока по всем осям, чтобы он не падал в разные стороны при столкновении с другими объектами на сцене.


Игрок.jsx


Объяснение кода:

  1. константный мир = рапира.мир; Получение доступа к сцене физического движка Rapier . Он содержит все физические объекты и управляет их взаимодействием.
  1. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); Именно здесь и происходит «рейкастинг» (raycasting). Создается луч, который начинается в текущей позиции игрока и указывает вниз по оси Y. Этот луч «прибрасывается» в сцену, чтобы определить, пересекается ли он с каким-либо объектом в сцене.
  1. const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.5; Условие проверяется, если игрок находится на земле:
  • луч - был ли создан луч ;
  • ray.collider — столкнулся ли луч с каким-либо объектом на сцене;
  • Math.abs(ray.toi) — «время экспозиции» луча. Если это значение меньше или равно заданному значению, это может указывать на то, что игрок находится достаточно близко к поверхности, чтобы считаться «находящимся на земле».


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


Ground.jsx


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


main.jsx


Персонаж прыгает


Код раздела

Перемещение камеры за персонажем

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


Игрок.jsx


Объяснение кода:

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


Давайте немного подкорректируем размер Player и сделаем его выше относительно куба, увеличив размер CapsuleCollider и исправив логику «прыжка».


Игрок.jsx


Перемещение камеры


Код раздела

Генерация кубов

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


 [ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]


В файле Cube.jsx создайте компонент Cubes , который будет генерировать кубы в цикле. А компонент Cube будет непосредственно сгенерированным объектом.


 import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); }


Добавим созданный компонент Cubes в компонент App , удалив предыдущий одиночный куб.


Приложение.jsx


Генерация кубов


Код раздела

Импорт модели в проект

Теперь добавим на сцену 3D-модель. Добавим модель оружия для персонажа. Начнем с поиска 3D модели. Например, возьмем вот этот .


Скачайте модель в формате GLTF и распакуйте архив в корень проекта.

Чтобы получить нужный нам формат для импорта модели в сцену, нам нужно будет установить дополнительный пакет gltf-pipeline .


npm i -D gltf-pipeline


С помощью пакета gltf-pipeline переконвертируем модель из формата GLTF в формат GLB , так как в этом формате все данные модели помещаются в один файл. В качестве выходного каталога для сгенерированного файла мы указываем общую папку.


gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb


Затем нам нужно сгенерировать компонент реагирования, который будет содержать разметку этой модели, чтобы добавить ее на сцену. Воспользуемся официальным ресурсом от разработчиков @react-three/fiber .


Для перехода к конвертеру вам потребуется загрузить преобразованный файл Weapon.glb .


Используя перетаскивание или поиск в Проводнике, найдите этот файл и загрузите его.


Конвертированная модель


В конвертере мы увидим сгенерированный реактив-компонент, код которого перенесём в наш проект в новом файле WeaponModel.jsx , изменив имя компонента на то же имя, что и файл.


Код раздела

Отображение модели оружия на сцене

Теперь импортируем созданную модель в сцену. В файл App.jsx добавьте компонент WeaponModel .


Приложение.jsx


Демонстрация импортной модели


Код раздела

Добавление теней

На этом этапе нашей сцены ни один из объектов не отбрасывает тени.

Чтобы включить тени на сцене, вам необходимо добавить атрибут Shadows в компонент Canvas .


main.jsx


Далее нам нужно добавить новый источник света. Несмотря на то, что у нас на сцене уже есть AmbientLight , он не может создавать тени для объектов, поскольку не имеет направленного луча света. Итак, давайте добавим новый источник света под названием directedLight и настроим его. Атрибут, позволяющий включить режим тени « cast », — castShadow . Именно добавление этого параметра указывает на то, что данный объект может отбрасывать тень на другие объекты.


Приложение.jsx


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


Ground.jsx


Модель отбрасывает тень


Аналогичные атрибуты следует добавить и к другим объектам на сцене: кубикам и игроку. Для кубов мы добавим castShadow и getShadow , потому что они могут как отбрасывать, так и получать тени, а для игрока мы добавим только castShadow .


Давайте добавим castShadow для Player .


Игрок.jsx


Добавьте castShadow и getShadow для Cube .


Куб.jsx


Все объекты на сцене отбрасывают тень


Код раздела

Добавление теней – исправление отсечения теней

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


Обрезка теней


Причина этого в том, что по умолчанию камера захватывает только небольшую область отображаемых теней от directedLight . Мы можем использовать компонент directedLight , добавив дополнительные атрибуты Shadow-Camera-(сверху, снизу, слева, справа), чтобы расширить эту область видимости. После добавления этих атрибутов тень станет слегка размытой. Для улучшения качества мы добавим атрибутshadow-mapSize .


Приложение.jsx


Код раздела

Привязка оружия к персонажу

Теперь добавим отображение оружия от первого лица. Создайте новый компонент Weapon , который будет содержать логику поведения оружия и саму 3D-модель.


 import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }


Давайте разместим этот компонент на том же уровне, что и RigidBody персонажа, и в хуке useFrame зададим положение и угол поворота на основе положения значений с камеры.


Игрок.jsx


Показ модели оружия от первого лица


Код раздела

Анимация раскачивания оружия при ходьбе

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


Компонент Weapon будет заключен в групповой тег, чтобы вы могли добавить ссылку на него с помощью перехватчика useRef .


Игрок.jsx


Давайте добавим useState для сохранения анимации.


Игрок.jsx


Давайте создадим функцию для инициализации анимации.


Игрок.jsx


Объяснение кода:

  1. const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Создание анимации «раскачивания» объекта из текущего положения в новое.
  1. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Создание анимации объекта, возвращающегося в исходное положение после завершения первой анимации.
  1. twSwayingAnimation.chain(twSwayingBackAnimation); Соединение двух анимаций, чтобы после завершения первой анимации автоматически запускалась вторая.


В useEffect мы вызываем функцию инициализации анимации.


Игрок.jsx


Теперь необходимо определить момент, в течение которого происходит движение. Это можно сделать, определив текущий вектор направления персонажа.


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


Игрок.jsx


Объяснение кода:

  1. const isMoving = направление.длина() > 0; Здесь проверяется состояние движения объекта. Если вектор направления имеет длину больше 0, это означает, что объект имеет направление движения.
  1. if (isMoving && isSwayingAnimationFinished) { ... } Это состояние выполняется, если объект движется и анимация «раскачивания» завершилась.


В компоненте App добавим useFrame , в котором мы будем обновлять анимацию анимации.


Приложение.jsx


TWEEN.update() обновляет все активные анимации в библиотеке TWEEN.js . Этот метод вызывается для каждого кадра анимации, чтобы гарантировать плавность всех анимаций.


Код раздела:

Анимация отдачи

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


Оружие.jsx


Оружие.jsx


Оружие.jsx


Давайте реализуем анимацию отдачи при нажатии кнопки мыши. Для этой цели мы будем использовать библиотеку tween.js .


Определим константы для силы отдачи и продолжительности анимации.


Оружие.jsx


Как и в случае с анимацией покачивания оружия, мы добавляем два состояния useState для анимации отдачи и возврата в исходное положение, а также состояние со статусом окончания анимации.


Оружие.jsx


Давайте создадим функции для получения случайного вектора анимации отдачи —generateRecoilOffset иgenerateNewPositionOfRecoil .


Оружие.jsx


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


Оружие.jsx


Оружие.jsx


А в useFrame добавим проверку «удержания» клавиши мыши при стрельбе, чтобы анимация стрельбы не прекращалась до тех пор, пока клавиша не будет отпущена.


Оружие.jsx


Анимация отдачи


Код раздела

Анимация во время бездействия

Реализовать анимацию «бездействия» для персонажа, чтобы не было ощущения «зависания» игры.


Для этого давайте добавим несколько новых состояний через useState .


Игрок.jsx


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


Игрок.jsx


Анимация ожидания


Заключение

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


Также опубликовано здесь .