これちのPost-it

技術ネタをペラペラと

ARKit 2.0 のサンプルアプリ SwiftShot で遊んで少し調べてみた

公式に SwiftShot(WWDC でデモされたアプリ) のサンプルコードがあったので DL して手元でビルドして遊んでみました。 (アプリに関する説明はリンク先に書かれているため割愛します。)

SwiftShot: Creating a Game for Augmented Reality | Apple Developer Documentation

f:id:korechi:20181003143120p:plain

少し遊んでみて分かったのは、まさに(ARKit 2.0 の目玉である)「複数端末間で AR 空間を共有」する機能を紹介するためのサンプルアプリだなという感じです。

ただ「ワールドの保存」ができないのは気になりました。これも ARKit 2.0 の目玉機能のはずなのに。 アプリを再起動したらまた0からゲームを始めることになります。(当然ゲームとしては問題ないのですが、デモアプリならこの機能も使ってほしかった)

では SwiftShot がどうやって実現されているか(主に通信まわり)簡単に調べてみます。

複数端末接続には MultipeerConnectivity を使用

SwiftShot は同じネットワーク内の端末と接続するために MultipeerConnectivity フレームワークを使っています。 (ARKit が収集した空間マップデータはピア・ツー・ピアのデバイス間のみで共有されるためデータが外に漏れるのを防ぐことが可能)

MultipeerConnectivity についてここで深掘りはしませんがもし興味があれば調べてみてください。

ARKit の spatial understanding(すみませんこの意味はまだ分かりません) を含む ARWorldMap 作成及び送信は Host の役割です。

Client はワールドのコピーを受信し Host 視点からのカメラ画像を見ることでマルチプレイが可能となります。 なので Host がゲームを開始した時の端末とだいたい同じ場所・向きに Client 端末を重ねないとゲームは開始しません。 (特徴点である ARAnchor を含む ARWorldMap を渡せば別に同じ画像でなくともワールドが復元できそうな気もしますが、どうなんだろう。正確なゲーム空間の座標共有のための安全策かな?)

ARSession のマルチプレイについては Creating a Multiuser AR Experience を見ると良さそう。 簡単にサマってみます。

ARWorldMap の送信

ARWorldMap は getCurrentWorldMap を呼ぶことで ARSession からキャプチャできます。

その際キャプチャするのに適したタイミングかを worldMappingStatus から知ることができます。

switch frame.worldMappingStatus {
case .notAvailable, .limited:
    sendMapButton.isEnabled = false
case .extending:
    sendMapButton.isEnabled = !multipeerSession.connectedPeers.isEmpty
case .mapped:
    sendMapButton.isEnabled = !multipeerSession.connectedPeers.isEmpty
}
mappingStatusLabel.text = frame.worldMappingStatus.description

その後 NSKeyedArchiver を使って Data オブジェクトに変換し multipeer セッションを介して他デバイスに送信されます。

sceneView.session.getCurrentWorldMap { worldMap, error in
    guard let map = worldMap
        else { print("Error: \(error!.localizedDescription)"); return }
    guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
        else { fatalError("can't encode map") }
    self.multipeerSession.sendToAllPeers(data)
}

Shared Map の受信

session(_:didReceive:fromPeer:) デリゲートから data を受信。その後 ARWorldMap オブジェクトにデシリアライズして configuration に登録します。

if let worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data) {
    // Run the session with the received world map.
    let configuration = ARWorldTrackingConfiguration()
    configuration.planeDetection = .horizontal
    configuration.initialWorldMap = worldMap
    sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    
    // Remember who provided the map for showing UI feedback.
    mapProvider = peer
}

AR コンテンツとユーザ操作の同期(共有)

ARAnchor などの AR オブジェクトは Host がワールドを送信する前の状態は当然 ARWorldMap に含まれるためそのまま他端末へ共有されます。 しかし、それ以降追加された AR オブジェクトは当然自動的に共有されないため随時ブロードキャストしていく必要があります。 SwiftShot では一例として、共有する情報をすべて ARAnchor オブジェクトに変換し共有しています。

// Place an anchor for a virtual character. The model appears in renderer(_:didAdd:for:).
let anchor = ARAnchor(name: "panda", transform: hitTestResult.worldTransform)
sceneView.session.add(anchor: anchor)

// Send the anchor info to peers, so they can place the same content.
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true)
    else { fatalError("can't encode anchor") }
self.multipeerSession.sendToAllPeers(data)

まず ARAnchor オブジェクトを作成しそれを sceneView.session.add(anchor:anchor) でシーンに追加。 その後先程と同様に NSKeyedArchiver.archivedData で data オブジェクトへシリアライズして NSKeyedArchiver.archivedData で送信します。

ARWorldMap を送信する手順とそんなに変わらないです。

受信側も同様に multipeer session から data を受信し ARAnchor を含んでいたらデシリアライズし、シーンに追加します。

if let anchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARAnchor.self, from: data) {
    // Add anchor to the session, ARSCNView delegate adds visible content.
    sceneView.session.add(anchor: anchor)
}

ただこれはあくまで多くある実現手段の一つにすぎないです。別の通信手段、別の送受信フォーマットを使うことも当然可能でしょう。 例えばゲームは様々なデータ型(例えば初速や加速度など)を含むオブジェクトを共有したい場合があります。 そんなときは Swift の Codable プロトコルを使ってバイナリにシリアライズしてネットワークに送信することも可能です。