React + Three.js. Создаём собственный 3D шутер. Часть 2
Привет, уважаемые участники Хабр!
Введение
В современной веб-разработке границы между классическими и веб-приложениями стираются с каждым днём. Сегодня мы можем создавать не только интерактивные сайты, но и полноценные игры прямо в браузере. Одним из инструментов, который делает это возможным, является библиотека React Three Fiber — мощное средство для создания 3D-графики на основе Three.js с использованием технологии React.
В сегодняшней статье мы реализуем:
анимацию прицеливания из оружия;
анимацию вспышки при стрельбе;
добавим звуковой эффект при выстреле.
Репозиторий на GitHub
Финальное демо
Небольшие правки
Перед началом работы, создадим новую папку images для изображений в папке assets. И перенесём в эту папку изображение поверхности пола.
Также изменим путь к изображению в файле Ground.jsx.
Файл Ground.jsx с изменённым путём к изображению
Код раздела
Механика прицеливания
Как и в большинстве шутеров, прицеливание задействуется при помощи нажатия правой кнопки мыши. Но пользователи могут переназначить данную кнопку на любую другую в любой момент в настройках игры. Поэтому мы не будем непосредственно в коде задавать условие нажатие данной клавиши, а реализуем отдельный конфиг, в котором будем задавать управление.
В React (Vite) уже существует возможность без дополнительных библиотек создать такой конфиг. Для этого необходимо создать в корне проекта файл .env. Далее, именно в формате VITE_*** мы можем задавать переменные окружения, которые мы сможем использовать в любом месте нашего проекта.
В файле конфигурации .env добавим две переменные, которые будут содержать коды нажатия кнопок мыши. А именно, код кнопки мыши для активации стрельбы, а также для прицеливания.
.env с добавленными переменными
Теперь необходимо переработать логику нажатия кнопок мыши, чтобы при нажатии разных кнопок активировались разные действия.
Но сначала необходимо исправить некоторое некорректное поведение при перехвате курсора мыши на холсте. В данный момент, при клике мыши по экрану сразу же срабатывает действие выстрела, что выглядит немного странно. Поэтому мы добавим логику, что пока курсор не будет перехвачен приложением, то никаких событий клика не будет происходить.
Теперь мы воспользуемся библиотекой для хранения глобального состояния Zustand. В файле App.jsx добавим состояние, а также добавим обработчики событий на блокировку и разблокировку курсора.
Изменения в файле App.jsx
В файле Weapon.jsx добавим новую логику, которая будет разграничивать нажимаемые клавиши мыши, а также учитывать состояние перехваченного курсора.
Создадим функцию mouseButtonHandler. Внутри будем определять текущее состояние перехваченного курсора и в случае, если курсор ещё не был перехвачен, то мы не будем выполнять никаких действий. Ещё необходимо импортировать из конфига .env значения для клавиш мыши, при использовании которых активируется режим стрельбы или прицеливания.
Получение значений из .env
Также изменим логику обработчиков событий при нажатии и отпускании клавиш мыши.
Изменённый обработчик событий по клику мыши
Теперь займёмся непосредственно реализацией анимацией прицеливания.
Для начала, добавим новое состояние useAimingStore для хранения состояния прицеливания.
Импорт Zustand
Состояние в Weapon.jsx
Добавим переменную для возможности изменения состояния прицеливания.
Получение функции setIsAiming
А в функции mouseButtonHandler, где ранее оставили пустое место для кнопки AIM_BUTTON, добавляем изменение состояния.
Изменение состояния в mouseButtonHandler
Перейдём к файлу Player.jsx и займёмся реализацией внутри него.
Во-первых, необходимо импортировать состояния useAimingStore из файла Weapon.jsx. А также перенести константу easing в корень файла.
Импорт useAimingStore и константа easing
Добавим состояние isAiming для дальнейшего использования в файле.
Состояние isAiming
Для сохранения состояния анимации добавим два состояния: анимация прицеливания и возврат в исходное состояние.
Добавление состояний aimingAnimation и aimingBackAnimation
Теперь создадим функцию initAimingAnimation, в которой будут описываться оба состояния анимации прицеливания.
Функция initAmingAnimation
Чтобы данная анимация могла запускаться, необходимо при инициализации приложения вызвать функцию initAimingAnimation. При этом именно тогда, когда уже будет готов для взаимодействия объект, внутри которого находится модель оружия.
useEffect для инициализации анимации initAimingAnimation
При изменении состояния isAiming необходимо запускать или анимацию прицеливания, или возврата оружия в начальное положение. Для этого добавим useEffect, внутри которого по условию будет срабатывать та или иная логика. Так, например, при начале процесса прицеливания, потребуется остановить анимацию «покачивания», а затем запустить анимацию прицеливания. При отпускании кнопки мыши запускается анимация возврата оружия в начальное положение, а при срабатывании события onComplete повторно запускается анимация «покачивания».
useEffect для активации эффекта анимации
Но сейчас при инициализации приложения происходит начальный запуск анимации и получается, что будто игрок прицеливался, а затем выходит из режима прицеливания. Происходит это потому, что по умолчанию isAiming установлен в false, и при инициализации сразу же срабатывает ветка «или» в условии. Решить это можно, исправив значение по умолчанию в null, а затем изменив условие, добавив конкретное значение в условии.
Состояние с изменённым значением isAiming на null
useEffect для активации анимации с исправленным условием
Таким образом, теперь при клике правой кнопки мыши у нас происходит анимация прицеливания, а при отпускании — выход из этого режима.
Анимация прицеливания
Код раздела
Рефакторинг анимации отдачи
Перед тем, как приступить к следующей части нашей статьи, проведём некоторый рефакторинг реализации анимаций.
В файле Player.jsx изменим название функции с setAnimationParams на setSwayingAnimationParams. А также заменим данное название функции в остальных местах.
Файл Player.jsx с изменённым названием функции
А из функции initSwayingObjectAnimation уберём константу easing, т.к. ранее мы вынесли её в корень файла.
Убранная константа easing
Перейдём к исправлению файла Weapon.jsx.
Изменим значение для recoilDuration на 50.
Изменённое значение recoilDuration
Удалим recoilBackAnimation за ненадобностью. Вместо этого теперь добавим isRecoilAnimationFinished.
Добавленное состояние isRecoilAnimationFinished
Для функции generateNewPositionOfRecoil добавим значение по умолчанию для параметра currentPosition.
Значение по умолчанию для функции generateNewPositionOfRecoil
В функции initRecoilAnimation удалим константу initialPosition за ненадобностью.
Убрана переменная initialPosition
Для Tween-анимации переработаем логику, сделав её автоматически-возвратной к исходной позиции, из которой началось выполнение анимации. Также удалим возвратную анимацию twRecoilBackAnimation.
Изменённая функция initRecoilAnimation
Переработаем useEffect, разделив на 2 разные функции. В первой будет инициализации анимации, а во второй будет запуск анимации отдачи оружия.
Изменённая логика useEffect
Код раздела
Анимация вспышки при выстреле
Теперь займёмся реализацией отображения вспышки при выстреле из оружия.
В файле Weapon.jsx импортируем функцию useLoader. А также загрузим изображение вспышки в assets/images.
Изображение вспышки при выстреле
В компоненте импортируем данное изображение как FlashShoot.
Импорт изображения
Далее воспользуемся функцией useLoader для загрузки данного изображение на сцену. А также добавим состояние для сохранения анимации flashAnimation.
Импорт useLoader
Загрузка изображения на сцену и состояние анимации вспышки
Создадим новое состояние flashOpacity для плавного изменения прозрачности вспышки. Также создадим новую функцию initFlashAnimation, в которой опишем последовательность анимации. И после этого воспользуемся useEffect для инициализации анимации.
Реализация анимации вспышки при выстреле
Теперь необходимо вывести данное изображение на сцене на одном уровне с моделью оружия, задав расположение таким образом, чтобы изображение располагалось непосредственно на конце дула оружия.
Добавление изображения на сцену
И в конце добавим вызов анимации при каждом выстреле в функции startShooting.
Доработанная функция startShooting
Анимация вспышки при выстреле
Код раздела
Добавление звука выстрела
Добавим подготовленный звуковой файл в папку assets/sounds.
Импортируем его в файл.
Импорт аудиофайла
При помощи HTMLAudioElement мы можем добавить звуковой файл для текущей страницы (не для сцены).
Добавление аудио в документ
При вызове функции startShooting запускаем данный аудиофайл. При нескольких выстрелах запускаемые аудиофайлы будут запускаться и накладываться друг на друга. И в конце просто прекращать воспроизводиться.
Проигрывание аудио в функции startShooting
Код раздела
Заключение
В этой статьи мы добавили анимацию прицеливания, анимацию появления вспышки при выстреле, а также звуковой эффект при выстреле. В следующей части мы продолжим дорабатывать нашу игру, добавляя новый функционал.
Спасибо за прочтение и буду рад ответить на комментарии!