Привет, уважаемые участники Хабр!

Введение

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

В сегодняшней статье мы реализуем:

  • анимацию прицеливания из оружия;

  • анимацию вспышки при стрельбе;

  • добавим звуковой эффект при выстреле.

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

Финальное демо

Небольшие правки

Перед началом работы, создадим новую папку images для изображений в папке assets. И перенесём в эту папку изображение поверхности пола.

Также изменим путь к изображению в файле Ground.jsx.

Файл Ground.jsx с изменённым путём к изображению
Файл Ground.jsx с изменённым путём к изображению

Код раздела

Механика прицеливания

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

В React(Vite) уже существует возможность без дополнительных библиотек создать такой конфиг. Для этого необходимо создать в корне проекта файл .env. Далее, именно в формате VITE_*** мы можем задавать переменные окружения, которые мы сможем использовать в любом месте нашего проекта.

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

.env с добавленными переменными
.env с добавленными переменными

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

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

Теперь мы воспользуемся библиотекой для хранения глобального состояния Zustand. В файле App.jsx добавим состояние, а также добавим обработчики событий на блокировку и разблокировку курсора.

Изменения в файле App.jsx
Изменения в файле App.jsx

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

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

Получение значений из .env
Получение значений из .env

Также изменим логику обработчиков событий при нажатии и отпускании клавиш мыши.

Изменённый обработчик событий по клику мыши
Изменённый обработчик событий по клику мыши

Теперь займёмся непосредственно реализацией анимацией прицеливания. 

Для начала, добавим новое состояние useAimingStore для хранения состояния прицеливания.

Импорт Zustand
Импорт Zustand
Состояние в Weapon.jsx
Состояние в Weapon.jsx

Добавим переменную для возможности изменения состояния прицеливания.

Получение функции setIsAiming
Получение функции setIsAiming

А в функции mouseButtonHandler, где ранее оставили пустое место для кнопки AIM_BUTTON, добавляем изменение состояния.

Изменение состояния в mouseButtonHandler
Изменение состояния в mouseButtonHandler

Перейдём к файлу Player.jsx и займёмся реализацией внутри него.

Во-первых, необходимо импортировать состояния useAimingStore из файла Weapon.jsx. А также перенести константу easing в корень файла.

Импорт useAimingStore и константа easing
Импорт useAimingStore и константа easing

Добавим состояние isAiming для дальнейшего использования в файле.

Состояние isAiming
Состояние isAiming

Для сохранения состояния анимации добавим два состояния: анимация прицеливания и возврат в исходное состояние.

Добавление состояний aimingAnimation и aimingBackAnimation
Добавление состояний aimingAnimation и aimingBackAnimation

Теперь создадим функцию initAimingAnimation, в которой будут описываться оба состояния анимации прицеливания.

Функция initAmingAnimation
Функция initAmingAnimation

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

useEffect для инициализации анимации initAimingAnimation
useEffect для инициализации анимации initAimingAnimation

При изменении состояния isAiming необходимо запускать или анимацию прицеливания, или возврата оружия в начальное положение. Для этого добавим useEffect, внутри которого по условию будет срабатывать та или иная логика. Так, например, при начале процесса прицеливания, потребуется остановить анимацию “покачивания”, а затем запустить анимацию прицеливания. При отпускании кнопки мыши запускается анимация возврата оружия в начальное положение, а при срабатывании события onComplete повторно запускается анимация “покачивания”.

useEffect для активации эффекта анимации
useEffect для активации эффекта анимации

Но сейчас при инициализации приложения происходит начальный запуск анимации и получается, что будто игрок прицеливался, а затем выходит из режима прицеливания. Происходит это потому, что по умолчанию isAiming установлен в false, и при инициализации сразу же срабатывает ветка “или” в условии. Решить это можно, исправив значение по умолчанию в null, а затем изменив условие, добавив конкретное значение в условии.

Состояние с изменённым значением isAiming на null
Состояние с изменённым значением isAiming на null
useEffect для активации анимации с исправленным условием
useEffect для активации анимации с исправленным условием

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

Анимация прицеливания
Анимация прицеливания

Код раздела

Рефакторинг анимации отдачи

Перед тем, как приступить к следующей части нашей статьи, проведём некоторый рефакторинг реализации анимаций.

В файле Player.jsx изменим название функции с setAnimationParams на setSwayingAnimationParams. А также заменим данное название функции в остальных местах.

Файл Player.jsx с изменённым названием функции
Файл Player.jsx с изменённым названием функции

А из функции initSwayingObjectAnimation уберём константу easing, т.к. ранее мы вынесли её в корень файла.

