Меня зовут Илья Проскуряков, я — iOS-разработчик компании Effective[ссылка удалена мод.] и в статье расскажу о разработке игр под Apple Vision Pro.
Мы с коллегами разработали две мини-игры в рамках хакатона Ludum Dare в Омске, а затем я сам немного поработал с Apple Vision Pro. Теперь хочу поделиться опытом с примерами и кодом, рассказать о плюсах и минусах Apple Vision Pro с точки зрения разработчика, и в целом, с какими сложностями столкнулся и как их решал.
В марте технический директор Effective Алексей Коровянский съездил в США, купил и привез на родину эту новую приблуду. У меня почти не было опыта взаимодействия с дополненной реальностью. Сначала я примерил очки как пользователь, а затем заинтересовался ими как iOS-разработчик, и мне захотелось разработать что-то для них самому. Так совпало, что 13-14 апреля в Омске проходил двухдневный хакатон по разработке игр Ludum Dare, и мы с двумя коллегами решили в него вписаться.
Мини-справка:
Apple Vision Pro — гарнитура дополненной и виртуальной реальности на базе чипа М2. Ее анонсировали в 2023 году и выпустили 2 февраля 2024 года. Сейчас гарнитура стоит 3,5 тысячи долларов на американском рынке, а на российском — в два раза дороже.
![Сейчас гарнитура стоит 3,5 тысячи долларов на американском рынке, а на российском — в два раза дороже. Сейчас гарнитура стоит 3,5 тысячи долларов на американском рынке, а на российском — в два раза дороже.](https://habrastorage.org/getpro/habr/upload_files/71c/14f/2cb/71c14f2cb2787837c47e71824c47320c.jpeg)
Что мы делали на хакатоне
Мы узнали о хакатоне за полторы недели, начали перебирать идеи и поняли, что хотим сделать нечто вроде ОСУ для глаз. ОСУ — это игра, в которой пользователь должен успеть кликнуть мышкой по простой движущейся цели. Поскольку Apple Vision Pro умеет отслеживать движение глаз, в нашей игре вместо управления мышкой должно было быть управление глазами. Также у хакатона была тема, которой должны были соответствовать все проекты — summoning (призыв). Мы узнали, что один из участников делает игру, в которой надо звать кота, чтобы его покормить, вдохновились и решили продолжить тему котов в двух мини-играх под Apple Vision Pro.
Чтобы начать первую игру, пользователь надевает гарнитуру и видит в своей руке пылесос. В пространстве вокруг пользователя появляются манулы, он засасывает их пылесосом, и для мемности происходящего мы добавили Google-голос, который подсчитывает котов. Когда счет доходит до 10, появляется огромный манул и выражает недовольство — изначально он должен был выходить из портала, но у нас не хватило времени и в итоге он просто спавнился рядом с пользователем.
Во второй игре пользователю нужно задобрить большого манула. В пространстве вокруг него летают бургеры и помидоры, а он ловит бургеры специальным жестом. Помидоры ловить нельзя, иначе манул разозлится и съест пользователя.
Все это должно было быть двумя этапами одной игры, но мы не успели соединить их в один сценарий и сделали две отдельные мини-игры, которые можно запустить из меню.
Писали все на Swift, использовали фреймворки Swift UI, ARKit и RealityKit.
ARKit помогает отслеживать все происходящее вокруг: движения рук пользователя, разные плоскости и весь мир в целом.
RealityKit позволяет рендерить 3D-объекты и взаимодействовать с их физикой, геометрией и прочим.
По сути, ARKit — некое подспорье под RealityKit на Vision OS.
Начинаем с меню
Дизайн не фонтан, но примерно так выглядит любой 2D-экран под Vision OS. По умолчанию экран можно ресайзить или перемещать: например, если расположить его в комнате, выйти из нее и вернуться, экран останется на месте.
![](https://habrastorage.org/getpro/habr/upload_files/8e9/7f6/9eb/8e97f69eb284efdc32a594c1232d0fb6.jpeg)
Так меню выглядит в коде:
![](https://habrastorage.org/getpro/habr/upload_files/8da/50d/40f/8da50d40f69d4718c82096b3bcc04d2f.jpeg)
По факту, это просто фреймворк Swift UI. Разработчик кодит в нем под Vision OS так же, как если бы писал под iOS. Здесь такие же VStack, модификаторы, паддинги, спейсеры и прочие вещи. Ради интереса я даже запустил этот код на iOS, и у меня ничего не сломалось. Так что можно написать одинаковый код под разные платформы, но функционально получить один и тот же результат.
![Меню игры на iPhone и на Apple Vision Pro Меню игры на iPhone и на Apple Vision Pro](https://habrastorage.org/getpro/habr/upload_files/55b/29c/a79/55b29ca790c059a5c1b6b60337ff40b5.jpeg)
Создаем пространство для дополненной реальности
В первую очередь, нужно объявить ImmersiveSpace — то есть дополненное пространство, и задать ему ID, как показано на левом скриншоте внизу.
![](https://habrastorage.org/getpro/habr/upload_files/f6b/b67/119/f6bb67119bdd8cc48fe6578145370674.jpeg)
По моим наблюдениям, на все приложения в VisionOS может быть открыто только одно ImmersiveSpace единовременно (в приложении точно только одно). Существуют environment-переменные — openImmersiveSpace и dismissImmersiveSpace, которые соответственно открывают и закрывают пространство. В openImmersiveSpace вы передаете нужный ID, соответствующий пространству, которое необходимо открыть, и вызываете все это дело через оператор await.
Наполняем пространство контентом
В closure ImmersiveSpace мы видим immersiveView — стандартную вьюшку Swift UI, внутри которой лежит объект realityView. У realityView в closure есть content — inout-параметр типа RealityViewContent, в который нужно класть ваши 3D-модели, а также attachments — 2D-вьюшки, которые прикрепляются к 3D-объектам. Например, в attachments может быть счетчик всосанных манулов на ручке пылесоса. И еще здесь важна функция update, которая вызывается на изменение кадра и позволяет изменять пространство с течением времени.
![](https://habrastorage.org/getpro/habr/upload_files/5d8/5d3/e8e/5d85d3e8e6670c1b936b08384d0f5bd6.jpeg)
RealityViewContent — это структура. Для работы придется каждый раз передавать ее по ссылке и это неудобно, поэтому дальше я расскажу, как упростить себе жизнь.
В realityKit есть classEntity, который выполняет похожие функции по заполнению пространства контентом. Схема выглядит так: нужно создать корневую пустую 3D-вьюшку rootEntity, положить ее в контент с помощью метода add(content.add(rootEntity)), а затем положить в rootEntity не пустые Entity, которые будут содержать ваши 3D-модели.
![](https://habrastorage.org/getpro/habr/upload_files/0bc/00a/27f/0bc00a27f9d49b74dd2a565c16165768.jpeg)
У Apple есть инструмент Reality Composer Pro, созданный чтобы упростить подготовку 3D-контента для приложений под VisionOS. Он напоминает редактор сцен Unity.
![](https://habrastorage.org/getpro/habr/upload_files/a56/dac/62a/a56dac62ac10b5dc3d0f02799b0478ea.jpeg)
Сначала вам понадобятся объекты, которые вы хотите отобразить — 3D-модели в формате USD. Здесь мы столкнулись с одной из основных проблем при разработке игр под VisionOS: с поиском 3D-моделей. У Apple есть набор бесплатных ассетов, и, хоть выбор невелик, оттуда можно что-то достать. Можно купить 3D-модель.Это дорого, цена варьируется от 2$ до 2000$ в зависимости от качества и функционала модели. Можно найти 3D-дизайнера или сделать модель самому. Самый доступный вариант — найти бесплатные ассеты на сайтах, в группах или Telegram-чатах.
Когда вы нашли нужные 3D-модели — например, Земли и Луны, вам нужно импортировать их в сцену. Просто перетащите их либо на панель слева, либо прямо в пространство. Затем их надо расположить на сцене. Положение объекта задается координатами на трех осях — x, y, z. Чтобы понять, куда смотрит каждая из координат, сделайте такой жест: большой палец — ось x, указательный — ось y, и средний — ось z.
![](https://habrastorage.org/getpro/habr/upload_files/613/70b/c86/61370bc867a69b754008a5741e6142b3.png)
Я не знал об этом жесте, поэтому тыкал позиции наугад, запускал проект и проверял, все ли ок. Видимо, из-за поворота портала, который изначально был горизонтальным, система координат повернулась на 90 градусов от меня. Спустя кучу попыток я нашел позиции объектов, которые меня удовлетворили.
Также в Reality Composer Pro можно менять размеры 3D-моделей, вращать и разворачивать их. Можно добавлять к объекту компоненты: освещение, тени, коллизии, физику, звуки. Например, если вы хотите, чтобы где-то летала и чирикала птица, вы можете добавить ей звук и он будет идти прямо от нее.
Создаем мир внутри портала
Чтобы сделать портал, нужно создать:
Мир, который будет отображаться внутри портала,
Сущность портала — черный кругляш,
Якорь — сущность, к которой прикрепляются все объекты. Якорем могут быть руки пользователя, стены помещения, пол, столы и прочие вещи.
Разработчик кладет в портал мир, сам портал кладет в якорь, а потом все три сущности кладет в контент.
Чтобы сделать мир, нужно создать сущность и задать ей соответствующее свойство. За него отвечает компонент, который называется World Component. Он отделяет от внешнего мира все, что лежит внутри портала, поэтому благодаря нему контент будет лежать именно в портале.
Теперь нужно заполнить все это дело контентом. Здесь вы видите функцию load, в котором мы загружаем ассет Solar System, который мы настроили ранее в Reality Composer Pro.
![](https://habrastorage.org/getpro/habr/upload_files/74f/8ac/1f7/74f8ac1f726ccc11586ac2aeaa16eda3.jpeg)
Создаем портал
Скриншот слева — это портал, который сделали Apple. Под видео даже есть секция «Code», но я пробовал и этот код не работал. В итоге мы справились сами, и наш результат — справа.
![](https://habrastorage.org/getpro/habr/upload_files/16d/0fc/3e1/16d0fc3e1fd20b5945290c74fe907f4b.jpeg)
Чтобы создать портал, нужно сделать сущность и настроить у нее Portal Component, в который мы поместим мир, и Model Component — внешний вид этой сущности, то есть черный кругляш. Этого будет достаточно для его работы.
Добавляем спецэффекты
Мы хотели сделать не простой, а красивый портал, поэтому на строчке 65 подгрузили ассет Particles — это частички, с помощью которых можно создавать различные эффекты.
![](https://habrastorage.org/getpro/habr/upload_files/d64/dcd/c2e/d64dcdc2ea0d343eda2d10394fe670dd.jpeg)
Возвращаемся в Reality Composer Pro и добавляем Particles. Здесь есть различные ассеты — например, фейерверки и туманы. Можно настроить, как часто будут пульсировать частички, их количество, форму и цвет.
![](https://habrastorage.org/getpro/habr/upload_files/51e/073/f84/51e073f84cf5963e1df9bb1e3ac22742.png)
Я час ковырял этот инструмент и получил такое:
![](https://habrastorage.org/getpro/habr/upload_files/1db/cb0/9ce/1dbcb09ce6367c8668c9dd18619935a4.gif)
На мой взгляд, неплохо. Хотя бы портал напоминает — уже хорошо. Дальше нужно добавить эту штуку в портал, и он будет сиять.
Прикрепляем пылесос к руке
Чтобы прикрепить пылесос к руке, сначала нужно загрузить ассеты. Пылесос должен взаимодействовать с крутящимися котами. Для этого ему нужно настроить коллизии через Collision-компоненту, маску и группу. Маска отвечает за то, с какими группами будет взаимодействовать пылесос, а группа — за то, к какой группе относится объект. Collision-компонента задается битовой маской. Внизу вы видите Collision Group, передаем туда битовую операцию и получается битовая маска.
![](https://habrastorage.org/getpro/habr/upload_files/201/3ac/508/2013ac508ac44c9a9be68620ea32b5a0.jpeg)
Затем нужно прикрепить пылесос к руке. Для этого сначала нужно научиться следить за руками пользователя. Вспоминаем про ARKit и создаем сессию ARKit session.
![](https://habrastorage.org/getpro/habr/upload_files/585/bc5/a40/585bc5a40b1d156baf58c81136318a55.jpeg)
У сессии есть метод run, который в качестве параметра принимает массив DataProvider’ов . В данном случае нужен Hand Tracker Provider, который следит за руками. Дальше можно отслеживать состояние переменной handTracking. Берем оттуда правую руку и получаем ее якорь с параметром originFromAnchorTransform — локацию руки относительно мира. Затем мы присваиваем эту локацию ручке пылесоса. Также через метод Look нужно настроить, куда будет смотреть эта ручка — потому что позицию объекта мы задаем через поле position, а метод look настраивает то, куда будет направлен объект.
Крутим манулов
Мы создали собственный кастомный компонент — структуру, которая будет конформить протокол Component. Задали в нем нужные поля и зарегистрировали компонент: один раз где-нибудь вызвали функцию Register Component.
Теперь нужна система, которая умеет взаимодействовать с этим компонентом. Для этого мы создали класс, законформили протокол System, и у него появилась возможность переопределить метод Update. Update вызывается каждый раз на обновление фрейма, а частота его вызова зависит от частоты обновления кадров в Vision OS. На очках примерно 90 Гц, соответственно она будет обновляться 90 раз в секунду. Нужно найти сущность, которая соответствует определенному параметру — в данном случае, имеет компонент Rotate Component, изменить ее параметр orientation, и тогда объект будет крутиться.
![](https://habrastorage.org/getpro/habr/upload_files/7a2/17b/9ce/7a217b9ce25b4303faa8fe09fe759c4e.jpeg)
![](https://habrastorage.org/getpro/habr/upload_files/32c/6f9/856/32c6f98565005d4b6a39fda213767760.jpeg)
Бургеры и помидоры
С точки зрения кода вторая мини-игра проще игры про манулов. Чтобы наполнить мир вокруг пользователя бургерами и помидорами, надо было вызвать функции Add Burger или Add Tomatoes столько раз, сколько бургеров или помидоров мы хотим.
Функция Add Burger простая — грузим ассет с моделью бургера и добавляем компоненты:
Input target component позволяет взаимодействовать с пользователем.
Hover эффект — чтобы объект, на который смотрит пользователь, выделялся среди других объектов. Наподобие кнопки, на которую наведен курсор мышки.
Также объекту нужно задать позицию. Ее можно сгенерировать рандомно: внизу справа видно функцию, которая возвращает структуру SIMD3, которая отвечает за координатную сетку. В ней мы и генерируем рандомные значения.
![](https://habrastorage.org/getpro/habr/upload_files/fb8/e43/a4d/fb8e43a4d8063efb5f11e1c5745dedf6.jpeg)
Затем нужно задать стандартный жест для Apple Vision Pro, при котором указательный и большой палец касаются друг друга. Это можно легко сделать через SpatialTapGesture(), модификатор .targetedToAnyEntity() позволяет этому жесту взаимодействовать с любыми объектами, которые мы положили в Immersive View.
Включаем музыку
Мы сгенерировали десять ассетов Google-голосом. Когда пылесос засасывает манулов, Google-голос ведет подсчет. По сути, разработчику нужно заполнить массив аудиофайловых ресурсов через функцию load(), потом на коллизии пылесоса и кота вызвать у сущности функцию playAudio() и передать нужный ассет. Фоновую музыку можно задать стандартно через AVAudioPlayer.
![](https://habrastorage.org/getpro/habr/upload_files/197/2c6/c3e/1972c6c3edb941d1e75dcffdb8b8338d.jpeg)
Все это мы с коллегами успели сделать на хакатоне за одну продуктивную ночь, а дальше я расскажу, что делал один.
После хакатона
У меня было время, чтобы поработать с Apple Vision Pro в спокойной обстановке. Поскольку в мини-играх на хакатоне мы не взаимодействовали с физикой, я решил делать что-то вроде тенниса.
Настраиваем физику
Сначала мне нужно было понять, как работает физика у объектов. Для этого я подгрузил соответствующий ассет и добавил ему PhysicsMotionComponent, который отвечает за движения объектов. Затем я добавил ему коллизии, физическое тело, которое отвечает за центр массы, коэффициент упругости, коэффициент трения и прочие вещи. Это можно сделать как в Reality Composer Pro, так и в коде.
![](https://habrastorage.org/getpro/habr/upload_files/a3a/1a0/167/a3a1a01672fae93af4e557a49a875ea4.jpeg)
У меня получился виртуальный объект, который не умел взаимодействовать с реальным миром. Если бы я просто загрузил его, он бы упал и бесконечно летел сквозь стены и потолки. Нужно было научиться отслеживать реальность вокруг.
Возвращаемся к ARKit и его сессии. В качестве дата-провайдера мне нужно было отдать ему SceneReconstructionProvider, который отвечает за отслеживание всех поверхностей вокруг. Я закинул его в метод Run, чтобы на апдейты sceneReconstruction пришли якоря стен, полов и прочих вещей, у которых были бы нужные параметры. По ним можно было воссоздать форму этого якоря, например стены.
Затем я создал сущность, в которую положил компонент CollisionComponent. Он делается на основе формы, созданной выше. Задал физическое тело и расположение (transform) через расположение пришедшего якоря. По сути, мы не можем напрямую взаимодействовать с реальными объектами, но можем отследить их форму, расположение и создать виртуальные копии, с которыми смогут взаимодействовать наши виртуальные объекты.
![](https://habrastorage.org/getpro/habr/upload_files/0fa/d89/bbc/0fad89bbcaab6d351b5069b9cb374c24.jpeg)
Кстати, чтобы уметь все это отслеживать, нужно получить разрешение от пользователя, для чего надо создать пару ключей с описанием для чего мы получаем это разрешение в info.plist.
Пытаемся схватить мячик
Мне нужно было схватить мячик и ракетку, а затем ударить одним о другое. Сначала я думал прикрепить ракетку к руке как к якорю, как мы это делали с пылесосом, а мячик двигать с помощью SpatialTapGesture. Но это было слишком просто — хотелось, как в реальности.
Я узнал, что могу отслеживать не только руку, но и каждый сустав этой руки. Как это сделать? Создать словарь, ключом которого будет сустав. А значением по ключу будет 3D-сущность, у которой будет физический компонент и компонент CollisionShape. Таким образом я прикрепил 3D-сущность к пальцу и учил его взаимодействовать со всем вокруг. В том же методе, где я следил за руками, можно следить за пальцами, тогда будут две координаты — руки относительно мира и суставы относительно руки. Перемножив эти, по сути, матрицы, получаем координату сустава относительно мира. А дальше вносим полученное изменение в словарь, упомянутый выше.
![](https://habrastorage.org/getpro/habr/upload_files/565/922/368/5659223682bd9d1e5f2a5b859ec7b191.jpeg)
Я думал, что этого хватит, но в итоге мячик превратился в скользкое мыло, выскальзывал из пальцев и не хотел прикрепляться к руке. Позже мне подсказали, что такие коллизии не предназначены для подобных взаимодействий.
![](https://habrastorage.org/getpro/habr/upload_files/cb9/95f/d13/cb995fd133e6507c4f7abd63585244c0.gif)
Поэтому нужно искать другой подход, например, распознать жест и после уже прикрепить мячик к руке.
Делаем распознавание жеста захвата
У Apple есть отличный пример — игра Happy Beam. В ней на пользователя летят грустные тучки, а он жестами в форме сердечек отправляет им лучи добра, чтобы они превратились в счастливые белые облачка.
Я посмотрел, как они отслеживают жест и сделал собственный метод. Вот как он работает: я точно так же отслеживаю суставы рук и их локацию, а дальше беру большой палец, указательный и безымянный и делаю так, чтобы расстояние между ними было меньше шести сантиметров — подобрал на глаз. Это и будет напоминать жест захвата. Вы можете придумать собственную интерпретацию жеста, но моя, в целом, работает.
![](https://habrastorage.org/getpro/habr/upload_files/ba4/136/eb5/ba4136eb55744ded9d11232c7b7d324c.jpeg)
Кроме определения жеста нужно было сделать так, чтобы он срабатывал только в момент коллизии с нужным объектом. То есть чтобы пользователь дотрагивался до мячика, делал жест, и только тогда мячик крепился к его руке. Для этого у content, который передается в кложуре RealityView, я вызвал метод subscribe() и подписался на все коллизии, происходящие в приложении.
На эту подписку я вызвал функцию, в которую передавал, был или не был жест, и смотрел в самой функции, какие именно объекты провзаимодействовали, потому что из всех коллизий всех объектов мне нужно было отследить конкретные. В итоге все выглядело так: происходила какая-то коллизия, я проверял, был ли в этот момент жест и те ли объекты — рука и мяч, провзаимодействовали. Если все совпадало, мяч крепился к основанию руки.
В результате скользкое мыло превратилось в слайм, который не отлеплялся от руки. К сожалению, это все, что я успел сделать. Тем не менее, я не планирую останавливаться и буду пытаться дальше!
![](https://habrastorage.org/getpro/habr/upload_files/17f/ae1/239/17fae12393404643e7b7733a58c2b99b.gif)
Недостатки Apple Vision Pro с точки зрения разработчика
Непросто найти 3D-модели объектов для дополненной реальности
Какие есть варианты:
Найти бесплатные модели в Reality Composer Pro, на TurboSqiud или в Telegram-каналах — там их немного, но что-то можно подобрать.
Купить 3D-модели, но в зависимости от их качества стоимость может варьироваться от двух долларов до двух тысяч долларов.
Попробовать сделать 3D-модели самостоятельно или найти 3D-дизайнера.
Сложности в тестировании разрабатываемого ПО
На хакатоне у нас была одна гарнитура Apple Vision Pro на троих, а наших манулов нужно тестировать. Симулятор неполноценен: да, в нем можно смотреть, как будут выглядеть объекты внутри, крутить их, вертеть и подобное. Однако проблема симулятора в том, что в нем невозможно отслеживать реальный мир. Если запустить в симуляторе приложение, которое пытается это сделать, оно крашнет. Поэтому если продукт разрабатывает команда из нескольких человек, его сможет тестировать по полной только тот, кто физически находится рядом с Apple Vision Pro.
Непопулярная технология
О разработке под Apple Vision Pro в интернете еще совсем немного информации и мало примеров. Есть, конечно, документация от Apple, но в силу неопытности в 3D-разработке можно прочитать о функции или параметре и все равно не понять, что с этим делать.
Оба фреймворка — и ARKit, и RealityKit находятся в бете. Я взял код из документации Apple, использовал его и оказалось, что он не работает. Пошел на форумы и обнаружил, что Apple переделали эту функцию, но не успели обновить документацию.
С другой стороны можно рассмотреть это как плюс, потратить больше времени, разобраться самому и стать первопроходцем.
Преимущества Apple Vision Pro с точки зрения разработчика
Это интересно, потому что разработка под дополненную реальность — кайф.
Декларативный подход. Многие функции, сложные с инженерной точки зрения, вроде загрузки 3D-объекта и расположения его в дополненной реальности, можно сделать за несколько строк кода.
Практический опыт 3D-разработки — разбираешься, как работать с физикой, как располагать объекты в 3D-пространстве и прочее.
За месяц экспериментов с Apple Vision Pro я прошелся только по верхам. Что еще стоит попробовать:
Shader Graph в Reality Composer Pro. В нем можно строить графы и создавать красивые эффекты и объекты с помощью узлов.
Приложения с полностью виртуальной реальностью Full Immersive с помощью фреймворка Metal. Это фреймворк, который позволяет взаимодействовать с графическим процессором и рисовать красивые вещи.
Разработку на Unity под Vision OS.
Я буду продолжать эксперименты с Apple Vision Pro и выкладывать их в наш Telegram-канал @effectiveband. Кстати, в нем уже лежит множество материалов по iOS-, Android-, Flutter-, Web-разработке и архитектуре решений.
Буду рад вашим комментариям!
Bardakan
здравствуйте
а есть исходники? С картинок читать не удобно, да и текст обрезан