React + Three.js. Создаём собственный 3D шутер. Часть 2

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

Введение

В современной веб-разработке границы между классическими и веб-приложениями стираются с каждым днём. Сегодня мы можем создавать не только интерактивные сайты, но и полноценные игры прямо в браузере. Одним из инструментов, который делает это возможным, является библиотека 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

Код раздела

Заключение

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

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

© Habrahabr.ru