Убранная константа easing
Убранная константа easing

Перейдём к исправлению файла Weapon.jsx

Изменим значение для recoilDuration на 50.

Изменённое значение recoilDuration
Изменённое значение recoilDuration

Удалим recoilBackAnimation за ненадобностью. Вместо этого теперь добавим isRecoilAnimationFinished.

Добавленное состояние isRecoilAnimationFinished
Добавленное состояние isRecoilAnimationFinished

Для функции generateNewPositionOfRecoil добавим значение по умолчанию для параметра currentPosition.

Значение по умолчанию для функции generateNewPositionOfRecoil
Значение по умолчанию для функции generateNewPositionOfRecoil

В функции initRecoilAnimation удалим константу initialPosition за ненадобностью.

Убрана переменная initialPosition
Убрана переменная initialPosition

Для Tween-анимации переработаем логику, сделав её автоматически-возвратной к исходной позиции, из которой началось выполнение анимации. Также удалим возвратную анимацию twRecoilBackAnimation.

Изменённая функция initRecoilAnimation
Изменённая функция initRecoilAnimation

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

Изменённая логика useEffect
Изменённая логика useEffect

Код раздела

Анимация вспышки при выстреле

Теперь займёмся реализацией отображения вспышки при выстреле из оружия. 

В файле Weapon.jsx импортируем функцию useLoader. А также загрузим изображение вспышки в assets/images.

Изображение вспышки при выстреле
Изображение вспышки при выстреле

В компоненте импортируем данное изображение как FlashShoot.

Импорт изображения
Импорт изображения

Далее воспользуемся функцией useLoader для загрузки данного изображение на сцену. А также добавим состояние для сохранения анимации flashAnimation.

Импорт useLoader
Импорт useLoader
Загрузка изображения на сцену и состояние анимации вспышки
Загрузка изображения на сцену и состояние анимации вспышки

Создадим новое состояние flashOpacity для плавного изменения прозрачности вспышки. Также создадим новую функцию initFlashAnimation, в которой опишем последовательность анимации. И после этого воспользуемся useEffect для инициализации анимации.

Реализация анимации вспышки при выстреле
Реализация анимации вспышки при выстреле

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

Добавление изображения на сцену
Добавление изображения на сцену

И в конце добавим вызов анимации при каждом выстреле в функции startShooting.

Доработанная функция startShooting
Доработанная функция startShooting
Анимация вспышки при выстреле
Анимация вспышки при выстреле

Код раздела

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

Добавим подготовленный звуковой файл в папку assets/sounds

Импортируем его в файл.

Импорт аудиофайла
Импорт аудиофайла

При помощи HTMLAudioElement мы можем добавить звуковой файл для текущей страницы (не для сцены).

Добавление аудио в документ
Добавление аудио в документ

При вызове функции startShooting запускаем данный аудиофайл. При нескольких выстрелах запускаемые аудиофайлы будут запускаться и накладываться друг на друга. И в конце просто прекращать воспроизводиться.

Проигрывание аудио в функции startShooting
Проигрывание аудио в функции startShooting

Код раздела

Заключение

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

Спасибо за прочтение и буду рад ответить на комментарии!

Комментарии (7)


  1. Vlaek
    15.11.2023 13:24
    +1

    Спасибо большое за гайд. Буквально несколько часов назад начал изучать Three.js и наткнулся на первую часть руководства, а тут час назад вторая вышла. Даже зарегался, чтобы поблагодарить)


    1. varlab Автор
      15.11.2023 13:24

      Буду очень рад, если мои статьи окажутся полезными)


  1. 19Zb84
    15.11.2023 13:24

    А сколько fps выдает игра ?


    1. varlab Автор
      15.11.2023 13:24
      +1

      Так как в данный момент в приложении имеется лишь несколько геометрических фигур и одна модель оружия, то fps, я думаю, будет максимальный везде. На моём мониторе 144Гц выдаёт 143.9 кадра в секунду.


  1. JerryI
    15.11.2023 13:24

    Неплохо, но вот код картинками - ужасно :( Я бы за такое по карме бил

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

    Если так хочется показывать изменения можно как в git
    >>>>

    <<<<

    UPD: Ну вот автор и такие публикации делает https://habr.com/ru/articles/762546/ и там все ок. Зачем здесь то так? не уж то так лень нормально оформить?


    1. varlab Автор
      15.11.2023 13:24
      +1

      Спасибо за Ваше мнение! Мне показалось это наиболее оптимальным и понятным способом отображения изменений в коде. Буду думать и работать над разными вариантами отображения


  1. AlexanderKudryavy
    15.11.2023 13:24
    +1

    Было интересно, лови +