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のみで作成しています。
歩き回るとモンスターがランダムで出てくるので、それをタップなりスワイプなりで倒していく感じになります。
細々した機能はたくさんあるのですが、全部はかけないので抜粋して掲載していきます。
①開始画面/職業選択画面
![スクリーンショット 2017-12-17 17.36.24.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F175242%2F219ed5b4-f0bc-8502-a711-9d5eb0297954.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8ee3837a8ddaa7a48284c255dc9f9cfd)
![スクリーンショット 2017-12-17 17.36.44.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F175242%2F92907122-e5ec-9ab8-66e5-dd1e0f5ae91f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=798d8c506796700fed8ddbafb9cd9f1a)
この部分は普通のStoryboard
とUIView
で作成しています。
職業を選択できるようにしました。フォントもドラクエ風のものを適応しています。
②戦闘画面
![Image uploaded from iOS (4).png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F175242%2F726f9f3c-9071-1e02-98d4-4f7504369365.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=65750e4fd4fa3942aad5c578eb2c9919)
モンスターを出現させるために、平面認識が開始されます。
認識の仕方は等々は以下を参考にしています。
⑤モンスターの出現
![Image uploaded from iOS (3).png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F175242%2Ff9f4ecd1-204f-1c2d-8854-e082018a2946.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=c8e9d9906366067be8c1a52a6ab9912d)
歩いている方向にモンスターが出現するように設定しています。
モンスターに関しては、フリーの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)
}
カメラを傾け続けると、平面認識は常におこなわれるため、アップデートされた座標をモンスターの出現場所に設定するようにしています。
![Image uploaded from iOS (2).png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F175242%2F5cda5933-2ff2-4876-29b8-f9a683ebc2d6.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=1621a46e1a7f451e80980d096a6e89e8)
// 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](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F175242%2F56c47b90-c6a9-b1ff-ca0f-5212e8204c7b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d193a8001c56fcf8bc2ae8b96d476ae5)
タップ判定の取得
// 画面
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だけでも十分良いものが作れるような気がしました。
部分的にしかコードを載せていないので、動作しなかったらすいません笑