LoginSignup
20
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-23

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

前書き

メルカリの1dayARハッカソン(Mercari AR Hackathon #0)に出場してきました!

以下、作品(リリースはしていません)
Image uploaded from iOS.png

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

前提

ARKit

これを読んでおけば大体なんとなくわかります。
- ARKitのまとめ
- ARKitを扱う際の心構えとTips
- Apple 純正の AR フレームワーク「ARKit」について理解する

SCNNode+Extension

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

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を作成しました。

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のみで作成しています。

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

①開始画面/職業選択画面

スクリーンショット 2017-12-17 17.36.24.png

スクリーンショット 2017-12-17 17.36.44.png

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

②戦闘画面

Image uploaded from iOS (4).png

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

認識の仕方は等々は以下を参考にしています。
- ARKitで豆腐作り
- [Swift4]ARKitで球体をランダムに描画する
- 公式ドキュメントを追いながらARKitを試してみよう

⑤モンスターの出現

Image uploaded from iOS (3).png

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

以下、方法例として

// モンスター定義
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
    }
}

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

使用例


// 生成される画面
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)

}

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

Image uploaded from iOS (2).png

// ARKit のライフサイクルの1つ
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)を使って演出してみました。

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

Image uploaded from iOS (1).png

タップ判定の取得

// 画面
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
}
// 魔法の定義
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)
    }

}

使用例

// 画面
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
- パーティクルの障害物判定と新しいエミッターの派生

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

あとがき

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

その他参考サイト

20
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
16