Nuclear Wars: чему меня научила игра для visionOS, сделанная в одиночку за три месяца
Стратегия про холодную войну на 3D-глобусе для Apple Vision Pro, написанная в одиночку на Swift и RealityKit без движка. Почему без движка, как устроен код, чему научили RealityKit и App Review.
Автор Дмитрий Червяков · Senior iOS Developer
Раз уж у меня внезапно появилось много свободного времени, я решил следовать правилу, которое сам для себя и придумал: у каждого уважающего себя разработчика под платформы Apple должна быть своя игра для Apple Vision Pro. Через три месяца Nuclear Wars вышла в App Store. Этот пост о том, чего это стоило и что я отсюда унёс.
Игра
Nuclear Wars — стратегия про холодную войну, где карта — трёхмерный глобус, висящий у вас в комнате. Крутите его жестом, приближаете щипком, обходите вокруг, наклоняетесь к шахте. В фазе подготовки расставляете на своём континенте ракетные шахты, радары, батареи ПРО, подводные лодки и авиабазу. Потом начинается холодная война: подлодки выходят в море, радары сканируют, ПВО открывает огонь. Потом — обмен ударами.
Победителя нет. Когда таймер истекает, тот, кто сохранил больше населения, проиграл меньше. Два правила делают из этого стратегию, а не фейерверк: вы видите только то, что видят ваши радары, и каждый пуск навсегда раскрывает юнит, который стрелял. Каждый выстрел — размен между силой удара и скрытностью.
Играть можно соло против одного–пяти ИИ-командующих или с друзьями через Game Center: код группы для своих, автоподбор для незнакомых, до шести игроков, пустые места занимает ИИ.
Почему без игрового движка
На visionOS нативный стек даёт passthrough, системные жесты, окна и весь пространственный UI бесплатно. Unity или Unreal добавили бы слой между мной и всем этим, а для одного глобуса с парой десятков юнитов этот слой ничего не покупает. Поэтому игра — это Swift, SwiftUI и RealityKit, и больше ничего.
Цена — игровой цикл приходится строить самому. Я разделил так: симуляция живёт в чистом Swift-пакете без импортов RealityKit и GameKit. Она шагает фиксированными тиками и ничего не знает о рендере. Приложение подписывается на эти тики и только интерполирует то, что ему отдали: позиции ракет, развёртки радаров, горящие города. Всё приложение — около 6 000 строк Swift, пакет симуляции и сети — меньше 2 000. Это же разделение позже позволило добавить мультиплеер, не трогая правила игры.
Чему научил RealityKit
Несколько вещей из проекта, которые я бы с удовольствием прочитал до старта.
- Слои на глобусе — текстуры, а не меши. Заливка территорий, туман войны и покрытие радаров — каждый слой это одна эквиректангулярная текстура на своей тонкой сфере, на доли процента больше глобуса. В первой версии купола радаров были отдельными прозрачными мешами, и на каждом пересечении появлялись артефакты рендера от z-fighting. Одна объединённая текстура на слой, пересчитываемая вне main actor, закрыла проблему навсегда.
- RealityKit читает альфу текстуры как straight, а не premultiplied. Полупрозрачные цвета, нарисованные через
UIGraphicsImageRenderer, темнели примерно вдвое. Лечится так: рисуем непрозрачно, снимаем premultiply и выставляем альфу вручную. - Покадровая работа живёт в подписке на
SceneEvents.Update.RealityView.updateсрабатывает только на изменение состояния SwiftUI, так что для всего, что должно двигаться каждый кадр, это неправильное место.
Мультиплеер через Game Center
Мультиплеер — детерминированный lockstep поверх GKMatch. Часы принадлежат хосту: каждый тик симуляции уходит клиентам кадром с номером тика и командами в нём. Клиенты никогда не шагают симуляцию сами, только по полученным кадрам, а их команды летят хосту, который вклеивает их в ближайший кадр. Боты живут на хосте. Поскольку симуляция детерминирована, все заканчивают матч с одинаковыми цифрами населения на экране разбора.
Неожиданной оказалась история с кодом группы. Первый вариант использовал новые активности Game Center из visionOS 26. Активность прошла ревью и стала Live, а loadGameActivityDefinitions всё равно возвращал пустой список — и на устройстве, и в симуляторе, сутки спустя после релиза. Я перестал ждать и обратимо закодировал четырёхсимвольный код в GKMatchRequest.playerGroup: Game Center сводит только игроков с одинаковой группой, так что одинаковый код — одна комната. Автоподбор — тот же запрос с группой по умолчанию.
Четыре раунда App Review
Карта стоила мне четырёх раундов. Первая сборка использовала Землю с реальными городами. Отклонено по Guideline 1.1.2: реальные страны в роли врагов. Я заменил названия городов вымышленными. Отклонено. Заменил страны абстрактными секторами по меридианам и параллелям. Снова отклонено: реальная карта сама по себе — «реальная сущность».
Так что игра больше не происходит на Земле. Планету генерирует шумовая функция, её континенты, береговые линии, острова и полярные шапки не имеют ничего общего с нашей географией, шесть игровых секторов — это её шесть вымышленных континентов, а все города синтетические, с одинаковым суммарным населением на сектор. Противники безымянны. Эта сборка прошла. Если планируете военную игру для App Store, планируйте вымышленный мир с первого дня.
Что я отсюда унёс
Это мой второй проект под visionOS, и делался он так же, как я описывал в прошлом посте: агент пишет большую часть первого черновика, вокруг него — петля обратной связи. За два проекта накопилось достаточно набитых шишек — особенности RealityKit вроде описанных выше, паттерны пространственного UI, что и почему просит поменять App Review, — чтобы собрать из них полноценный скилл для разработки visionOS-приложений с агентом. Игра — приятная вещь. Скилл — то, с чем я буду делать следующую.
Nuclear Wars есть в App Store, а если хочется увидеть глобус в комнате — есть 37-секундный трейлер. С удовольствием поговорю про visionOS, RealityKit и пространственные игры.