[Swift]ARKitでドラクエっぽいものをつくってみた


[Swift その2 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/swift2)で、本当は「セル上に簡単な広告実装と、複数広告の工夫」的なことを書く予定だったのですが、
ハッカソン参加で忙しくARを触っていたため怠ってしまいました、、、
ということで内容変更してARの記事を載せます。

# 前書き
メルカリの1dayARハッカソン（[Mercari AR Hackathon #0](https://vrtokyo.connpass.com/event/71473/)）に出場してきました！

以下、作品（リリースはしていません）
![Image uploaded from iOS.png](https://qiita-image-store.s3.amazonaws.com/0/175242/0f0af208-e667-f5b3-c805-96c7efc0333b.png)

１dayといっても事前準備とかOKなんですが、、笑
せっかく``ARKit``に触れたので、作ったものを説明しつつコード記録も込めて書いておきます。


## 前提

### ARKit
これを読んでおけば大体なんとなくわかります。
- [ARKitのまとめ](https://mike-neko.github.io/blog/arkit3/)
- [ARKitを扱う際の心構えとTips](https://qiita.com/kaoryuuu/items/184dbd0fc9c9017dd7d2)
- [Apple 純正の AR フレームワーク「ARKit」について理解する ](https://dev.classmethod.jp/smartphone/iphone/understanding-arkit/)

### SCNNode+Extension
ARには欠かせない``SCNNode``をあつかうための``extension``を作成しました。
これがあれば、チェーンでどんどん書いていけるかと。

```swift
extension SCNNode { 

    // 重力設定
    public func setPhysics<T>(with object: T, and type: SCNPhysicsBodyType = .kinematic) -> SCNNode where T: SCNGeometry {
        let shape = SCNPhysicsShape(geometry: object, options: nil)
        self.physicsBody = SCNPhysicsBody(type: type, shape: shape)
        return self
    }
    
    // 座標設定
    public func setPosition(from position: SCNVector3) -> SCNNode {
        self.position = position
        return self
    }
    
    // 回転設定
    public func setRotation(from rotation: SCNVector4) -> SCNNode {
        self.rotation = rotation
        return self
    }
    
    // サイズ設定
    public func setScale(from scale: SCNVector3) -> SCNNode {
        self.scale = scale
        return self
    }
    
    // ノード生成
    public func addNode(to scene: inout SCNScene) {
        scene.rootNode.addChildNode(self)
    }

    // ノード削除
    public func removeNode(to scene: inout SCNScene) {
        scene.rootNode.removeFromParentNode()
    }
    
}
```

### SCNParticleSystem+Extension
簡単なアニメーション演出が行えるパーティクル用の``extension``を作成しました。

```swift
extension SCNParticleSystem {
    
    // ノード衝突時に死亡判定（寿命的なもの）をつける
    public func setDiedOnCollision<N>(for nodes: [N]) -> SCNParticleSystem where N: SCNNode {
        self.colliderNodes = nodes
        self.particleDiesOnCollision = true
        return self
    }
    
    // ノードの生成
    public func initNode() -> SCNNode {
        let node = SCNNode()
        node.addParticleSystem(self)
        return node
    }
}
```

上記いずれもそのうち説明に出てくるので、使用例は割愛。

## ARクエスト
ハッカソンのアイデアが浮かばず、、、ドラクエのAR版的なものを作りました笑
今回はUnityとかつかわず、すべて**ARKitのみ**で作成しています。

歩き回るとモンスターがランダムで出てくるので、それをタップなりスワイプなりで倒していく感じになります。
細々した機能はたくさんあるのですが、全部はかけないので抜粋して掲載していきます。

### ①開始画面/職業選択画面
<img width="306" alt="スクリーンショット 2017-12-17 17.36.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/175242/219ed5b4-f0bc-8502-a711-9d5eb0297954.png">

<img width="309" alt="スクリーンショット 2017-12-17 17.36.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/175242/92907122-e5ec-9ab8-66e5-dd1e0f5ae91f.png">

この部分は普通の``Storyboard``と``UIView``で作成しています。
職業を選択できるようにしました。フォントもドラクエ風のものを適応しています。

### ②戦闘画面
<img width="414" alt="Image uploaded from iOS (4).png" src="https://qiita-image-store.s3.amazonaws.com/0/175242/726f9f3c-9071-1e02-98d4-4f7504369365.png">

モンスターを出現させるために、**平面認識**が開始されます。

認識の仕方は等々は以下を参考にしています。
- [ARKitで豆腐作り](https://qiita.com/negi0205/items/3314605d3053c7fdccac)
- [[Swift4]ARKitで球体をランダムに描画する](https://qiita.com/takuyama29/items/a14810585149b4dcb260)
- [公式ドキュメントを追いながらARKitを試してみよう](https://qiita.com/touyoubuntu/items/97e481f37a2d775853d7)


### ⑤モンスターの出現
<img width="414" alt="Image uploaded from iOS (3).png" src="https://qiita-image-store.s3.amazonaws.com/0/175242/f9f4ecd1-204f-1c2d-8854-e082018a2946.png">

歩いている方向にモンスターが出現するように設定しています。
モンスターに関しては、フリーの3Dモデル（``dae``ファイル）を使用しています。
（3Dモデルはぐぐればたくさん出てきます。）

以下、方法例として

```swift
// モンスター定義
enum MonsterType: UInt32 {
    case dragon
    case spider
    case wolf

    // ファイル名
    private var fileName: String {
        switch self {
        case .dragon: return "dragon"
        case .spider: return "spider"
        case .wolf: return "wolf"
        }
    }

    // daeファイルの読み込み
    private var bundleURL: URL? {
        return Bundle.main.url(forResource: fileName, withExtension: "dae")
    }
    
    // モンスターの生成
    var node: SCNNode? {
        
        guard
            let url = bundleURL,
            let scene = try? SCNScene(url: url, options: nil)
            else { return nil }
        
        // daeの3Dモデルは複数パーツで構成されているので、すべてaddChildNodeする必要がある
        let node = SCNNode()
        scene.rootNode.childNodes.map { node.addChildNode($0) } 
        return node
    }
}
```

``enum``が``UInt32``なのはモンスターの種類をランダムで出現させるたい時に
``enum``の``rawValue``で行えるからです。

使用例

```swift

// 生成される画面
private var sceneView: ARSCNView!

// モンスターの呼び出し（今回はくもを呼び出している）
func encountMonster(with position: SCNVector3) {

        // 冒頭で書いたノードのextensionを使っている
        MonsterType.spider.node?
            .setPosition(from: SCNVector3(position.x, position.y, position.z+1.5))
            .setRotation(from: SCNVector4(1, 0, 0, -0.5 * Float.pi))
            .setScale(from: SCNVector3(0.005, 0.005, 0.005))
            .addNode(to: &sceneView.scene)

}
```




カメラを傾け続けると、平面認識は常におこなわれるため、**アップデートされた座標**を**モンスターの出現場所に設定**するようにしています。

<img width="414" alt="Image uploaded from iOS (2).png" src="https://qiita-image-store.s3.amazonaws.com/0/175242/5cda5933-2ff2-4876-29b8-f9a683ebc2d6.png">

```swift
// ARKit のライフサイクルの１つ
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        let position = SCNVector3Make(node.position.x,
                                      node.position.y,
                                      node.position.z-0.2) // 少し前に出すために-0.2している
        
        encountMonster(with: position)
}
```

### ⑥技の設定

タップやスワイプいろんな技が使えるようにできたらと思い、パーティクル（``SCNParticleSystem``）を使って演出してみました。

イメージは、スクショですが以下

<img width="414" alt="Image uploaded from iOS (1).png" src="https://qiita-image-store.s3.amazonaws.com/0/175242/56c47b90-c6a9-b1ff-ca0f-5212e8204c7b.png">

タップ判定の取得

```swift
// 画面
private var sceneView: ARSCNView!

@IBAction private func sceneViewTapped(_ recognizer: UITapGestureRecognizer) {
        let tapPoint = recognizer.location(in: sceneView)
        let results = sceneView.hitTest(tapPoint, types: .existingPlaneUsingExtent)
        
        guard let result = results.first else { return }

        // タップした座標
        print(result.worldTransform.columns.3.x)
        print(result.worldTransform.columns.3.y)
        print(result.worldTransform.columns.3.z)

        // タップしたに存在の、一番上に存在するノードを取得
        // 今回はタップしたところにモンスターがいることを検知したいので
        let targetNode = sceneView.hitTest(tapPoint, options: nil).first?.node

        // do something
}
```


```swift
// 魔法の定義
enum SpellType {
    case fire
    case wind
    
    private var name: String {
        switch self {
        case .fire: return "Fire.scnp"
        case .wind: return "Smoke.scnp"
        }
    }
    
    // 魔法の呼び出し
    public func getParticle(with inDirectory: String? = nil) -> SCNParticleSystem? {
        return SCNParticleSystem(named: name, inDirectory: inDirectory)
    }
    
}
```

使用例

```swift
// 画面
private var sceneView: ARSCNView!

// 魔法呼び出し（今回は炎）
func onFire(with position: SCNVector3) {
    SpellType.fire.getParticle()?
            .initNode()
            .setPosition(from: position)
            .addNode(to: &sceneView.scene)
}
```

パーティクルの作り方に関しては
- [SceneKit Particle System File と パーティクルシステム の emitterShape](http://appleengine.hatenablog.com/entry/2017/08/22/155521)
- [パーティクルの障害物判定と新しいエミッターの派生](http://appleengine.hatenablog.com/entry/2017/08/22/183742)

に詳しく載っているので、お好みのものを作成していただけると！

## あとがき
Unityの方が良いってのをよく聞くんですが、ARKitだけでも十分良いものが作れるような気がしました。
部分的にしかコードを載せていないので、動作しなかったらすいません笑

## その他参考サイト
- [距離の計測について #WWDC2017](https://dev.classmethod.jp/smartphone/ios-11arkit-measure/)
- [オブジェクトの位置と回転「常にこっちを向いている怪獣」](https://dev.classmethod.jp/smartphone/iphone/ios11-arkit-pointofview/)