女朋友說(shuō)她要玩 Pokemon Go,所以...
...作為一個(gè)程序員有一個(gè)女朋友已經(jīng)是相當(dāng)不容易赦政,所以...為了滿足她的需求胜宇,我準(zhǔn)備自己做一個(gè)耀怜。去谷歌了一下發(fā)現(xiàn) Ray Wenderlich 有一篇類似的教程,就參考這篇教程真的自己做了一個(gè)桐愉,女朋友玩的很開(kāi)心财破。
現(xiàn)在把制作這個(gè)增強(qiáng)現(xiàn)實(shí)小游戲的方法分享給大家,只要會(huì) iOS 開(kāi)發(fā)就可以看懂从诲,希望大家都可以做出自己的 Pokemon Go左痢,找到女朋友...
在這篇山寨 Pokemon Go 的教程中,會(huì)教你創(chuàng)建一個(gè)自己的增強(qiáng)現(xiàn)實(shí)怪物狩獵游戲系洛。游戲有一個(gè)地圖俊性,顯示了你和敵人的位置,一個(gè) 3D SceneKit view 以顯示后置攝像頭的實(shí)時(shí)預(yù)覽和敵人的 3D 模型描扯。
如果你不了解增強(qiáng)現(xiàn)實(shí)定页,在開(kāi)始前花時(shí)間閱讀一下 Ray Wenderlich 基于位置的增強(qiáng)現(xiàn)實(shí)教程 。這不是學(xué)習(xí)本篇山寨 Pokemon Go 教程的必要條件荆烈,但里面包含了很多關(guān)于數(shù)學(xué)和增強(qiáng)現(xiàn)實(shí)有價(jià)值的信息拯勉,本教程中并不會(huì)涉及。
上手
我準(zhǔn)備了一個(gè)山寨 Pokemon Go 的起始項(xiàng)目憔购,放在了我的 GitHub宫峦,下載或克隆一下。項(xiàng)目包含了兩個(gè) view controller 以及文件夾 art.scnassets玫鸟,里面包含了需要的 3D 模型以及紋理导绷。
ViewController.swift 是 UIViewController
的子類,用于顯示 app 的 AR 部分屎飘。MapViewController
會(huì)被用于顯示地圖妥曲,上面有你的當(dāng)前位置以及身邊其他敵人的當(dāng)前位置∏展海基本的約束和 outlet 我已經(jīng)弄好了檐盟,這樣大家就可以專注于本教程中最重要的部分,即山寨 Pokemon Go押桃。
把敵人加到地圖上
在女朋友出門(mén)打怪前葵萎,她需要知道怪獸都在哪里。創(chuàng)建一個(gè)新的 Swift File唱凯,命名為 ARItem.swift羡忘。
將如下代碼添加到 ARItem.swift 中 import Foundation
行之后:
import CoreLocation
struct ARItem {
let itemDescription: String
let location: CLLocation
}
ARItem
有一個(gè)描述和一個(gè)位置,以便了解敵人的類型——以及他正躺在哪里等著你磕昼。
打開(kāi) MapViewController.swift 添加一個(gè) CoreLocation
的 import卷雕,再添加一個(gè)用于存儲(chǔ)目標(biāo)的屬性:
var targets = [ARItem]()
現(xiàn)在添加如下方法:
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)
}
在這里用硬編碼的方式創(chuàng)建了三個(gè)敵人,位置和描述都是硬編碼的票从。然后要把 (0, 0) 坐標(biāo)替換為靠近你的物理位置的坐標(biāo)漫雕。
有很多方法可以找到這些位置滨嘱。例如,可以創(chuàng)建幾個(gè)圍繞你當(dāng)前位置的隨機(jī)位置蝎亚、使用 Ray Wenderlich 最早的增強(qiáng)現(xiàn)實(shí)教程中的 PlacesLoader
九孩、甚至使用 Xcode 偽造你的當(dāng)前位置先馆。但是发框,你不會(huì)希望某個(gè)隨機(jī)的位置是在隔壁老王的臥室里。那樣就尷尬了煤墙。
為了簡(jiǎn)化操作梅惯,可以使用 GPSSPG 這個(gè)在線查詢經(jīng)緯度的網(wǎng)站。打開(kāi)網(wǎng)站然后搜索你所在的位置仿野,會(huì)出現(xiàn)一個(gè)彈出窗口铣减,點(diǎn)擊其他位置也會(huì)出現(xiàn)彈出窗口。
在這個(gè)彈出窗口里可以看到 5 組經(jīng)緯度的值脚作,前面是緯度(latitude)葫哗,后面是經(jīng)度(longitude)。用高德那組球涛,否則會(huì)出現(xiàn)地圖偏移量劣针。我建議你在附近或街上找一些位置來(lái)創(chuàng)建硬編碼,這樣你的女朋友就不用告訴老王她需要到他的房間里捉一條龍了亿扁。
選擇三個(gè)位置捺典,用它們的值替換掉上面的零。
把敵人釘在地圖上
現(xiàn)在已經(jīng)有敵人的位置了从祝,現(xiàn)在需要顯示 MapView
襟己。添加一個(gè)新的 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()
}
我們創(chuàng)建了一個(gè) MapAnnotation
類擎浴,實(shí)現(xiàn)了 MKAnnoation
協(xié)議。說(shuō)明白一點(diǎn):
- 該協(xié)議需要一個(gè)變量
coordinate
和一個(gè)可選值title
毒涧。 - 在這里存儲(chǔ)屬于該 annotation 的
ARItem
贮预。 - 用該初始化方法可以分配所有變量。
現(xiàn)在回到 MapViewController.swift链嘀。添加如下代碼到 setupLocations()
的最后:
for item in targets {
let annotation = MapAnnotation(location: item.location.coordinate, item: item)
self.mapView.addAnnotation(annotation)
}
我們?cè)谏厦姹闅v了 targets
數(shù)組并且為每一個(gè) target 都添加了 annotation
萌狂。
現(xiàn)在,在 viewDidLoad()
的最后怀泊,調(diào)用 setupLocations()
:
override func viewDidLoad() {
super.viewDidLoad()
mapView.userTrackingMode = MKUserTrackingMode.followWithHeading
setupLocations()
}
要使用位置茫藏,必須先索要權(quán)限。為 MapViewController
添加如下屬性:
let locationManager = CLLocationManager()
在 viewDidLoad()
的末尾霹琼,添加如下代碼索取所需的權(quán)限:
if CLLocationManager.authorizationStatus() == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
注意:如果忘記添加這個(gè)權(quán)限請(qǐng)求务傲,map view 將無(wú)法定位用戶凉当。不幸的是沒(méi)有錯(cuò)誤消息會(huì)指出這一點(diǎn)。這會(huì)導(dǎo)致每次使用位置服務(wù)的時(shí)候都無(wú)法獲取位置售葡,這樣會(huì)比后面搜索尋找錯(cuò)誤的源頭好的多看杭。
構(gòu)建運(yùn)行項(xiàng)目;短時(shí)間后挟伙,地圖會(huì)縮放到你的當(dāng)前位置楼雹,并且在你的敵人的位置上顯示幾個(gè)紅色標(biāo)記。
添加增強(qiáng)現(xiàn)實(shí)
現(xiàn)在已經(jīng)有了一個(gè)很棒的 app尖阔,但還需要添加增強(qiáng)現(xiàn)實(shí)的代碼贮缅。在下面幾節(jié)中,會(huì)添加相機(jī)的實(shí)時(shí)預(yù)覽以及一個(gè)簡(jiǎn)單的小方塊介却,用作敵人的占位符谴供。
首先需要追蹤用戶的位置。為 MapViewController
添加如下屬性:
var userLocation: CLLocation?
然后在底部添加如下擴(kuò)展:
extension MapViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
self.userLocation = userLocation.location
}
}
每次設(shè)備位置更新 MapView
都會(huì)調(diào)用這個(gè)方法齿坷;簡(jiǎn)單存一下桂肌,以用于另一個(gè)方法。
在擴(kuò)展中添加如下代理方法:
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)
}
}
}
}
}
如果用戶點(diǎn)擊距離 50 米以內(nèi)的敵人永淌,則會(huì)顯示相機(jī)預(yù)覽垒酬,過(guò)程如下:
- 獲取被選擇的 annotation 的坐標(biāo)秧秉。
- 確狈蛲梗可選值
userLocation
已分配秦叛。 - 確保被點(diǎn)擊的對(duì)象在用戶的位置范圍以內(nèi)。
- 從 storyboard 實(shí)例化
ARViewController
答恶。 - 這一行檢查被點(diǎn)擊的 annotation 是否是
MapAnnotation
饺蚊。 - 最后,顯示
viewController
悬嗓。
構(gòu)建運(yùn)行項(xiàng)目污呼,點(diǎn)擊你當(dāng)前位置附近的某個(gè) annotation。你會(huì)看到顯示了一個(gè)白屏:
添加相機(jī)預(yù)覽
打開(kāi) ViewController.swift包竹,然后在 SceneKit
的 import 后面 import AVFoundation
import UIKit
import SceneKit
import AVFoundation
class ViewController: UIViewController {
...
然后添加如下屬性以存儲(chǔ) AVCaptureSession
和 AVCaptureVideoPreviewLayer
:
var cameraSession: AVCaptureSession?
var cameraLayer: AVCaptureVideoPreviewLayer?
使用 capture session 來(lái)連接到視頻輸入燕酷,比如攝像頭,然后連接到輸出周瞎,比如預(yù)覽層苗缩。
現(xiàn)在添加如下方法:
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)
}
上面的代碼做了如下事情:
- 創(chuàng)建了幾個(gè)變量,用于方法返回声诸。
- 獲取設(shè)備的后置攝像頭酱讶。
- 如果攝像頭存在,獲取它的輸入彼乌。
- 創(chuàng)建
AVCaptureSession
的實(shí)例泻肯。 - 將視頻設(shè)備加為輸入渊迁。
- 返回一個(gè)元組,包含
captureSession
或是 error灶挟。
現(xiàn)在你有了攝像頭的輸入琉朽,可以把它加載到視圖中了:
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
}
}
一步步講解上面的方法:
- 首先,調(diào)用上面創(chuàng)建的方法來(lái)獲得 capture session稚铣。
- 如果有錯(cuò)誤箱叁,或者
captureSession
是nil
,就 return榛泛。再見(jiàn)了我的增強(qiáng)現(xiàn)實(shí)蝌蹂。 - 如果一切正常噩斟,就在
cameraSession
里存儲(chǔ) capture session曹锨。 - 這行嘗試創(chuàng)建一個(gè)視頻預(yù)覽層;如果成功了剃允,它會(huì)設(shè)置
videoGravity
以及把該層的 frame 設(shè)置為 view 的 bounds沛简。這樣會(huì)給用戶一個(gè)全屏預(yù)覽。 - 最后斥废,將該層添加為子圖層椒楣,然后將其存儲(chǔ)在
cameraLayer
中。
添加添加如下代碼到 viewDidLoad()
中:
loadCamera()
self.cameraSession?.startRunning()
其實(shí)這里就干了兩件事:首先調(diào)用剛剛寫(xiě)的那段卓爾不群的代碼牡肉,然后開(kāi)始從相機(jī)捕獲幀捧灰。幀將會(huì)自動(dòng)顯示到預(yù)覽層上。
構(gòu)建運(yùn)行項(xiàng)目统锤,點(diǎn)擊附近的一個(gè)位置毛俏,然后享受一下全新的相機(jī)預(yù)覽:
添加小方塊
預(yù)覽效果很好,但還不是增強(qiáng)現(xiàn)實(shí)——目前還不是饲窿。在這一節(jié)煌寇,我們會(huì)為每個(gè)敵人添加一個(gè)簡(jiǎn)單的小方塊,根據(jù)用戶的位置和朝向來(lái)移動(dòng)它逾雄。
這個(gè)小游戲有兩種敵人:狼和龍阀溶。因此,我們需要知道面對(duì)的是哪種敵人鸦泳,以及要在哪兒放置它银锻。
把下面的屬性添加到 ViewController
(它會(huì)幫你存儲(chǔ)關(guān)于敵人的信息):
var target: ARItem!
現(xiàn)在打開(kāi) MapViewController.swift,找到 mapView(_:, didSelect:)
然后改變最后一條 if
語(yǔ)句做鹰,讓它看起來(lái)像這樣:
if let mapAnnotation = view.annotation as? MapAnnotation {
//1
viewController.target = mapAnnotation.item
self.present(viewController, animated: true, completion: nil)
}
- 在顯示
viewController
之前击纬,存儲(chǔ)了被點(diǎn)擊 annotation 的 ARItem 的引用。所以viewController
知道你面對(duì)的是什么樣的敵人誊垢。
現(xiàn)在 ViewController
知道了所有需要了解的有關(guān) target 的事情掉弛。
打開(kāi) ARItem.swift 然后 import SceneKit
症见。
import Foundation
import SceneKit
struct ARItem {
...
}
然后,添加下面這個(gè)屬性以存儲(chǔ) item 的 SCNNode
:
var itemNode: SCNNode?
確保在 ARItem 結(jié)構(gòu)體現(xiàn)有的屬性之后定義這個(gè)屬性殃饿,因?yàn)槲覀儠?huì)依賴定義了相同參數(shù)順序的隱式初始化方法谋作。
現(xiàn)在 Xcode 在 MapViewController.swift 里顯示了一個(gè) error。要修復(fù)它乎芳,打開(kāi)該文件然后滑動(dòng)到 setupLocations()
遵蚜。
修改 Xcode 在編輯器面板左側(cè)用紅點(diǎn)標(biāo)注的行。
在每一行奈惑,為缺少的 itemNode
參數(shù)添加 nil
值吭净。
舉個(gè)例子,改變下面的這行:
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)
現(xiàn)在知道了要顯示的敵人類型肴甸,也知道了它的位置寂殉,但你還不知道設(shè)備的朝向(heading)。
打開(kāi) ViewController.swift 然后 import CoreLocation
原在,你的所有 import 看起來(lái)應(yīng)該如下:
import UIKit
import SceneKit
import AVFoundation
import CoreLocation
接下來(lái)友扰,添加如下屬性:
//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
來(lái)接收設(shè)備目前的朝向。朝向從真北或磁北極以度數(shù)測(cè)量庶柿。 - 創(chuàng)建了空的
SCNScene
和SCNNode
村怪。targetNode
是一個(gè)包含小方塊的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)
這段代碼相當(dāng)直接:
- 把
ViewController
設(shè)置為CLLocationManager
的代理浮庐。 - 調(diào)用本行后甚负,就會(huì)獲得朝向信息。默認(rèn)情況下审残,朝向改變超過(guò) 1 度時(shí)就會(huì)通知代理梭域。
- 這是
SCNView
的一些設(shè)置代碼。它創(chuàng)建了一個(gè)空的場(chǎng)景维苔,并且添加了一個(gè)鏡頭碰辅。
為了采用 CLLocationManagerDelegate
協(xié)議,為 ViewController
添加如下擴(kuò)展:
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
//1
self.heading = fmod(newHeading.trueHeading, 360.0)
repositionTarget()
}
}
每次新的朝向信息可用時(shí)介时,CLLocationManager
會(huì)調(diào)用此委派方法没宾。 fmod
是 double 值的模數(shù)函數(shù),確保朝向在 0 到 359 內(nèi)沸柔。
現(xiàn)在為 ViewController.swift 添加 repostionTarget()
循衰,但要加在常規(guī)的 implementation 內(nèi),而不是在 CLLocationManagerDelegate
擴(kuò)展里:
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))
}
}
}
上面每個(gè)注釋的部分都做了如下事情:
- 你會(huì)在下個(gè)步驟里實(shí)現(xiàn)這個(gè)方法褐澎,這就是用來(lái)計(jì)算當(dāng)前位置到目標(biāo)的朝向的会钝。
- 然后計(jì)算設(shè)備當(dāng)前朝向和位置朝向的增量值。如果增量小于 -15,顯示左指示器 label迁酸。如果大于 15先鱼,顯示右指示器 label。如果介于 -15 和 15 之間奸鬓,把二者都隱藏焙畔,因?yàn)閿橙藨?yīng)該在屏幕上了。
- 這里獲取了設(shè)備位置到敵人的距離串远。
- 如果 item 已分配 node...
- 如果 node 沒(méi)有 parent宏多,使用 distance 設(shè)置位置,并且把 node 加到場(chǎng)景里澡罚。
- 否則伸但,移除所有 action,然后創(chuàng)建一個(gè)新 action留搔。
如果你很熟悉 SceneKit 或 SpriteKit更胖,最后一行理解起來(lái)應(yīng)該沒(méi)什么問(wèn)題。如果不是催式,我會(huì)在這里詳細(xì)解析一下函喉。
SCNAction.move(to:, duration:)
創(chuàng)建了一個(gè) action,把 node 移動(dòng)到給定的位置荣月,耗時(shí)也是給定的。runAction(_:)
是 SCNOde
的方法梳毙,執(zhí)行了一個(gè) action哺窄。你還可以創(chuàng)建 action 的組和/或序列。如果想學(xué)習(xí)更多账锹,Ray Wenderlich 的書(shū) 3D 蘋(píng)果游戲教程 是一個(gè)很好的資源萌业。
現(xiàn)在來(lái)實(shí)現(xiàn)缺失的方法。將如下方法添加到 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(_:)
只是兩個(gè)簡(jiǎn)單的幫助方法奸柬,用于在弧度和角度之間轉(zhuǎn)換值生年。
getHeadingForDirectionFromCoordinate(from:to:):
內(nèi)部發(fā)生了這些事情:
- 首先,將經(jīng)度和緯度的值轉(zhuǎn)換為弧度廓奕。
- 使用這些值抱婉,計(jì)算朝向,然后將其轉(zhuǎn)換回角度桌粉。
- 如果值為負(fù)蒸绩,則添加 360 度使它為正。這沒(méi)有錯(cuò)铃肯,因?yàn)?-90 度其實(shí)就是 270 度患亿。
還有兩小步,就可以看見(jiàn)我們的工作成果了押逼。
首先步藕,需要將用戶的位置傳遞給 viewController
惦界。打開(kāi) MapViewController.swift 然后找到 mapView(_:, didSelect:)
中的最后一個(gè) if
語(yǔ)句,并在顯示 view controller 之前添加下面這行咙冗;
viewController.userLocation = mapView.userLocation.location!
現(xiàn)在把如下方法添加到 ViewController:
func setupTarget() {
targetNode.name = "敵人"
self.target.itemNode = targetNode
}
這里只需要給 targetNode
一個(gè)名字表锻,并將其分配給 target。現(xiàn)在可以在 viewDidLoad()
方法的末尾調(diào)用此方法乞娄,就在添加鏡頭 node 之后:
scene.rootNode.addChildNode(cameraNode)
setupTarget()
構(gòu)建運(yùn)行項(xiàng)目瞬逊;看著你那個(gè)并不是很有威脅性的小方塊四處移動(dòng):
拋光
使用立方體或球這種簡(jiǎn)陋的物體構(gòu)建 app 是一種簡(jiǎn)單的方式,不需要花費(fèi)太多時(shí)間搗鼓 3D 模型——但 3D 模型看起來(lái) 太 帥 了仪或。在這節(jié)中确镊,我們會(huì)為游戲添加一些高光,為敵人添加 3D 模型以及拋火球功能范删。
打開(kāi) art.scnassets 文件夾查看兩個(gè) .dae 文件蕾域。這些文件包含了敵人的模型:一個(gè)狼,另一個(gè)是龍到旦。
下一步是更改 ViewController.swift 中的 setupTarget()
以加載其中一個(gè)模型并將其分配給目標(biāo)的 itemNode
屬性旨巷。
用如下代碼替換 setupTarget()
的內(nèi)容:
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
}
上面發(fā)生了這些事情:
- 首先把模型加載到場(chǎng)景里。target 的
itemDescription
用 .dae 文件的名字添忘。 - 接下來(lái)采呐,遍歷場(chǎng)景,找到一個(gè)名為
itemDescription
的 node搁骑。只有一個(gè)具有此名稱的 node斧吐,正好是模型的根 node。 - 然后調(diào)整位置仲器,讓兩個(gè)模型出現(xiàn)在相同的位置上煤率。如果這兩個(gè)模型來(lái)自同一個(gè)設(shè)計(jì)器,可能不會(huì)需要這個(gè)步驟乏冀。然而蝶糯,我使用了來(lái)自不同設(shè)計(jì)器的兩個(gè)模型:狼來(lái)自 https://3dwarehouse.sketchup.com/,龍來(lái)自 https://clara.io 辆沦。
- 最后昼捍,將模型添加到空 node,然后把它分配給當(dāng)前 target 的
itemNode
屬性众辨。這是一個(gè)小竅門(mén)端三,讓下一節(jié)的觸摸處理更簡(jiǎn)單一些。
構(gòu)建運(yùn)行項(xiàng)目鹃彻;你會(huì)看到一個(gè)狼的 3D 模型郊闯,看起來(lái)比 low 逼的小方塊危險(xiǎn)多了!
事實(shí)上,狼看起來(lái)已經(jīng)可怕到足夠把你的女朋友嚇跑团赁,但作為一個(gè)勇敢的英雄育拨,撤退不是我們的選擇!下面會(huì)為她準(zhǔn)備幾個(gè)小火球欢摄,這樣在她成為狼的午餐之前可以把它殺掉熬丧。
觸摸結(jié)束事件是拋出火球的好時(shí)機(jī),因此將以下方法添加到 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))
}
}
火球工作邏輯:
- 把觸摸轉(zhuǎn)換為場(chǎng)景里的坐標(biāo)怀挠。
-
hitTest(_, options:)
發(fā)送光線跟蹤到給定位置析蝴,并為光線跟蹤線上的每個(gè) node 返回一個(gè)SCNHitTestResult
數(shù)組。 -
從SceneKit
的顆粒文件加載火球的顆粒系統(tǒng)绿淋。 - 然后將顆粒系統(tǒng)加載到空節(jié)點(diǎn)闷畸,并將其放在屏幕外面的底下。這使得后球看起來(lái)像來(lái)自玩家的位置吞滞。
- 如果檢測(cè)到點(diǎn)擊...
- ...等待一小段時(shí)間佑菩,然后刪除包含敵人的
itemNode
。同時(shí)把發(fā)射器 node 移動(dòng)到敵人的位置裁赠。 - 如果沒(méi)有打中殿漠,火球只是移動(dòng)到了固定的位置。
構(gòu)建運(yùn)行項(xiàng)目佩捞,讓狼在烈焰中燃燒吧绞幌!
結(jié)束觸摸
要結(jié)束游戲,需要從列表中移除敵人失尖,關(guān)閉增強(qiáng)現(xiàn)實(shí)視圖啊奄,回到地圖尋找下一個(gè)敵人。
從列表中移除敵人必須在 MapViewController
中完成掀潮,因?yàn)閿橙肆斜碓谀抢铩榇饲砀唬枰砑右粋€(gè)只帶有一個(gè)方法的委托協(xié)議仪吧,在 target 被擊中時(shí)調(diào)用。
在 ViewController.swift 中添加如下協(xié)議鞠眉,就在類聲明之上:
protocol ARControllerDelegate {
func viewController(controller: ViewController, tappedTarget: ARItem)
}
還要給 ViewController
添加如下屬性:
var delegate: ARControllerDelegate?
代理協(xié)議中的方法告訴代理有一次命中薯鼠;然后代理可以決定接下來(lái)要做什么。
仍然在 ViewController.swift 中械蹋,找到 touchesEnded(_:with:)
并將 if
語(yǔ)句的條件代碼塊更改如下:
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 {
...
}
改變解釋如下:
- 將發(fā)射器 node 的操作更改為序列出皇,移動(dòng)操作保持不變。
- 發(fā)射器移動(dòng)后哗戈,暫停 3.5 秒郊艘。
- 然后通知代理目標(biāo)被擊中。
打來(lái) MapViewController.swift 添加如下屬性以存儲(chǔ)被選中的 annotation:
var selectedAnnotation: MKAnnotation?
稍后會(huì)用到它以從 MapView
移除。
現(xiàn)在找到 mapView(_:, didSelect:)
纱注,并對(duì)那個(gè)實(shí)例化了 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)
}
}
相當(dāng)簡(jiǎn)單:
- 這行把
ViewController
的代理設(shè)置為MapViewController
畏浆。 - 保存被選中的 annotation。
在 MKMapViewDelegate
擴(kuò)展下面添加如下代碼:
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!)
}
}
}
依次思考每個(gè)已注釋的部分:
- 首先關(guān)閉了增強(qiáng)現(xiàn)實(shí)視圖狞贱。
- 然后從 target 列表中刪除 target刻获。
- 最后從地圖上移除 annotation。
構(gòu)建運(yùn)行瞎嬉,看看最后的成品:
下一步蝎毡?
我的 GitHub 上有最終項(xiàng)目,帶有上面的全部代碼氧枣。
如果你想學(xué)習(xí)更多沐兵,以給這個(gè) app 增加更多可能性,可以看看下面的 Ray Wenderlich 的教程:
- 使用位置和 MapKit挑胸,看 Swift 語(yǔ)言 MapKit 介紹痒筒。
- 要學(xué)習(xí)更多有關(guān)視頻捕捉的內(nèi)容,讀一讀 AVFoundation 系列茬贵。
- 要更了解 SceneKit簿透,讀一讀 SceneKit 系列教程。
- 要擺脫硬編碼的敵人解藻,你需要提供后端數(shù)據(jù)老充。看看如何做一個(gè)簡(jiǎn)單的 PHP/MySQL 服務(wù)螟左,再看看如何用 Vapor 實(shí)現(xiàn)服務(wù)器端 Swift啡浊。
希望你喜歡這篇山寨 Pokemon Go 的教程。如果有任何意見(jiàn)或問(wèn)題胶背,請(qǐng)?jiān)谙旅嬖u(píng)論巷嚣!