Использование фреймворков SwiftUI, RealityKit, ARKit и Multipeer

Я провел большую часть этого года (2022), изучая SceneKit. Путешествие, которое я задокументировал почти двумя дюжинами статей на эту тему, вы найдёте здесь, на Medium. Изучив большинство элементов в SceneKit, я решил перейти на RealityKit/ARKit в 2023 году.

Я не был уверен, с чего начать, поэтому я смотрел последние презентации WWDC2022 на ARKit, а затем на RealityKit, ну и — это не помогло. Я посмотрел, что было вначале, а затем самый ранний WWDC.

Я смотрел ролик “Building Apps with RealityKit” (Создание приложений с помощью RealityKit) с WWDC2019. Это был момент озарения, ну, почти. Я не мог не задуматься о сходстве между SceneKit и RealityKit. RealityKit — это SceneKit. Возможно, они разработали его с нуля, имея в виду многопроцессорность, но примитивы во многих случаях выглядят почти одинаково. Они хотели сделать переход от одного к другому как можно более плавным. И действительно, кажется, что если вы даже можете создать приложение на основе RealityKit без камеры — приложение, которое в конечном итоге будет выглядеть так же, как вы сделали это в SceneKit. ARKit кажется неотъемлемой частью RealityKit, поэтому они как инь и янь: одно имеет смысл существования только с другим.

Посмотрев презентацию, я решил перестроить шахматную партию, описанную в этой статье, используя RealityKit/ARKit, сосредоточившись на первых двух разделах: создание прототипа и приведение его в состояние игры.

Прототип

UIRepresentable

Итак, это почти 2023 год, и нет для меня пути назад, кроме использования UIKit. План сборки будет таким же, как описанный в той презентации WWDC2019, с небольшими изменениями и обновлениями.

Я скопировал код из своей последней статьи о SceneKit и использовал его в качестве основы для сборки. Мой UIRepresentable был настолько прост, насколько вы могли себе представить, всего десять строк или около того.

struct CustomARView: UIViewRepresentable {
    typealias UIViewType = ARView
    
    var view:ARView
    var options: [Any] = []
    
    func makeUIView(context: Context) -> ARView {
        view.session.delegate = context.coordinator
        return view
    }
    
    func updateUIView(_ view: ARView, context: Context) {
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self.view)
    }

    // coordinator 
}

Класс Coordinator

С классом Coordinator всё иначе (столь же краткий), вот скелет кода:

class Coordinator: NSObject, ARSessionDelegate {
        private let arView: ARView

