Меня зовут Илья Проскуряков, я — 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 тысячи долларов на американском рынке, а на российском — в два раза дороже.
Что мы делали на хакатоне
Мы узнали о хакатоне за полторы недели, начали перебирать идеи и поняли, что хотим сделать нечто вроде ОСУ для глаз. ОСУ — это игра, в которой пользователь должен успеть кликнуть мышкой по простой движущейся цели. Поскольку Apple Vision Pro умеет отслеживать движение глаз, в нашей игре вместо управления мышкой должно было быть управление глазами. Также у хакатона была тема, которой должны были соответствовать все проекты — summoning (призыв). Мы узнали, что один из участников делает игру, в которой надо звать кота, чтобы его покормить, вдохновились и решили продолжить тему котов в двух мини-играх под Apple Vision Pro.
Чтобы начать первую игру, пользователь надевает гарнитуру и видит в своей руке пылесос. В пространстве вокруг пользователя появляются манулы, он засасывает их пылесосом, и для мемности происходящего мы добавили Google-голос, который подсчитывает котов. Когда счет доходит до 10, появляется огромный манул и выражает недовольство — изначально он должен был выходить из портала, но у нас не хватило времени и в итоге он просто спавнился рядом с пользователем.
Во второй игре пользователю нужно задобрить большого манула. В пространстве вокруг него летают бургеры и помидоры, а он ловит бургеры специальным жестом. Помидоры ловить нельзя, иначе манул разозлится и съест пользователя.
Все это должно было быть двумя этапами одной игры, но мы не успели соединить их в один сценарий и сделали две отдельные мини-игры, которые можно запустить из меню.
Писали все на Swift, использовали фреймворки Swift UI, ARKit и RealityKit.
ARKit помогает отслеживать все происходящее вокруг: движения рук пользователя, разные плоскости и весь мир в целом.
RealityKit позволяет рендерить 3D-объекты и взаимодействовать с их физикой, геометрией и прочим.
По сути, ARKit — некое подспорье под RealityKit на Vision OS.
Начинаем с меню
Дизайн не фонтан, но примерно так выглядит любой 2D-экран под Vision OS. По умолчанию экран можно ресайзить или перемещать: например, если расположить его в комнате, выйти из нее и вернуться, экран останется на месте.
Так меню выглядит в коде:
По факту, это просто фреймворк Swift UI. Разработчик кодит в нем под Vision OS так же, как если бы писал под iOS. Здесь такие же VStack, модификаторы, паддинги, спейсеры и прочие вещи. Ради интереса я даже запустил этот код на iOS, и у меня ничего не сломалось. Так что можно написать одинаковый код под разные платформы, но функционально получить один и тот же результат.
Создаем пространство для дополненной реальности
В первую очередь, нужно объявить ImmersiveSpace — то есть дополненное пространство, и задать ему ID, как показано на левом скриншоте внизу.
По моим наблюдениям, на все приложения в 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, которая вызывается на изменение кадра и позволяет изменять пространство с течением времени.
RealityViewContent — это структура. Для работы придется каждый раз передавать ее по ссылке и это неудобно, поэтому дальше я расскажу, как упростить себе жизнь.
В realityKit есть classEntity, который выполняет похожие функции по заполнению пространства контентом. Схема выглядит так: нужно создать корневую пустую 3D-вьюшку rootEntity, положить ее в контент с помощью метода add(content.add(rootEntity)), а затем положить в rootEntity не пустые Entity, которые будут содержать ваши 3D-модели.
У Apple есть инструмент Reality Composer Pro, созданный чтобы упростить подготовку 3D-контента для приложений под VisionOS. Он напоминает редактор сцен Unity.
Сначала вам понадобятся объекты, которые вы хотите отобразить — 3D-модели в формате USD. Здесь мы столкнулись с одной из основных проблем при разработке игр под VisionOS: с поиском 3D-моделей. У Apple есть набор бесплатных ассетов, и, хоть выбор невелик, оттуда можно что-то достать. Можно купить 3D-модель.Это дорого, цена варьируется от 2$ до 2000$ в зависимости от качества и функционала модели. Можно найти 3D-дизайнера или сделать модель самому. Самый доступный вариант — найти бесплатные ассеты на сайтах, в группах или Telegram-чатах.
Когда вы нашли нужные 3D-модели — например, Земли и Луны, вам нужно импортировать их в сцену. Просто перетащите их либо на панель слева, либо прямо в пространство. Затем их надо расположить на сцене. Положение объекта задается координатами на трех осях — x, y, z. Чтобы понять, куда смотрит каждая из координат, сделайте такой жест: большой палец — ось x, указательный — ось y, и средний — ось z.
Я не знал об этом жесте, поэтому тыкал позиции наугад, запускал проект и проверял, все ли ок. Видимо, из-за поворота портала, который изначально был горизонтальным, система координат повернулась на 90 градусов от меня. Спустя кучу попыток я нашел позиции объектов, которые меня удовлетворили.
Также в Reality Composer Pro можно менять размеры 3D-моделей, вращать и разворачивать их. Можно добавлять к объекту компоненты: освещение, тени, коллизии, физику, звуки. Например, если вы хотите, чтобы где-то летала и чирикала птица, вы можете добавить ей звук и он будет идти прямо от нее.
Создаем мир внутри портала
Чтобы сделать портал, нужно создать:
Мир, который будет отображаться внутри портала,
Сущность портала — черный кругляш,
Якорь — сущность, к которой прикрепляются все объекты. Якорем могут быть руки пользователя, стены помещения, пол, столы и прочие вещи.
Разработчик кладет в портал мир, сам портал кладет в якорь, а потом все три сущности кладет в контент.
Чтобы сделать мир, нужно создать сущность и задать ей соответствующее свойство. За него отвечает компонент, который называется World Component. Он отделяет от внешнего мира все, что лежит внутри портала, поэтому благодаря нему контент будет лежать именно в портале.
Теперь нужно заполнить все это дело контентом. Здесь вы видите функцию load, в котором мы загружаем ассет Solar System, который мы настроили ранее в Reality Composer Pro.
Создаем портал
Скриншот слева — это портал, который сделали Apple. Под видео даже есть секция «Code», но я пробовал и этот код не работал. В итоге мы справились сами, и наш результат — справа.
Чтобы создать портал, нужно сделать сущность и настроить у нее Portal Component, в который мы поместим мир, и Model Component — внешний вид этой сущности, то есть черный кругляш. Этого будет достаточно для его работы.
Добавляем спецэффекты
Мы хотели сделать не простой, а красивый портал, поэтому на строчке 65 подгрузили ассет Particles — это частички, с помощью которых можно создавать различные эффекты.
Возвращаемся в Reality Composer Pro и добавляем Particles. Здесь есть различные ассеты — например, фейерверки и туманы. Можно настроить, как часто будут пульсировать частички, их количество, форму и цвет.
Я час ковырял этот инструмент и получил такое:
На мой взгляд, неплохо. Хотя бы портал напоминает — уже хорошо. Дальше нужно добавить эту штуку в портал, и он будет сиять.
Прикрепляем пылесос к руке
Чтобы прикрепить пылесос к руке, сначала нужно загрузить ассеты. Пылесос должен взаимодействовать с крутящимися котами. Для этого ему нужно настроить коллизии через Collision-компоненту, маску и группу. Маска отвечает за то, с какими группами будет взаимодействовать пылесос, а группа — за то, к какой группе относится объект. Collision-компонента задается битовой маской. Внизу вы видите Collision Group, передаем туда битовую операцию и получается битовая маска.
Затем нужно прикрепить пылесос к руке. Для этого сначала нужно научиться следить за руками пользователя. Вспоминаем про ARKit и создаем сессию ARKit session.
У сессии есть метод run, который в качестве параметра принимает массив DataProvider’ов . В данном случае нужен Hand Tracker Provider, который следит за руками. Дальше можно отслеживать состояние переменной handTracking. Берем оттуда правую руку и получаем ее якорь с параметром originFromAnchorTransform — локацию руки относительно мира. Затем мы присваиваем эту локацию ручке пылесоса. Также через метод Look нужно настроить, куда будет смотреть эта ручка — потому что позицию объекта мы задаем через поле position, а метод look настраивает то, куда будет направлен объект.
Крутим манулов
Мы создали собственный кастомный компонент — структуру, которая будет конформить протокол Component. Задали в нем нужные поля и зарегистрировали компонент: один раз где-нибудь вызвали функцию Register Component.
Теперь нужна система, которая умеет взаимодействовать с этим компонентом. Для этого мы создали класс, законформили протокол System, и у него появилась возможность переопределить метод Update. Update вызывается каждый раз на обновление фрейма, а частота его вызова зависит от частоты обновления кадров в Vision OS. На очках примерно 90 Гц, соответственно она будет обновляться 90 раз в секунду. Нужно найти сущность, которая соответствует определенному параметру — в данном случае, имеет компонент Rotate Component, изменить ее параметр orientation, и тогда объект будет крутиться.
Бургеры и помидоры
С точки зрения кода вторая мини-игра проще игры про манулов. Чтобы наполнить мир вокруг пользователя бургерами и помидорами, надо было вызвать функции Add Burger или Add Tomatoes столько раз, сколько бургеров или помидоров мы хотим.
Функция Add Burger простая — грузим ассет с моделью бургера и добавляем компоненты:
Input target component позволяет взаимодействовать с пользователем.
Hover эффект — чтобы объект, на который смотрит пользователь, выделялся среди других объектов. Наподобие кнопки, на которую наведен курсор мышки.
Также объекту нужно задать позицию. Ее можно сгенерировать рандомно: внизу справа видно функцию, которая возвращает структуру SIMD3, которая отвечает за координатную сетку. В ней мы и генерируем рандомные значения.
Затем нужно задать стандартный жест для Apple Vision Pro, при котором указательный и большой палец касаются друг друга. Это можно легко сделать через SpatialTapGesture(), модификатор .targetedToAnyEntity() позволяет этому жесту взаимодействовать с любыми объектами, которые мы положили в Immersive View.
Включаем музыку
Мы сгенерировали десять ассетов Google-голосом. Когда пылесос засасывает манулов, Google-голос ведет подсчет. По сути, разработчику нужно заполнить массив аудиофайловых ресурсов через функцию load(), потом на коллизии пылесоса и кота вызвать у сущности функцию playAudio() и передать нужный ассет. Фоновую музыку можно задать стандартно через AVAudioPlayer.
Все это мы с коллегами успели сделать на хакатоне за одну продуктивную ночь, а дальше я расскажу, что делал один.
После хакатона
У меня было время, чтобы поработать с Apple Vision Pro в спокойной обстановке. Поскольку в мини-играх на хакатоне мы не взаимодействовали с физикой, я решил делать что-то вроде тенниса.
Настраиваем физику
Сначала мне нужно было понять, как работает физика у объектов. Для этого я подгрузил соответствующий ассет и добавил ему PhysicsMotionComponent, который отвечает за движения объектов. Затем я добавил ему коллизии, физическое тело, которое отвечает за центр массы, коэффициент упругости, коэффициент трения и прочие вещи. Это можно сделать как в Reality Composer Pro, так и в коде.
У меня получился виртуальный объект, который не умел взаимодействовать с реальным миром. Если бы я просто загрузил его, он бы упал и бесконечно летел сквозь стены и потолки. Нужно было научиться отслеживать реальность вокруг.
Возвращаемся к ARKit и его сессии. В качестве дата-провайдера мне нужно было отдать ему SceneReconstructionProvider, который отвечает за отслеживание всех поверхностей вокруг. Я закинул его в метод Run, чтобы на апдейты sceneReconstruction пришли якоря стен, полов и прочих вещей, у которых были бы нужные параметры. По ним можно было воссоздать форму этого якоря, например стены.
Затем я создал сущность, в которую положил компонент CollisionComponent. Он делается на основе формы, созданной выше. Задал физическое тело и расположение (transform) через расположение пришедшего якоря. По сути, мы не можем напрямую взаимодействовать с реальными объектами, но можем отследить их форму, расположение и создать виртуальные копии, с которыми смогут взаимодействовать наши виртуальные объекты.
Кстати, чтобы уметь все это отслеживать, нужно получить разрешение от пользователя, для чего надо создать пару ключей с описанием для чего мы получаем это разрешение в info.plist.
Пытаемся схватить мячик
Мне нужно было схватить мячик и ракетку, а затем ударить одним о другое. Сначала я думал прикрепить ракетку к руке как к якорю, как мы это делали с пылесосом, а мячик двигать с помощью SpatialTapGesture. Но это было слишком просто — хотелось, как в реальности.
Я узнал, что могу отслеживать не только руку, но и каждый сустав этой руки. Как это сделать? Создать словарь, ключом которого будет сустав. А значением по ключу будет 3D-сущность, у которой будет физический компонент и компонент CollisionShape. Таким образом я прикрепил 3D-сущность к пальцу и учил его взаимодействовать со всем вокруг. В том же методе, где я следил за руками, можно следить за пальцами, тогда будут две координаты — руки относительно мира и суставы относительно руки. Перемножив эти, по сути, матрицы, получаем координату сустава относительно мира. А дальше вносим полученное изменение в словарь, упомянутый выше.
Я думал, что этого хватит, но в итоге мячик превратился в скользкое мыло, выскальзывал из пальцев и не хотел прикрепляться к руке. Позже мне подсказали, что такие коллизии не предназначены для подобных взаимодействий.
Поэтому нужно искать другой подход, например, распознать жест и после уже прикрепить мячик к руке.
Делаем распознавание жеста захвата
У Apple есть отличный пример — игра Happy Beam. В ней на пользователя летят грустные тучки, а он жестами в форме сердечек отправляет им лучи добра, чтобы они превратились в счастливые белые облачка.
Я посмотрел, как они отслеживают жест и сделал собственный метод. Вот как он работает: я точно так же отслеживаю суставы рук и их локацию, а дальше беру большой палец, указательный и безымянный и делаю так, чтобы расстояние между ними было меньше шести сантиметров — подобрал на глаз. Это и будет напоминать жест захвата. Вы можете придумать собственную интерпретацию жеста, но моя, в целом, работает.
Кроме определения жеста нужно было сделать так, чтобы он срабатывал только в момент коллизии с нужным объектом. То есть чтобы пользователь дотрагивался до мячика, делал жест, и только тогда мячик крепился к его руке. Для этого у content, который передается в кложуре RealityView, я вызвал метод subscribe() и подписался на все коллизии, происходящие в приложении.
На эту подписку я вызвал функцию, в которую передавал, был или не был жест, и смотрел в самой функции, какие именно объекты провзаимодействовали, потому что из всех коллизий всех объектов мне нужно было отследить конкретные. В итоге все выглядело так: происходила какая-то коллизия, я проверял, был ли в этот момент жест и те ли объекты — рука и мяч, провзаимодействовали. Если все совпадало, мяч крепился к основанию руки.
В результате скользкое мыло превратилось в слайм, который не отлеплялся от руки. К сожалению, это все, что я успел сделать. Тем не менее, я не планирую останавливаться и буду пытаться дальше!
Недостатки 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
здравствуйте
а есть исходники? С картинок читать не удобно, да и текст обрезан