Игра в браузере на React и Three.js!
Я занимаюсь фронтендом уже очень давно, порядка 10 лет. И как любой уважающий себя фронтендер, я люблю тащить javascript туда, где обычно его не используют: на сервер, в мобильные приложения, в геймдев. С тех пор как я увидел первые WebGL демосцены в 2013-м, я мечтал сделать что-то похожее, скажем, на это.
Так что я провел немало времени экспериментируя и читая документацию, и вот что у меня получилось.
Дисклеймер: я не претендую на абсолютные знания того, как делают игры, я всего лишь описываю свой наивный подход к поиску ответа на вопрос, который задает заголовок. Поэтому это будет не совсем игра, а скорее концепт, интерактивная демо-сцена.
Дизайн
В рамках эксперимента я создам игру с изометрическим видом, в которой герой будет убивать зомби. Каждый раз, когда герой кастует файрбол, на карте в случайном месте появляется новый враг. Цель игры - как можно дольше остаться в живых. Управление будет с помощью геймпада.
Шаг 1. Настройка проекта
Как и в любом другом JS-проекте, в первую очередь придется выполнить настройку. Я ненавижу долгие настройки, полные магических операций, поэтому буду пользоваться Parcel, так как он придерживается zero-config концепции. В качестве рендер-движка я буду использовать Three.js и react-three-fiber, который склеит React и Three.js и освободит от императивности последней.
В первую очередь, я создам папку под проект и выполню следующее:
npm init -y
npm install parcel-bundler react react-dom three react-three-fiber
npm install -D @babel/core @babel/preset-react parcel-plugin-static-files-copy
Так же понадобится библиотека с крайне полезными утилитами для Three - drei.
npm install @react-three/drei
![](https://habrastorage.org/getpro/habr/upload_files/c1d/1a4/c79/c1d1a4c792bfa2590d691be8d2ad30a6.jpg)
А так же следующая строчка в разделе scripts package.json:
"start": "parcel ./index.html"
А еще index.html со следующим содержимым.
Теперь после выполнения команды npm start можно перейти по адресу http://localhost:1234 и приступить к делу.
Шаг 2. Базовые вещи
Позвольте мне пропустить подробности того, как сделан react-three-fiber и как он работает, в сети довольно много информации по этой теме. Например, в официальной документации.
Прежде всего, в игре должна быть земля, поверхность, чтобы все не выглядело так, словно персонажи висят в воздухе. В приведенном ниже примере я создал базовую сцену с компонентами:
Canvas. Самый главный компонент, который делает за вас много работы: запускает игровой цикл, следит за всем, является контекстом. Большинство настроек сцены будут в нем.
axesHelper. Компонент, который добавляет в сцену оси (хотя, конечно, не очень полезный, потому что ось Y направлена вверх, хотя я ожидал, что вверх будет направлена ось Z).
mesh. Поверхность. В такой компонент нужно прокинуть два потомка: описание геометрии поверхности и ее материала.
pointLight. Источник света. Без него вы не увидите ничего.
Тут должен был быть эмбед codesandbox, но что-то пошло не так: https://codesandbox.io/embed/sleepy-wu-4vt60
Шаг 3. Движение камеры
Сейчас вся сцена статична и не очень впечатляюща, поэтому я воспользуюсь вспомогательной сущностью three.js - OrbitControls, которая даст возможность манипулировать камерой с помощью мыши. Если зажать левую клавишу, то камера будет вращаться вокруг фиксированной точки и всегда будет обращена к ней. Колесико будет регулировать степень приближения. Кроме того, компонент принимает свойства, которые определяют ограничения, то есть, например, не позволят камере быть выше или ниже определенных координат.
Шаг 4. Файрбол
Ок, пришло время перейти к более интересным и сложным вещам. К созданию файрбола. К сожалению, я не умею писать шейдеры, поэтому взял и адаптировал уже готовый код из подходящей демки.
Идея проста как дважды два. Всего три компонента: сфера, цилиндр для огня и еще один, большего размера, для дыма. Все компоненты имеют прозрачный shaderMaterial, который обновляется в каждом новом кадре. В итоге все выглядит, как будто он состоит из пылающей лавы.
![](https://habrastorage.org/getpro/habr/upload_files/302/280/530/302280530c595023e5ce28f0755bf928.png)
Вот тут лежит полный код файрбола, а пока я расскажу о некоторых аспектах.
Файл /src/fireball/fireball.jsx
экспортирует единственный компонент, который, в свою очередь, состоит из трех других, завернутых в группу. Почему? Потому что это позволяет применять трансформации на всех потомках, сохраняя их относительное местоположение и соотношения.
![](https://habrastorage.org/getpro/habr/upload_files/49f/b5b/3b0/49fb5b3b0ca35c9eac31726495b6818d.png)
В каждом подкомпоненте есть следующие строки:
![](https://habrastorage.org/getpro/habr/upload_files/4fc/464/69b/4fc46469b2abb0675bd5773d62c0d340.png)
useFrame
вызывается каждый раз, когда срабатывает requestAnimationFrame
- в зависимости от вашего оборудования 60 или, например, 144 раза в секунду.
Наконец, если поместить компонент в сцену и прокинуть в него свойства position и scale, то выглядеть это будет следующим образом:
Тут должен был быть эмбед codesandbox, но что-то пошло не так: https://codesandbox.io/embed/exciting-marco-iyy6h
Шаг 5. Персонажи
Где взять модели персонажей? Отличный вопрос. К примеру, в mannequin.js или mixamo. Можно даже купить на sketchfab (и использовать mixamo для создания скелета в нем). В целях эксперимента я буду использовать mixamo, там есть подходящие модели и анимации.
При скачивании модели стоит выбрать формат FBX.
Файл .fbx может весить до 15 МБ (27 тысяч полигонов!), и не хочется, чтобы они были частью бандла. Так что будем грузить их через ajax тогда, когда они понадобятся.
parcel-plugin-static-files-copy сделает все необходимое: возьмет статику из папки /assets/models
в /dist
. Для этого нужно добавить следующее в package.json:
"staticFiles": { "staticPath": "assets"}
В целях отображения модели персонажа я создал два новых файла: getModel.js и NPC.jsx. Первый экспортирует хук, который ответственен за асинхронную загрузку файлов моделей и включение теней, второй использует этот хук и контролирует анимации. Все довольно просто. Кстати, анимации уже включены в состав FBX файла, но не будем же мы каждый раз из заново парсить. Проще сохранить их где-то в виде json-а и использовать на разных моделях (в этом универсальное преимущество mixamo: все модели имеют скелет с одинаково названными костями, так что можно взять описание анимации одного персонажа и сыграть на другом).
Animation clip в three.js - это просто json файл с описанием того, как будет двигаться каждая кость в скелете. Так что их можно просто взять и добавить в специальный объект AnimationMixer. AnimationMixer, ну, смешивает анимации ¯\_(ツ)_/¯. Но здесь я буду использовать его для переключения между различными состояниями.
Спасибо, что дочитали до конца! В следующей части я опишу, как использовать геймпад, заставить модели двигаться и как сюда вписывается state management.
Полезные ресурсы
https://codepen.io/pizza3/pen/Rwoqemx?editors=0010 — Демка файрбола
https://ykob.github.io/sketch-threejs/sketch/fire_ball_2.html — Еще одна
https://docs.pmnd.rs/react-three-fiber/getting-started/introduction— Документация по react-three-fiber
https://www.mixamo.com/ — Каталог 3D-моделей и штука для авторига
https://boytchev.github.io/mannequin.js/ — Любопытная библиотека для генерации моделей персонажей