Swift その2 Advent Calendar 2017で、本当は「セル上に簡単な広告実装と、複数広告の工夫」的なことを書く予定だったのですが、
ハッカソン参加で忙しくARを触っていたため怠ってしまいました、、、
ということで内容変更してARの記事を載せます。
前書き
メルカリの1dayARハッカソン(Mercari AR Hackathon #0)に出場してきました!
1dayといっても事前準備とかOKなんですが、、笑
せっかくARKit
に触れたので、作ったものを説明しつつコード記録も込めて書いておきます。
前提
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のみで作成しています。
歩き回るとモンスターがランダムで出てくるので、それをタップなりスワイプなりで倒していく感じになります。
細々した機能はたくさんあるのですが、全部はかけないので抜粋して掲載していきます。
①開始画面/職業選択画面


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

モンスターを出現させるために、平面認識が開始されます。
認識の仕方は等々は以下を参考にしています。
⑤モンスターの出現

歩いている方向にモンスターが出現するように設定しています。
モンスターに関しては、フリーの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
}
}
enum
がUInt32
なのはモンスターの種類をランダムで出現させるたい時に
enum
のrawValue
で行えるからです。
使用例
// 生成される画面
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)
}
カメラを傾け続けると、平面認識は常におこなわれるため、アップデートされた座標をモンスターの出現場所に設定するようにしています。

// 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
)を使って演出してみました。
イメージは、スクショですが以下

タップ判定の取得
// 画面
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)
}
パーティクルの作り方に関しては
に詳しく載っているので、お好みのものを作成していただけると!
あとがき
Unityの方が良いってのをよく聞くんですが、ARKitだけでも十分良いものが作れるような気がしました。
部分的にしかコードを載せていないので、動作しなかったらすいません笑