女朋友要玩 Pokemon Go,所以我就山寨了一个…附带全部源
女朋友说她要玩 Pokemon Go,所以...
现在把制作这个增强现实小游戏的方法分享给大家,只要会 iOS 开发就可以看懂,希望大家都可以做出自己的 Pokemon Go,找到女朋友...
将如下代码添加到 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。你会看到显示了一个白屏:
IMG_0109.PNG添加相机预览
打开 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 在编辑器面板左侧用红点标注的行。
结束触摸
要结束游戏,需要从列表中移除敌人,关闭增强现实视图,回到地图寻找下一个敌人。
从列表中移除敌人必须在 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 的教程。如果有任何意见或问题,请在下面评论!