女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码

女朋友说她要玩 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.swiftUIViewController 的子类,用于显示 app 的 AR 部分。MapViewController 会被用于显示地图,上面有你的当前位置以及身边其他敌人的当前位置。基本的约束和 outlet 我已经弄好了,这样大家就可以专注于本教程中最重要的部分,即山寨 Pokemon Go。

把敌人加到地图上

在女朋友出门打怪前,她需要知道怪兽都在哪里。创建一个新的 Swift File,命名为 ARItem.swift

女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码_第1张图片

将如下代码添加到 ARItem.swiftimport 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)。用高德那组,否则会出现地图偏移量。我建议你在附近或街上找一些位置来创建硬编码,这样你的女朋友就不用告诉老王她需要到他的房间里捉一条龙了。

选择三个位置,用它们的值替换掉上面的零。

女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码_第2张图片

把敌人钉在地图上

现在已经有敌人的位置了,现在需要显示 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 协议。说明白一点:

  1. 该协议需要一个变量 coordinate 和一个可选值 title
  2. 在这里存储属于该 annotation 的 ARItem
  3. 用该初始化方法可以分配所有变量。

现在回到 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 将无法定位用户。不幸的是没有错误消息会指出这一点。这会导致每次使用位置服务的时候都无法获取位置,这样会比后面搜索寻找错误的源头好的多。

构建运行项目;短时间后,地图会缩放到你的当前位置,并且在你的敌人的位置上显示几个红色标记。

女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码_第3张图片

添加增强现实

现在已经有了一个很棒的 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 米以内的敌人,则会显示相机预览,过程如下:

  1. 获取被选择的 annotation 的坐标。
  2. 确保可选值 userLocation 已分配。
  3. 确保被点击的对象在用户的位置范围以内。
  4. 从 storyboard 实例化 ARViewController
  5. 这一行检查被点击的 annotation 是否是 MapAnnotation
  6. 最后,显示 viewController

构建运行项目,点击你当前位置附近的某个 annotation。你会看到显示了一个白屏:

女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码_第4张图片
IMG_0109.PNG

添加相机预览

打开 ViewController.swift,然后在 SceneKit 的 import 后面 import AVFoundation

 import UIKit
 import SceneKit
 import AVFoundation
  
 class ViewController: UIViewController {
 ...

然后添加如下属性以存储 AVCaptureSessionAVCaptureVideoPreviewLayer

 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)
}

上面的代码做了如下事情:

  1. 创建了几个变量,用于方法返回。
  2. 获取设备的后置摄像头。
  3. 如果摄像头存在,获取它的输入。
  4. 创建 AVCaptureSession 的实例。
  5. 将视频设备加为输入。
  6. 返回一个元组,包含 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
  }
}

一步步讲解上面的方法:

  1. 首先,调用上面创建的方法来获得 capture session。
  2. 如果有错误,或者 captureSessionnil,就 return。再见了我的增强现实。
  3. 如果一切正常,就在 cameraSession 里存储 capture session。
  4. 这行尝试创建一个视频预览层;如果成功了,它会设置 videoGravity 以及把该层的 frame 设置为 view 的 bounds。这样会给用户一个全屏预览。
  5. 最后,将该层添加为子图层,然后将其存储在 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))

分步讲解:

  1. 使用 CLLocationManager 来接收设备目前的朝向。朝向从真北或磁北极以度数测量。
  2. 创建了空的 SCNSceneSCNNodetargetNode 是一个包含小方块的 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)

这段代码相当直接:

  1. ViewController 设置为 CLLocationManager 的代理。
  2. 调用本行后,就会获得朝向信息。默认情况下,朝向改变超过 1 度时就会通知代理。
  3. 这是 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))
    }
  }
}

