互联网的那些事儿iOS奋斗iOS开发

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

2017-03-02  本文已影响1941人  张嘉夫

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

现在把制作这个增强现实小游戏的方法分享给大家,只要会 iOS 开发就可以看懂,希望大家都可以做出自己的 Pokemon Go,找到女朋友...

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

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

把敌人钉在地图上

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

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

添加增强现实

现在已经有了一个很棒的 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。你会看到显示了一个白屏:

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 知道了所有需要了解的有关 target 的事情。

打开 ARItem.swift 然后 import SceneKit

import Foundation
import SceneKit
 
struct ARItem {
...
}

然后,添加下面这个属性以存储 item 的 SCNNode

var itemNode: SCNNode?

确保在 ARItem 结构体现有的属性之后定义这个属性,因为我们会依赖定义了相同参数顺序的隐式初始化方法。
现在 Xcode 在 MapViewController.swift 里显示了一个 error。要修复它,打开该文件然后滑动到 setupLocations()
修改 Xcode 在编辑器面板左侧用红点标注的行。

结束触摸

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

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

下一步?

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

上一篇下一篇

猜你喜欢

热点阅读