美文网首页互联网的那些事儿iOS奋斗iOS开发
女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源

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

作者: 张嘉夫 | 来源:发表于2017-03-02 19:32 被阅读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 之前,存储了被点击 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 在编辑器面板左侧用红点标注的行。

    结束触摸

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

    相关文章

      网友评论

      • 麦哲文:omg,来晚了,粉了再说
      • 吕唯:你好!我叫吕唯。我看了好几篇你写的文章,觉得很好。很有想法!我们在做AR方面的创业项目。不知道能否加个微信,以后有机会可以交流交流哈哈。谢谢!我微信是386911226。
      • Heikki_:大神 打完龙 就崩溃了呢
      • helloDolin:膜拜
      • 诸子百家谁的天下:向大神膜拜
      • 靜學:向大神膜拜:pray:
      • Codepgq:大神 收下我的膝盖
      • 丁建雄:看到了当年我们一起作驴岛的日子,怀念~
        张嘉夫:@丁建雄 哈哈哈 技术都差不多
      • BYQiu:膜拜一下:smiley:
      • 卜丁:向前辈学习:pray:

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

      本文链接:https://www.haomeiwen.com/subject/femhgttx.html