上面每个注释的部分都做了如下事情:

  1. 你会在下个步骤里实现这个方法,这就是用来计算当前位置到目标的朝向的。
  2. 然后计算设备当前朝向和位置朝向的增量值。如果增量小于 -15,显示左指示器 label。如果大于 15,显示右指示器 label。如果介于 -15 和 15 之间,把二者都隐藏,因为敌人应该在屏幕上了。
  3. 这里获取了设备位置到敌人的距离。
  4. 如果 item 已分配 node...
  5. 如果 node 没有 parent,使用 distance 设置位置,并且把 node 加到场景里。
  6. 否则,移除所有 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:): 内部发生了这些事情:

  1. 首先,将经度和纬度的值转换为弧度。
  2. 使用这些值,计算朝向,然后将其转换回角度。
  3. 如果值为负,则添加 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
}

上面发生了这些事情:

  1. 首先把模型加载到场景里。target 的 itemDescription.dae 文件的名字。
  2. 接下来,遍历场景,找到一个名为 itemDescription 的 node。只有一个具有此名称的 node,正好是模型的根 node。
  3. 然后调整位置,让两个模型出现在相同的位置上。如果这两个模型来自同一个设计器,可能不会需要这个步骤。然而,我使用了来自不同设计器的两个模型:狼来自 https://3dwarehouse.sketchup.com/,龙来自 https://clara.io 。
  4. 最后,将模型添加到空 node,然后把它分配给当前 target 的 itemNode 属性。这是一个小窍门,让下一节的触摸处理更简单一些。

构建运行项目;你会看到一个狼的 3D 模型,看起来比 low 逼的小方块危险多了!
事实上,狼看起来已经可怕到足够把你的女朋友吓跑,但作为一个勇敢的英雄,撤退不是我们的选择!下面会为她准备几个小火球,这样在她成为狼的午餐之前可以把它杀掉。
触摸结束事件是抛出火球的好时机,因此将以下方法添加到 ViewController.swift

override func touchesEnded(_ touches: Set, 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))
  }
}

火球工作逻辑:

  1. 把触摸转换为场景里的坐标。
  2. hitTest(_, options:) 发送光线跟踪到给定位置,并为光线跟踪线上的每个 node 返回一个 SCNHitTestResult 数组。
  3. 从SceneKit 的颗粒文件加载火球的颗粒系统。
  4. 然后将颗粒系统加载到空节点,并将其放在屏幕外面的底下。这使得后球看起来像来自玩家的位置。
  5. 如果检测到点击...
  6. ...等待一小段时间,然后删除包含敌人的 itemNode。同时把发射器 node 移动到敌人的位置。
  7. 如果没有打中,火球只是移动到了固定的位置。

构建运行项目,让狼在烈焰中燃烧吧!

结束触摸

要结束游戏,需要从列表中移除敌人,关闭增强现实视图,回到地图寻找下一个敌人。
从列表中移除敌人必须在 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 {
  ...
}

改变解释如下:

  1. 将发射器 node 的操作更改为序列,移动操作保持不变。
  2. 发射器移动后,暂停 3.5 秒。
  3. 然后通知代理目标被击中。

打来 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)
  }
}

相当简单:

  1. 这行把 ViewController 的代理设置为 MapViewController
  2. 保存被选中的 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!)
    }
  }
}

依次思考每个已注释的部分:

  1. 首先关闭了增强现实视图。
  2. 然后从 target 列表中删除 target。
  3. 最后从地图上移除 annotation。

构建运行,看看最后的成品:

女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码_第5张图片

下一步?

我的 GitHub 上有最终项目,带有上面的全部代码。
如果你想学习更多,以给这个 app 增加更多可能性,可以看看下面的 Ray Wenderlich 的教程:

  • 使用位置和 MapKit,看 Swift 语言 MapKit 介绍。
  • 要学习更多有关视频捕捉的内容,读一读 AVFoundation 系列。
  • 要更了解 SceneKit,读一读 SceneKit 系列教程。
  • 要摆脱硬编码的敌人,你需要提供后端数据。看看如何做一个简单的 PHP/MySQL 服务,再看看如何用 Vapor 实现服务器端 Swift。
    希望你喜欢这篇山寨 Pokemon Go 的教程。如果有任何意见或问题,请在下面评论!

你可能感兴趣的:(女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源代码)