女朋友說她要玩 Pokemon Go,所以...

...作為一個程序員有一個女朋友已經是相當不容易,所以...為了滿足她的需求,我準備自己做一個。去谷歌了一下發現 Ray Wenderlich 有一篇類似的教程,就參考這篇教程真的自己做了一個,女朋友玩的很開心。
現在把制作這個增強現實小游戲的方法分享給大家,只要會 iOS 開發就可以看懂,希望大家都可以做出自己的 Pokemon Go,找到女朋友...

在這篇山寨 Pokemon Go 的教程中,會教你創建一個自己的增強現實怪物狩獵游戲。游戲有一個地圖,顯示了你和敵人的位置,一個 3D SceneKit view 以顯示后置攝像頭的實時預覽和敵人的 3D 模型。
如果你不了解增強現實,在開始前花時間閱讀一下 Ray Wenderlich 基于位置的增強現實教程 。這不是學習本篇山寨 Pokemon Go 教程的必要條件,但里面包含了很多關于數學和增強現實有價值的信息,本教程中并不會涉及。
上手
我準備了一個山寨 Pokemon Go 的起始項目,放在了我的 GitHub,下載或克隆一下。項目包含了兩個 view controller 以及文件夾 art.scnassets,里面包含了需要的 3D 模型以及紋理。
ViewController.swift 是 UIViewController
的子類,用于顯示 app 的 AR 部分。MapViewController
會被用于顯示地圖,上面有你的當前位置以及身邊其他敵人的當前位置。基本的約束和 outlet 我已經弄好了,這樣大家就可以專注于本教程中最重要的部分,即山寨 Pokemon Go。
把敵人加到地圖上
在女朋友出門打怪前,她需要知道怪獸都在哪里。創建一個新的 Swift File,命名為 ARItem.swift。
將如下代碼添加到 ARItem.swift 中 import Foundation
行之后:
import CoreLocation
struct ARItem {
let itemDescription: String
let location: CLLocation
}
ARItem
有一個描述和一個位置,以便了解敵人的類型——以及他正躺在哪里等著你。
打開 MapViewController.swift 添加一個 CoreLocation
的 import,再添加一個用于存儲目標的屬性:
var targets = [ARItem]()
現在添加如下方法:
func setupLocations() {
let firstTarget = ARItem(itemDescription: "wolf", location: CLLocation(latitude: 0, longitude: 0))
targets.append(firstTarget)
let secondTarget = ARItem(itemDescription: "wolf", location: CLLocation(latitude: 0, longitude: 0))
targets.append(secondTarget)
let thirdTarget = ARItem(itemDescription: "dragon", location: CLLocation(latitude: 0, longitude: 0))
targets.append(thirdTarget)
}
在這里用硬編碼的方式創建了三個敵人,位置和描述都是硬編碼的。然后要把 (0, 0) 坐標替換為靠近你的物理位置的坐標。
有很多方法可以找到這些位置。例如,可以創建幾個圍繞你當前位置的隨機位置、使用 Ray Wenderlich 最早的增強現實教程中的 PlacesLoader
、甚至使用 Xcode 偽造你的當前位置。但是,你不會希望某個隨機的位置是在隔壁老王的臥室里。那樣就尷尬了。
為了簡化操作,可以使用 GPSSPG 這個在線查詢經緯度的網站。打開網站然后搜索你所在的位置,會出現一個彈出窗口,點擊其他位置也會出現彈出窗口。
在這個彈出窗口里可以看到 5 組經緯度的值,前面是緯度(latitude),后面是經度(longitude)。用高德那組,否則會出現地圖偏移量。我建議你在附近或街上找一些位置來創建硬編碼,這樣你的女朋友就不用告訴老王她需要到他的房間里捉一條龍了。
選擇三個位置,用它們的值替換掉上面的零。
把敵人釘在地圖上
現在已經有敵人的位置了,現在需要顯示 MapView
。添加一個新的 Swift File,保存為 MapAnnotation.swift。在文件中添加如下代碼:
import MapKit
class MapAnnotation: NSObject, MKAnnotation {
//1
let coordinate: CLLocationCoordinate2D
let title: String?
//2
let item: ARItem
//3
init(location: CLLocationCoordinate2D, item: ARItem) {
self.coordinate = location
self.item = item
self.title = item.itemDescription
super.init()
}
我們創建了一個 MapAnnotation
類,實現了 MKAnnoation
協議。說明白一點:
- 該協議需要一個變量
coordinate
和一個可選值title
。 - 在這里存儲屬于該 annotation 的
ARItem
。 - 用該初始化方法可以分配所有變量。
現在回到 MapViewController.swift。添加如下代碼到 setupLocations()
的最后:
for item in targets {
let annotation = MapAnnotation(location: item.location.coordinate, item: item)
self.mapView.addAnnotation(annotation)
}
我們在上面遍歷了 targets
數組并且為每一個 target 都添加了 annotation
。
現在,在 viewDidLoad()
的最后,調用 setupLocations()
:
override func viewDidLoad() {
super.viewDidLoad()
mapView.userTrackingMode = MKUserTrackingMode.followWithHeading
setupLocations()
}
要使用位置,必須先索要權限。為 MapViewController
添加如下屬性:
let locationManager = CLLocationManager()
在 viewDidLoad()
的末尾,添加如下代碼索取所需的權限:
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
注意:如果忘記添加這個權限請求,map view 將無法定位用戶。不幸的是沒有錯誤消息會指出這一點。這會導致每次使用位置服務的時候都無法獲取位置,這樣會比后面搜索尋找錯誤的源頭好的多。
構建運行項目;短時間后,地圖會縮放到你的當前位置,并且在你的敵人的位置上顯示幾個紅色標記。
添加增強現實
現在已經有了一個很棒的 app,但還需要添加增強現實的代碼。在下面幾節中,會添加相機的實時預覽以及一個簡單的小方塊,用作敵人的占位符。
首先需要追蹤用戶的位置。為 MapViewController
添加如下屬性:
var userLocation: CLLocation?
然后在底部添加如下擴展:
extension MapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
self.userLocation = userLocation.location
}
}
每次設備位置更新 MapView
都會調用這個方法;簡單存一下,以用于另一個方法。
在擴展中添加如下代理方法:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
//1
let coordinate = view.annotation!.coordinate
//2
if let userCoordinate = userLocation {
//3
if userCoordinate.distance(from: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) < 50 {
//4
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let viewController = storyboard.instantiateViewController(withIdentifier: "ARViewController") as? ViewController {
// more code later
//5
if let mapAnnotation = view.annotation as? MapAnnotation {
//6
self.present(viewController, animated: true, completion: nil)
}
}
}
}
}
如果用戶點擊距離 50 米以內的敵人,則會顯示相機預覽,過程如下:
- 獲取被選擇的 annotation 的坐標。
- 確保可選值
userLocation
已分配。 - 確保被點擊的對象在用戶的位置范圍以內。
- 從 storyboard 實例化
ARViewController
。 - 這一行檢查被點擊的 annotation 是否是
MapAnnotation
。 - 最后,顯示
viewController
。
構建運行項目,點擊你當前位置附近的某個 annotation。你會看到顯示了一個白屏:
添加相機預覽
打開 ViewController.swift,然后在 SceneKit
的 import 后面 import AVFoundation
import UIKit
import SceneKit
import AVFoundation
class ViewController: UIViewController {
...
然后添加如下屬性以存儲 AVCaptureSession
和 AVCaptureVideoPreviewLayer
:
var cameraSession: AVCaptureSession?
var cameraLayer: AVCaptureVideoPreviewLayer?
使用 capture session 來連接到視頻輸入,比如攝像頭,然后連接到輸出,比如預覽層。
現在添加如下方法:
func createCaptureSession() -> (session: AVCaptureSession?, error: NSError?) {
//1
var error: NSError?
var captureSession: AVCaptureSession?
//2
let backVideoDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back)
//3
if backVideoDevice != nil {
var videoInput: AVCaptureDeviceInput!
do {
videoInput = try AVCaptureDeviceInput(device: backVideoDevice)
} catch let error1 as NSError {
error = error1
videoInput = nil
}
//4
if error == nil {
captureSession = AVCaptureSession()
//5
if captureSession!.canAddInput(videoInput) {
captureSession!.addInput(videoInput)
} else {
error = NSError(domain: "", code: 0, userInfo: ["description": "Error adding video input."])
}
} else {
error = NSError(domain: "", code: 1, userInfo: ["description": "Error creating capture device input."])
}
} else {
error = NSError(domain: "", code: 2, userInfo: ["description": "Back video device not found."])
}
//6
return (session: captureSession, error: error)
}
上面的代碼做了如下事情:
- 創建了幾個變量,用于方法返回。
- 獲取設備的后置攝像頭。
- 如果攝像頭存在,獲取它的輸入。
- 創建
AVCaptureSession
的實例。 - 將視頻設備加為輸入。
- 返回一個元組,包含
captureSession
或是 error。
現在你有了攝像頭的輸入,可以把它加載到視圖中了:
func loadCamera() {
//1
let captureSessionResult = createCaptureSession()
//2
guard captureSessionResult.error == nil, let session = captureSessionResult.session else {
print("Error creating capture session.")
return
}
//3
self.cameraSession = session
//4
if let cameraLayer = AVCaptureVideoPreviewLayer(session: self.cameraSession) {
cameraLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
cameraLayer.frame = self.view.bounds
//5
self.view.layer.insertSublayer(cameraLayer, at: 0)
self.cameraLayer = cameraLayer
}
}
一步步講解上面的方法:
- 首先,調用上面創建的方法來獲得 capture session。
- 如果有錯誤,或者
captureSession
是nil
,就 return。再見了我的增強現實。 - 如果一切正常,就在
cameraSession
里存儲 capture session。 - 這行嘗試創建一個視頻預覽層;如果成功了,它會設置
videoGravity
以及把該層的 frame 設置為 view 的 bounds。這樣會給用戶一個全屏預覽。 - 最后,將該層添加為子圖層,然后將其存儲在
cameraLayer
中。
添加添加如下代碼到 viewDidLoad()
中:
loadCamera()
self.cameraSession?.startRunning()
其實這里就干了兩件事:首先調用剛剛寫的那段卓爾不群的代碼,然后開始從相機捕獲幀。幀將會自動顯示到預覽層上。
構建運行項目,點擊附近的一個位置,然后享受一下全新的相機預覽:
添加小方塊
預覽效果很好,但還不是增強現實——目前還不是。在這一節,我們會為每個敵人添加一個簡單的小方塊,根據用戶的位置和朝向來移動它。
這個小游戲有兩種敵人:狼和龍。因此,我們需要知道面對的是哪種敵人,以及要在哪兒放置它。
把下面的屬性添加到 ViewController
(它會幫你存儲關于敵人的信息):
var target: ARItem!
現在打開 MapViewController.swift,找到 mapView(_:, didSelect:)
然后改變最后一條 if
語句,讓它看起來像這樣:
if let mapAnnotation = view.annotation as? MapAnnotation {
//1
viewController.target = mapAnnotation.item
self.present(viewController, animated: true, completion: nil)
}
- 在顯示
viewController
之前,存儲了被點擊 annotation 的 ARItem 的引用。所以viewController
知道你面對的是什么樣的敵人。
現在 ViewController
知道了所有需要了解的有關 target 的事情。
打開 ARItem.swift 然后 import SceneKit
。
import Foundation
import SceneKit
struct ARItem {
...
}
然后,添加下面這個屬性以存儲 item 的 SCNNode
:
var itemNode: SCNNode?
確保在 ARItem 結構體現有的屬性之后定義這個屬性,因為我們會依賴定義了相同參數順序的隱式初始化方法。
現在 Xcode 在 MapViewController.swift 里顯示了一個 error。要修復它,打開該文件然后滑動到 setupLocations()
。
修改 Xcode 在編輯器面板左側用紅點標注的行。