        init(_ view: ARView) {
            
            self.arView = view
            super.init()
            
         }

На данный момент это было легко. Я играл в WWDC2019 на одном экране и обновлял код в Coordinator на другом. Я создавал здесь прототип, так что мне не нужна была вся доска, только самое необходимое для вставки туда базового кода, чтобы понять семантику и синтаксис.

Сначала у меня возникла небольшая проблема с масштабированием, так как мои модели выглядели как монстры, но это легко исправить. Я столкнулся со второй проблемой: с синтаксисом преобразования, которая отличалась от того, что я ожидал, хотя, к приятному удивлению, компания Apple хорошо задокументировала её здесь, и она также была быстро решена.

Это была основа. На самом деле, я даже продвинулся на шаг вперед, так как уже использовал 3D модели для своего прототипа, только я использовал неправильный вызов для их загрузки. Я делал это синхронно, из-за чего моё приложение зависало. Я модифицировал асинхронный код с помощью этого поста на Stack Overflow. Загрузка переменной здесь имеет тип Cancelable (код WWDC2019 даже не компилируется).

loading = Entity.loadModelAsync(named: assetName)
    .sink(receiveCompletion: { completion in
        if case let .failure(error) = completion {
            print("Unable to load a model due to error \(error)")
        }
        self.loading?.cancel()
        
    }, receiveValue: { [self] (entity: Entity) in
        if let entity = entity as? ModelEntity {
            let piecePlayer = entity
            loading?.cancel()
            print("Congrats! Model is successfully loaded!")
            piecePlayer.position = SIMD3(x: 0, y: 0, z: 0)
            piecePlayer.setScale(SIMD3(0.01,0.01,0.01), relativeTo: piecePlayer)
            piecePlayer.generateCollisionShapes(recursive: true)
            piecePlayer.name = "GCHKing"
            anchor.addChild(piecePlayer)
        }
    })

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

let material = SimpleMaterial(color: .black, isMetallic: true)
let plainMesh = MeshResource.generatePlane(width: 0.1, depth: 0.1)
let entity = ModelEntity(mesh: plainMesh, materials: [material])
anchor.addChild(entity)

Я был на полпути к созданию игры на WWDC2019, но в этот момент я столкнулся с ещё одной загвоздкой. Освещение не работало — полезная проблема, так как я понятия не имел, как работает освещение в ARKit, и нашёл здесь полезный код.

directionalLight.light.color = .white
directionalLight.light.intensity = 500
directionalLight.light.isRealWorldProxy = true
directionalLight.shadow?.maximumDistance = 1
directionalLight.shadow?.depthBias = 4.0
directionalLight.orientation = simd_quatf(angle: Float(0).degrees2radians(),
                                           axis: [0,1,0])

let lightAnchor = AnchorEntity(world: [0,1,0])
lightAnchor.addChild(directionalLight)
arView.scene.anchors.append(lightAnchor)

Состояние игры

Когда мы подошли к этой части презентации WWDC2019, я начал подозревать, что у них не хватило времени на доработку. Я предполагаю, что они упустили критическую сторону для объяснения того, как интегрировать код объекта/компонента. Хуже того, меня смутило ужасное наименование ModelEntity в показанном асинхронном коде (изменено в моём коде). В презентации они называют это “моделью”. Имя, которое фактически означает, что вам нужно ссылаться на модель внутри модели с помощью model.model — это нехорошо, Apple.

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

enum PieceColor: Codable {
    case black
    case white
}

struct PieceComponent: Component, Codable {
    var color: PieceColor
    var value: Int
}

class ChessEntity: Entity, HasModel, HasCollision, HasPhysics {
    //public var model: ModelEntity!
    public var piece: PieceComponent {
        get { return components[PieceComponent.self] ?? PieceComponent(color: .white, value: 0) }
        set { components[PieceComponent.self] = newValue }
    }
}

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

extension ChessEntity {
    func movePiece() {
        print("name \(self.name)")
        self.transform.translation -= SIMD3<Float>(0,0,-0.1)
    }
}

Код, который вы интегрируете с вашими существующими процедурами, а именно загрузкой ресурсов и обнаружением/действием, когда кто-то нажимает на элемент. Изменение, которое вам необходимо внести в загрузку после настройки объекта, состоит всего из двух строк:

let piecePlayer = ChessEntity()
piecePlayer.model = entity.model

Расставив важные фигуры по местам, я был готов добавить в шахматную партию экшн. Действие, которое я получил на тарелке с презентации WWDC2019, более или менее. Код, запускаемый простым UITapgestureRecognizer.

@objc func handleTap2(_ sender: UIGestureRecognizer? = nil) {
    let tapLocation = sender?.location(in: arView)
    if let piece = arView.entity(at: tapLocation!) as? ChessEntity {
        piece.movePiece()
    }
    
}

Изображение

Теперь, когда всё на месте, я построил доску и поставил на неё свою шахматную фигуру. Я был готов к следующему шагу.

Будущее

И вот он у вас есть, прототип шахматной версии с дополненной реальностью, использующий не только RealityKit, но и ARKit — они кажутся почти неразлучными.

Конечно, реализовать ещё совсем немного — в презентации они заканчивают многопользовательской версией, это существенное отличие от SceneKit и ОБЯЗАТЕЛЬНО для моей реализации. Я хочу в этот раз попытаться сделать немного больше, чем я делал и в этот, и в прошлый раз; возможно, внести некоторый интеллект в игру.

Наконец, мои мысли вернулись к тому моменту, когда я начал это путешествие в марте 2022 года. Тогда я утверждал, что SceneKit не умер, он просто отдыхал. Пока рано говорить, в моей уверенности, но я удивлён сходством между RealityKit и SceneKit.

Тем не менее, покопавшись, я нашел отличную статью Тони Моралеса (Tony Morales), сравнивающую RealityKit и SceneKit, которую он написал два года назад. Я связался с ним, и он сказал мне, что “большинство моих проблем были решены”. Теперь это выглядит и звучит так, будто RealityKit заменил SceneKit. Я дам вам знать, если я передумаю на этот раз в следующем году, когда у меня есть шанс по настоящему вникнуть в это самостоятельно. Подписывайтесь на меня на Medium, чтобы быть в курсе этой темы.

Комментарии (0)