В современной веб-разработке границы между классическими и веб-приложениями стираются с каждым днем. Сегодня мы можем создавать не только интерактивные сайты, но и полноценные игры прямо в браузере. Одним из инструментов, который делает это возможным, является библиотека — мощный инструмент для создания 3D-графики на основе с использованием технологии . React Three Fiber Three.js React О стеке React Three Fiber — это оболочка над , которая использует структуру и принципы для создания 3D-графики в Интернете. Этот стек позволяет разработчикам объединить мощь с удобством и гибкостью , делая процесс создания приложения более интуитивным и организованным. React Three Fiber Three.js React Three.js React В основе лежит идея о том, что все, что вы создаете в сцене, является компонентом . Это позволяет разработчикам применять знакомые шаблоны и методологии. React Three Fiber React Одним из главных преимуществ является простота интеграции с экосистемой . Любые другие инструменты по-прежнему можно легко интегрировать при использовании этой библиотеки. React Three Fiber React React Актуальность веб-разработчика игр За последние годы претерпела серьезные изменения: от простых 2D игр до сложных 3D-проектов, сравнимых с настольными приложениями. Этот рост популярности и возможностей делает Web-GameDev областью, которую нельзя игнорировать. Web-GameDev Flash- Одним из главных преимуществ веб-игр является их доступность. Игрокам не нужно скачивать и устанавливать какое-либо дополнительное программное обеспечение – достаточно перейти по ссылке в своем браузере. Это упрощает распространение и продвижение игр, делая их доступными для широкой аудитории по всему миру. Наконец, разработка веб-игр может стать для разработчиков отличным способом попробовать свои силы в разработке игр с использованием знакомых технологий. Благодаря имеющимся инструментам и библиотекам даже без опыта работы в 3D-графике можно создавать интересные и качественные проекты! Производительность игры в современных браузерах Современные браузеры прошли долгий путь, превратившись из довольно простых инструментов просмотра веб-страниц в мощные платформы для запуска сложных приложений и игр. Основные браузеры, такие как , , и , постоянно оптимизируются и развиваются для обеспечения высокой производительности, что делает их идеальной платформой для разработки сложных приложений. Chrome Firefox Edge другие Одним из ключевых инструментов, который способствовал развитию браузерных игр, является . Этот стандарт позволил разработчикам использовать аппаратное ускорение графики, что значительно улучшило производительность 3D-игр. Вместе с другими веб-API открывает новые возможности для создания впечатляющих веб-приложений прямо в браузере. WebGL WebGL Тем не менее, при разработке игр для браузера крайне важно учитывать различные аспекты производительности: оптимизация ресурсов, управление памятью и адаптация под разные устройства — все это ключевые моменты, которые могут повлиять на успех проекта. На вашу отметку! Однако слова и теория – это одно, а практический опыт – совсем другое. Чтобы по-настоящему понять и оценить весь потенциал разработки веб-игр, лучший способ — погрузиться в процесс разработки. Поэтому в качестве примера успешной разработки веб-игр мы создадим собственную игру. Этот процесс позволит нам изучить ключевые аспекты разработки, столкнуться с реальными проблемами и найти их решения, а также увидеть, насколько мощной и гибкой может быть платформа для разработки веб-игр. В серии статей мы рассмотрим, как создать шутер от первого лица, используя возможности этой библиотеки, и окунемся в захватывающий мир веб-геймдева! Финальная демо-версия https://codesandbox.io/p/github/JI0PATA/fps-game?embedable=true Репозиторий на 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 Затем из нашего проекта все ненужное. удалите Код раздела Настройка отображения холста В файле добавьте элемент div, который будет отображаться на странице в качестве области видимости. Вставьте компонент и установите поле зрения камеры. Внутри компонента поместите компонент . main.jsx Canvas Canvas App Давайте добавим стили в , чтобы растянуть элементы пользовательского интерфейса на всю высоту экрана и отобразить область видимости в виде круга в центре экрана. index.css В компонент мы добавляем компонент , который будет отображаться в качестве фона в нашей игровой сцене в виде неба. App Sky Код раздела Поверхность пола Давайте создадим компонент и поместим его в компонент . Ground App В создайте элемент плоской поверхности. По оси Y переместите его вниз так, чтобы эта плоскость оказалась в поле зрения камеры. А также переверните плоскость по оси X, чтобы сделать ее горизонтальной. Ground Несмотря на то, что в качестве цвета материала мы указали серый, плоскость кажется полностью черной. Код раздела Основное освещение По умолчанию в сцене нет освещения, поэтому добавим источник , который освещает объект со всех сторон и не имеет направленного луча. В качестве параметра задайте интенсивность свечения. светаambientLight Код раздела Текстура поверхности пола Чтобы поверхность пола не выглядела однородной, добавим текстуру. Сделайте рисунок поверхности пола в виде ячеек, повторяющихся по всей поверхности. В папку добавьте изображение PNG с текстурой. с ресурсами Чтобы загрузить текстуру на сцену, воспользуемся хуком из пакета . А в качестве параметра для хука мы передадим импортированное в файл изображение текстуры. Установите повторение изображения по горизонтальным осям. useTexture @react-three/drei Код раздела Движение камеры С помощью компонента из пакета зафиксируйте курсор на экране так, чтобы он не перемещался при перемещении мыши, а менял положение камеры на сцене. PointerLockControls @react-three/drei Давайте внесем небольшие изменения в компонент . «Земля» Код раздела Добавляем физику Для наглядности добавим в сцену простой куб. <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 После этого мы сразу увидим, что при каждой перезагрузке страницы куб падает вниз под действием силы тяжести. Но теперь есть другая задача — необходимо сделать пол объектом, с которым куб сможет взаимодействовать, и за пределы которого он не упадет. Код раздела Пол как физический объект Давайте вернемся к компоненту и добавим компонент в качестве оболочки над поверхностью пола. Ground RigidBody Теперь при падении куб остается на полу, как настоящий физический объект. Код раздела Подчинение персонажа законам физики Давайте создадим компонент , который будет управлять персонажем на сцене. Player Персонаж представляет собой тот же физический объект, что и добавленный куб, поэтому он должен взаимодействовать с поверхностью пола так же, как и с кубом на сцене. Вот почему мы добавляем компонент . И сделаем персонажа в виде капсулы. RigidBody Поместите компонент внутри компонента Physics. Player Теперь на сцене появился наш персонаж. Код раздела Перемещение персонажа – создание крючка Управляться персонажем будет с помощью клавиш , а прыгать — с помощью . WASD клавиши «Пробел» С помощью собственного React-хука мы реализуем логику перемещения персонажа. Давайте создадим и добавим туда новую функцию . файлooks.js usePersonControls Давайте определим объект в формате {"код ключа": "действие, которое необходимо выполнить"}. Затем добавьте обработчики событий для нажатия и отпускания клавиш клавиатуры. При срабатывании обработчиков мы определим текущие выполняемые действия и обновим их активное состояние. В конечном результате хук вернет объект в формате {"действие в процессе": "статус"}. Код раздела Перемещение персонажа — реализация хука После реализации хука его следует использовать при управлении персонажем. В компоненте мы добавим отслеживание состояния движения и обновим вектор направления движения персонажа. usePersonControls Player Мы также определим переменные, которые будут хранить состояния направлений движения. Чтобы обновить положение персонажа, давайте , предоставляемым пакетом . Этот хук работает аналогично и выполняет тело функции примерно 60 раз в секунду. воспользуемся фреймом @react-three/fiber requestAnimationFrame Объяснение кода: Создайте ссылку для объекта игрока. Эта ссылка позволит напрямую взаимодействовать с объектом игрока на сцене. 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); Вычислите конечный вектор движения игрока, вычитая векторы движения, нормируя результат (так, чтобы длина вектора была равна 1) и умножая на константу скорости движения. 8. направление.subVectors(frontVector,sideVector).normalize().multiplyScalar(MOVE_SPEED); «Буждает» объект игрока, чтобы убедиться, что он реагирует на изменения. Если не использовать этот метод, через некоторое время объект «уснет» и не будет реагировать на изменение положения. 9. playerRef.current.wakeUp(); Установите новую линейную скорость игрока на основе рассчитанного направления движения и сохраните текущую вертикальную скорость (чтобы не влиять на прыжки или падения). 10. playerRef.current.setLinvel({ x: Direction.x, y: Velocity.y, Z: Direction.z }); В результате при нажатии клавиш персонаж начинал перемещаться по сцене. Он также может взаимодействовать с кубом, поскольку они оба являются физическими объектами. WASD Код раздела Перемещение персонажа - прыжок Для реализации прыжка воспользуемся функционалом из пакетов и . В этом примере давайте проверим, что персонаж находится на земле и нажата клавиша прыжка. В данном случае мы задаем направление и силу ускорения персонажа по оси Y. @dimforge/rapier3d-compat @react-three/rapier Для мы добавим массу и вращение блока по всем осям, чтобы он не падал в разные стороны при столкновении с другими объектами на сцене. Player Объяснение кода: Получение доступа к сцене физического движка . Он содержит все физические объекты и управляет их взаимодействием. константный мир = рапира.мир; Rapier Именно здесь и происходит «рейкастинг» (raycasting). Создается луч, который начинается в текущей позиции игрока и указывает вниз по оси Y. Этот луч «прибрасывается» в сцену, чтобы определить, пересекается ли он с каким-либо объектом в сцене. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); Условие проверяется, если игрок находится на земле: const grounded = ray && ray.collider && Math.abs(ray.toi) <= 1.5; - был ли создан ; луч луч — столкнулся ли луч с каким-либо объектом на сцене; ray.collider — «время экспозиции» луча. Если это значение меньше или равно заданному значению, это может указывать на то, что игрок находится достаточно близко к поверхности, чтобы считаться «находящимся на земле». Math.abs(ray.toi) Также необходимо изменить компонент , чтобы алгоритм трассировки лучей для определения статуса «приземления» работал корректно, добавив физический объект, который будет взаимодействовать с другими объектами сцены. Ground Давайте поднимем камеру немного выше, чтобы лучше рассмотреть сцену. Код раздела Первый коммит Второй коммит Перемещение камеры за персонажем Чтобы переместить камеру, мы получим текущую позицию игрока и изменим положение камеры каждый раз при обновлении кадра. А чтобы персонаж двигался точно по той траектории, куда направлена камера, нам нужно добавить . applyEuler Объяснение кода: Метод применяет поворот к вектору на основе заданных углов Эйлера. В этом случае вращение камеры применяется к вектору . Это используется для согласования движения относительно ориентации камеры, чтобы игрок двигался в направлении вращения камеры. applyEuler направления Давайте немного подкорректируем размер и сделаем его выше относительно куба, увеличив размер и исправив логику «прыжка». Player CapsuleCollider Код раздела Первый коммит Второй коммит Генерация кубов Чтобы сцена не казалась совершенно пустой, добавим генерацию куба. В 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 Код раздела Импорт модели в проект Теперь добавим на сцену 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 Код раздела Добавление теней На этом этапе нашей сцены ни один из объектов не отбрасывает тени. Чтобы включить на сцене, вам необходимо добавить атрибут в компонент . тени Shadows Canvas Далее нам нужно добавить новый источник света. Несмотря на то, что у нас на сцене уже есть , он не может создавать тени для объектов, поскольку не имеет направленного луча света. Итак, давайте добавим новый источник света под названием и настроим его. Атрибут, позволяющий включить режим тени « », — . Именно добавление этого параметра указывает на то, что данный объект может отбрасывать тень на другие объекты. AmbientLight directedLight cast castShadow После этого добавим к компоненту еще один атрибут , который означает, что компонент в сцене может получать и отображать тени на себе. Ground getShadow Аналогичные атрибуты следует добавить и к другим объектам на сцене: кубикам и игроку. Для кубов мы добавим и , потому что они могут как отбрасывать, так и получать тени, а для игрока мы добавим только . castShadow getShadow castShadow Давайте добавим для . castShadow Player Добавьте и для . castShadow getShadow Cube Код раздела Добавление теней – исправление отсечения теней Если вы присмотритесь сейчас, вы обнаружите, что площадь поверхности, на которую отбрасывается тень, довольно мала. А при выходе за пределы этой области тень просто обрезается. Причина этого в том, что по умолчанию камера захватывает только небольшую область отображаемых теней от . Мы можем использовать компонент , добавив дополнительные атрибуты чтобы расширить эту область видимости. После добавления этих атрибутов тень станет слегка размытой. Для улучшения качества мы добавим . directedLight directedLight Shadow-Camera-(сверху, снизу, слева, справа), атрибутshadow-mapSize Код раздела Привязка оружия к персонажу Теперь добавим отображение оружия от первого лица. Создайте новый компонент , который будет содержать логику поведения оружия и саму 3D-модель. Weapon import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); } Давайте разместим этот компонент на том же уровне, что и персонажа, и в хуке зададим положение и угол поворота на основе положения значений с камеры. RigidBody useFrame Код раздела Анимация раскачивания оружия при ходьбе Чтобы сделать походку персонажа более естественной, добавим небольшое покачивание оружия во время движения. Для создания анимации мы будем использовать установленную библиотеку . tween.js Компонент будет заключен в групповой тег, чтобы вы могли добавить ссылку на него с помощью перехватчика . Weapon useRef Давайте добавим для сохранения анимации. useState Давайте создадим функцию для инициализации анимации. Объяснение кода: Создание анимации «раскачивания» объекта из текущего положения в новое. const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Создание анимации объекта, возвращающегося в исходное положение после завершения первой анимации. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Соединение двух анимаций, чтобы после завершения первой анимации автоматически запускалась вторая. twSwayingAnimation.chain(twSwayingBackAnimation); В мы вызываем функцию инициализации анимации. useEffect Теперь необходимо определить момент, в течение которого происходит движение. Это можно сделать, определив текущий вектор направления персонажа. Если произойдет движение персонажа, мы обновим анимацию и запустим ее снова, когда закончим. Объяснение кода: Здесь проверяется состояние движения объекта. Если вектор направления имеет длину больше 0, это означает, что объект имеет направление движения. const isMoving = направление.длина() > 0; Это состояние выполняется, если объект движется и анимация «раскачивания» завершилась. if (isMoving && isSwayingAnimationFinished) { ... } В компоненте добавим , в котором мы будем обновлять анимацию анимации. App useFrame обновляет все активные анимации в библиотеке . Этот метод вызывается для каждого кадра анимации, чтобы гарантировать плавность всех анимаций. TWEEN.update() TWEEN.js Код раздела: Первый коммит Второй коммит Анимация отдачи Нам нужно определить момент выстрела, то есть момента нажатия кнопки мыши. Давайте добавим для хранения этого состояния, для хранения ссылки на объект оружия и два обработчика событий для нажатия и отпускания кнопки мыши. useState useRef Давайте реализуем анимацию отдачи при нажатии кнопки мыши. Для этой цели мы будем использовать библиотеку . tween.js Определим константы для силы отдачи и продолжительности анимации. Как и в случае с анимацией покачивания оружия, мы добавляем два состояния useState для анимации отдачи и возврата в исходное положение, а также состояние со статусом окончания анимации. Давайте создадим функции для получения случайного вектора анимации отдачи . —generateRecoilOffset иgenerateNewPositionOfRecoil Создайте функцию для инициализации анимации отдачи. Мы также добавим , в котором укажем состояние «кадра» как зависимость, чтобы при каждом кадре анимация инициализировалась заново и генерировались новые конечные координаты. useEffect А в добавим проверку «удержания» клавиши мыши при стрельбе, чтобы анимация стрельбы не прекращалась до тех пор, пока клавиша не будет отпущена. useFrame Код раздела Анимация во время бездействия Реализовать анимацию «бездействия» для персонажа, чтобы не было ощущения «зависания» игры. Для этого давайте добавим несколько новых состояний через . useState Давайте исправим инициализацию анимации «покачивания», чтобы использовать значения из состояния. Идея состоит в том, что разные состояния: ходьба или остановка — будут использовать разные значения анимации, и каждый раз анимация будет инициализироваться первой. Заключение В этой части мы реализовали генерацию сцены и движение персонажа. Также мы добавили модель оружия, анимацию отдачи при стрельбе и на холостом ходу. В следующей части мы продолжим дорабатывать нашу игру, добавляя новый функционал. Также опубликовано . здесь