在每一行,為缺少的 itemNode
參數添加 nil
值。
舉個例子,改變下面的這行:
let firstTarget = ARItem(itemDescription: "wolf", location: CLLocation(latitude: 50.5184, longitude: 8.3902))
…為下面這樣:
let firstTarget = ARItem(itemDescription: "wolf", location: CLLocation(latitude: 50.5184, longitude: 8.3902), itemNode: nil)
現在知道了要顯示的敵人類型,也知道了它的位置,但你還不知道設備的朝向(heading)。
打開 ViewController.swift 然后 import CoreLocation
,你的所有 import 看起來應該如下:
import UIKit
import SceneKit
import AVFoundation
import CoreLocation
接下來,添加如下屬性:
//1
var locationManager = CLLocationManager()
var heading: Double = 0
var userLocation = CLLocation()
//2
let scene = SCNScene()
let cameraNode = SCNNode()
let targetNode = SCNNode(geometry: SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0))
分步講解:
- 使用
CLLocationManager
來接收設備目前的朝向。朝向從真北或磁北極以度數測量。 - 創建了空的
SCNScene
和SCNNode
。targetNode
是一個包含小方塊的SCNNode
。
將如下代碼添加到 viewDidLoad()
的最后:
//1
self.locationManager.delegate = self
//2
self.locationManager.startUpdatingHeading()
//3
sceneView.scene = scene
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
scene.rootNode.addChildNode(cameraNode)
這段代碼相當直接:
- 把
ViewController
設置為CLLocationManager
的代理。 - 調用本行后,就會獲得朝向信息。默認情況下,朝向改變超過 1 度時就會通知代理。
- 這是
SCNView
的一些設置代碼。它創建了一個空的場景,并且添加了一個鏡頭。
為了采用 CLLocationManagerDelegate
協議,為 ViewController
添加如下擴展:
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
//1
self.heading = fmod(newHeading.trueHeading, 360.0)
repositionTarget()
}
}
每次新的朝向信息可用時,CLLocationManager
會調用此委派方法。 fmod
是 double 值的模數函數,確保朝向在 0 到 359 內。
現在為 ViewController.swift 添加 repostionTarget()
,但要加在常規的 implementation 內,而不是在 CLLocationManagerDelegate
擴展里:
func repositionTarget() {
//1
let heading = getHeadingForDirectionFromCoordinate(from: userLocation, to: target.location)
//2
let delta = heading - self.heading
if delta < -15.0 {
leftIndicator.isHidden = false
rightIndicator.isHidden = true
} else if delta > 15 {
leftIndicator.isHidden = true
rightIndicator.isHidden = false
} else {
leftIndicator.isHidden = true
rightIndicator.isHidden = true
}
//3
let distance = userLocation.distance(from: target.location)
//4
if let node = target.itemNode {
//5
if node.parent == nil {
node.position = SCNVector3(x: Float(delta), y: 0, z: Float(-distance))
scene.rootNode.addChildNode(node)
} else {
//6
node.removeAllActions()
node.runAction(SCNAction.move(to: SCNVector3(x: Float(delta), y: 0, z: Float(-distance)), duration: 0.2))
}
}
}
上面每個注釋的部分都做了如下事情:
- 你會在下個步驟里實現這個方法,這就是用來計算當前位置到目標的朝向的。
- 然后計算設備當前朝向和位置朝向的增量值。如果增量小于 -15,顯示左指示器 label。如果大于 15,顯示右指示器 label。如果介于 -15 和 15 之間,把二者都隱藏,因為敵人應該在屏幕上了。
- 這里獲取了設備位置到敵人的距離。
- 如果 item 已分配 node...
- 如果 node 沒有 parent,使用 distance 設置位置,并且把 node 加到場景里。
- 否則,移除所有 action,然后創建一個新 action。
如果你很熟悉 SceneKit 或 SpriteKit,最后一行理解起來應該沒什么問題。如果不是,我會在這里詳細解析一下。
SCNAction.move(to:, duration:)
創建了一個 action,把 node 移動到給定的位置,耗時也是給定的。runAction(_:)
是 SCNOde
的方法,執行了一個 action。你還可以創建 action 的組和/或序列。如果想學習更多,Ray Wenderlich 的書 3D 蘋果游戲教程 是一個很好的資源。
現在來實現缺失的方法。將如下方法添加到 ViewController.swift:
func radiansToDegrees(_ radians: Double) -> Double {
return (radians) * (180.0 / M_PI)
}
func degreesToRadians(_ degrees: Double) -> Double {
return (degrees) * (M_PI / 180.0)
}
func getHeadingForDirectionFromCoordinate(from: CLLocation, to: CLLocation) -> Double {
//1
let fLat = degreesToRadians(from.coordinate.latitude)
let fLng = degreesToRadians(from.coordinate.longitude)
let tLat = degreesToRadians(to.coordinate.latitude)
let tLng = degreesToRadians(to.coordinate.longitude)
//2
let degree = radiansToDegrees(atan2(sin(tLng-fLng)*cos(tLat), cos(fLat)*sin(tLat)-sin(fLat)*cos(tLat)*cos(tLng-fLng)))
//3
if degree >= 0 {
return degree
} else {
return degree + 360
}
}
radiansToDegrees(_:)
和 degreesToRadians(_:)
只是兩個簡單的幫助方法,用于在弧度和角度之間轉換值。
getHeadingForDirectionFromCoordinate(from:to:):
內部發生了這些事情:
- 首先,將經度和緯度的值轉換為弧度。
- 使用這些值,計算朝向,然后將其轉換回角度。
- 如果值為負,則添加 360 度使它為正。這沒有錯,因為 -90 度其實就是 270 度。
還有兩小步,就可以看見我們的工作成果了。
首先,需要將用戶的位置傳遞給 viewController
。打開 MapViewController.swift 然后找到 mapView(_:, didSelect:)
中的最后一個 if
語句,并在顯示 view controller 之前添加下面這行;
viewController.userLocation = mapView.userLocation.location!
現在把如下方法添加到 ViewController:
func setupTarget() {
targetNode.name = "敵人"
self.target.itemNode = targetNode
}
這里只需要給 targetNode
一個名字,并將其分配給 target。現在可以在 viewDidLoad()
方法的末尾調用此方法,就在添加鏡頭 node 之后:
scene.rootNode.addChildNode(cameraNode)
setupTarget()
構建運行項目;看著你那個并不是很有威脅性的小方塊四處移動:

