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