拋光
使用立方體或球這種簡陋的物體構建 app 是一種簡單的方式,不需要花費太多時間搗鼓 3D 模型——但 3D 模型看起來 太 帥 了。在這節中,我們會為游戲添加一些高光,為敵人添加 3D 模型以及拋火球功能。
打開 art.scnassets 文件夾查看兩個 .dae 文件。這些文件包含了敵人的模型:一個狼,另一個是龍。
下一步是更改 ViewController.swift 中的 setupTarget()
以加載其中一個模型并將其分配給目標的 itemNode
屬性。
用如下代碼替換 setupTarget()
的內容:
func setupTarget() {
//1
let scene = SCNScene(named: "art.scnassets/\(target.itemDescription).dae")
//2
let enemy = scene?.rootNode.childNode(withName: target.itemDescription, recursively: true)
//3
if target.itemDescription == "dragon" {
enemy?.position = SCNVector3(x: 0, y: -15, z: 0)
} else {
enemy?.position = SCNVector3(x: 0, y: 0, z: 0)
}
//4
let node = SCNNode()
node.addChildNode(enemy!)
node.name = "敵人"
self.target.itemNode = node
}
上面發生了這些事情:
- 首先把模型加載到場景里。target 的
itemDescription
用 .dae 文件的名字。 - 接下來,遍歷場景,找到一個名為
itemDescription
的 node。只有一個具有此名稱的 node,正好是模型的根 node。 - 然后調整位置,讓兩個模型出現在相同的位置上。如果這兩個模型來自同一個設計器,可能不會需要這個步驟。然而,我使用了來自不同設計器的兩個模型:狼來自 https://3dwarehouse.sketchup.com/,龍來自 https://clara.io 。
- 最后,將模型添加到空 node,然后把它分配給當前 target 的
itemNode
屬性。這是一個小竅門,讓下一節的觸摸處理更簡單一些。
構建運行項目;你會看到一個狼的 3D 模型,看起來比 low 逼的小方塊危險多了!
事實上,狼看起來已經可怕到足夠把你的女朋友嚇跑,但作為一個勇敢的英雄,撤退不是我們的選擇!下面會為她準備幾個小火球,這樣在她成為狼的午餐之前可以把它殺掉。
觸摸結束事件是拋出火球的好時機,因此將以下方法添加到 ViewController.swift:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
//1
let touch = touches.first!
let location = touch.location(in: sceneView)
//2
let hitResult = sceneView.hitTest(location, options: nil)
//3
let fireBall = SCNParticleSystem(named: "Fireball.scnp", inDirectory: nil)
//4
let emitterNode = SCNNode()
emitterNode.position = SCNVector3(x: 0, y: -5, z: 10)
emitterNode.addParticleSystem(fireBall!)
scene.rootNode.addChildNode(emitterNode)
//5
if hitResult.first != nil {
//6
target.itemNode?.runAction(SCNAction.sequence([SCNAction.wait(duration: 0.5), SCNAction.removeFromParentNode(), SCNAction.hide()]))
let moveAction = SCNAction.move(to: target.itemNode!.position, duration: 0.5)
emitterNode.runAction(moveAction)
} else {
//7
emitterNode.runAction(SCNAction.move(to: SCNVector3(x: 0, y: 0, z: -30), duration: 0.5))
}
}
火球工作邏輯:
- 把觸摸轉換為場景里的坐標。
-
hitTest(_, options:)
發送光線跟蹤到給定位置,并為光線跟蹤線上的每個 node 返回一個SCNHitTestResult
數組。 -
從SceneKit
的顆粒文件加載火球的顆粒系統。 - 然后將顆粒系統加載到空節點,并將其放在屏幕外面的底下。這使得后球看起來像來自玩家的位置。
- 如果檢測到點擊...
- ...等待一小段時間,然后刪除包含敵人的
itemNode
。同時把發射器 node 移動到敵人的位置。 - 如果沒有打中,火球只是移動到了固定的位置。
構建運行項目,讓狼在烈焰中燃燒吧!
結束觸摸
要結束游戲,需要從列表中移除敵人,關閉增強現實視圖,回到地圖尋找下一個敵人。
從列表中移除敵人必須在 MapViewController
中完成,因為敵人列表在那里。為此,需要添加一個只帶有一個方法的委托協議,在 target 被擊中時調用。
在 ViewController.swift 中添加如下協議,就在類聲明之上:
protocol ARControllerDelegate {
func viewController(controller: ViewController, tappedTarget: ARItem)
}
還要給 ViewController
添加如下屬性:
var delegate: ARControllerDelegate?
代理協議中的方法告訴代理有一次命中;然后代理可以決定接下來要做什么。
仍然在 ViewController.swift 中,找到 touchesEnded(_:with:)
并將 if
語句的條件代碼塊更改如下:
if hitResult.first != nil {
target.itemNode?.runAction(SCNAction.sequence([SCNAction.wait(duration: 0.5), SCNAction.removeFromParentNode(), SCNAction.hide()]))
//1
let sequence = SCNAction.sequence(
[SCNAction.move(to: target.itemNode!.position, duration: 0.5),
//2
SCNAction.wait(duration: 3.5),
//3
SCNAction.run({_ in
self.delegate?.viewController(controller: self, tappedTarget: self.target)
})])
emitterNode.runAction(sequence)
} else {
...
}
改變解釋如下:
- 將發射器 node 的操作更改為序列,移動操作保持不變。
- 發射器移動后,暫停 3.5 秒。
- 然后通知代理目標被擊中。
打來 MapViewController.swift 添加如下屬性以存儲被選中的 annotation:
var selectedAnnotation: MKAnnotation?
稍后會用到它以從 MapView
移除。
現在找到 mapView(_:, didSelect:)
,并對那個實例化了 ViewController
的條件綁定和塊(即 if let)作出如下改變:
if let viewController = storyboard.instantiateViewController(withIdentifier: "ARViewController") as? ViewController {
//1
viewController.delegate = self
if let mapAnnotation = view.annotation as? MapAnnotation {
viewController.target = mapAnnotation.item
viewController.userLocation = mapView.userLocation.location!
//2
selectedAnnotation = view.annotation
self.present(viewController, animated: true, completion: nil)
}
}
相當簡單:
- 這行把
ViewController
的代理設置為MapViewController
。 - 保存被選中的 annotation。
在 MKMapViewDelegate
擴展下面添加如下代碼:
extension MapViewController: ARControllerDelegate {
func viewController(controller: ViewController, tappedTarget: ARItem) {
//1
self.dismiss(animated: true, completion: nil)
//2
let index = self.targets.index(where: {$0.itemDescription == tappedTarget.itemDescription})
self.targets.remove(at: index!)
if selectedAnnotation != nil {
//3
mapView.removeAnnotation(selectedAnnotation!)
}
}
}
依次思考每個已注釋的部分:
- 首先關閉了增強現實視圖。
- 然后從 target 列表中刪除 target。
- 最后從地圖上移除 annotation。
構建運行,看看最后的成品:
下一步?
我的 GitHub 上有最終項目,帶有上面的全部代碼。
如果你想學習更多,以給這個 app 增加更多可能性,可以看看下面的 Ray Wenderlich 的教程:
- 使用位置和 MapKit,看 Swift 語言 MapKit 介紹。
- 要學習更多有關視頻捕捉的內容,讀一讀 AVFoundation 系列。
- 要更了解 SceneKit,讀一讀 SceneKit 系列教程。
- 要擺脫硬編碼的敵人,你需要提供后端數據。看看如何做一個簡單的 PHP/MySQL 服務,再看看如何用 Vapor 實現服務器端 Swift。
希望你喜歡這篇山寨 Pokemon Go 的教程。如果有任何意見或問題,請在下面評論!