抖音的潛水艇小遊戲只能玩一會兒,不盡興,於是想着自己開發一個。
ARKit的各種入門介紹這裏就不說了,網上一堆都是,自己注意甄別。
第一步,創建一個具有增強現實功能AR的項目:
選擇語言Swift, SpriteKit是2D遊戲引擎開發框架,考慮到遊戲還是以2D畫面爲主,所以選擇了SpriteKit,SceneKit是3D開發引擎。
第二步,在ViewController中可以開打已經默認導入了ARKit和SpriteKit兩個框架,而且SB中也添加了一個ARSKView實例sceneView,ARSKView可以渲染攝像頭捕捉到的畫面和畫面中添加的每一個節點node,它把ARSession和Spritekit結合了起來,具體代碼:
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.showsPhysics = true
sceneView.showsFPS = true
sceneView.showsNodeCount = true
//檢測手機能不能用人臉追蹤功能
guard ARFaceTrackingConfiguration.isSupported else {
fatalError("Face tracking is not supported on this device")
}
if let view = self.view as! SKView? {
//通過代碼創建一個GameScene類的實例對象 不用項目中自帶的
scene.size = view.bounds.size
sceneView.presentScene(scene)
}
}
在視圖即將出現和即將消失的時候打開和關閉ARSession
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARFaceTrackingConfiguration()
sceneView.session.delegate = self
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
sceneView.delegate = self 代理方法提供了添加節點、節點即將更新、已經更新的方法:
這裏我們先添加一個簡單的圖片上去看下效果
// MARK: - ARSKViewDelegate
func view(_ view: ARSKView, didAdd node: SKNode, for anchor: ARAnchor) {
let image = SKSpriteNode(imageNamed: "速摳圖")
image.size = CGSize(width: 20, height: 20)
image.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "速摳圖"), size: CGSize(width: 20, height: 20))
image.position = CGPoint(x: -60, y: 0)
node.addChild(image)
}
運行可以看到攝像頭識別人臉並讓圖片跟隨人臉移動,當前image默認是以識別到的人臉中心位置爲錨點,所以image.position = CGPoint(x: -60, y: 0)修改了當前節點的位置,偏臉人臉到左邊
到這裏人臉識別追蹤的簡單功能就完成了,接下來就是添加各種2D的模型,潛水艇和柱子,潛水艇的位置跟隨識別到的人臉的位置,再給模型添加物理屬性。
第三步,在Scene中添加潛水艇和柱子模型
image = SKSpriteNode(imageNamed: "速摳圖")
image.size = CGSize(width: 60, height: 60)
image.physicsBody = SKPhysicsBody(rectangleOf:image.size)
image.physicsBody?.affectedByGravity = false
image.physicsBody?.categoryBitMask = birdCategory
image.physicsBody?.contactTestBitMask = pipeCategory
image.position = CGPoint(x: 50, y: 50)
addChild(image)
image位置隨便指一個,我喜歡先看效果再處理
創建柱子節點,柱子節點是多個而且成對出現,上下各一個,隔一段時間出現一對,隔一段時間再出現一對,如此循環
所以這可以看做是一個創建+等待+創建+等待的循環過程
於是:
//MARK:開始重複創建水管
func startCreateRandomPipesAction() {
//創建一個等待的action,等待時間的平均值爲秒,變化範圍爲1秒
let waitAct = SKAction.wait(forDuration: 1.5)
//創建一個產生隨機水管的action,這個action實際上就是我們下面新添加的那個createRandomPipes()方法
let generatePipeAct = SKAction.run {
self.createRandomPipes()
}
//讓場景開始重複循環執行“等待->創建->等待->創建...”
//並且給這個循環的動作設置一個叫做createPipe的key類標識它
run(SKAction.repeatForever(SKAction.sequence([waitAct,generatePipeAct])), withKey: "createPipe")
}
//MARK:具體某一次創建一對水管
func createRandomPipes() {
let height = self.size.height
let pipeGap = CGFloat(arc4random_uniform(60)) + image.size.width*2
let pipeWidth:CGFloat = 60
let topHeight = CGFloat(arc4random_uniform(UInt32(height/2))) + height/4
let bottomPipeHeight = height - pipeGap - topHeight
addPipes(topSize: CGSize(width: pipeWidth, height: topHeight), bottomSize: CGSize(width: pipeWidth, height: bottomPipeHeight))
}
//MARK:添加水管到場景裏
func addPipes(topSize:CGSize,bottomSize:CGSize) {
//創建上水管
guard let image = UIImage(named: "topPipe") else { return }
let topTextture = SKTexture(image: image)
//利用上水管圖片創建一個上水管紋理對象
let topPipe = SKSpriteNode(texture: topTextture, size: topSize)
//利用上水管紋理對象和傳入的上水管大小參數創建一個上水管對象
topPipe.name = "pipe" //給這個水管取個名字叫pipe
topPipe.position = CGPoint(x: self.size.width + topPipe.size.width * 0.5, y: self.size.height - topPipe.size.height * 0.5)
//創建下水管
let bottomTexture = SKTexture(imageNamed: "bottomPipe")
let bottomPipe = SKSpriteNode(texture: bottomTexture, size: bottomSize)
bottomPipe.name = "pipe"
bottomPipe.position = CGPoint(x: self.size.width + bottomPipe.size.width * 0.5, y: bottomPipe.size.height * 0.5)
//將上下水管天驕到場景中
addChild(topPipe)
addChild(bottomPipe)
}
讓柱子移動:
//MARK:移動和移除
func moveScene() {
//make pipe move
for pipeNode in self.children where pipeNode.name == "pipe" {
//因爲我們要用到水管的size,但是SKNode沒有size屬性,所以我們要把它轉成SKSpriteNode
if let pipeSprite = pipeNode as? SKSpriteNode {
//將水管左移2
pipeSprite.position = CGPoint(x: pipeSprite.position.x - 2, y: pipeSprite.position.y)
//檢查水管是否完全超出屏幕左側了,如果是則將它從場景裏移除掉
if pipeSprite.position.x < -pipeSprite.size.width * 0.5 {
pipeSprite.removeFromParent()
}
}
}
}
添加物理屬性:
self.physicsWorld.contactDelegate = self
各個節點添加物理屬性
image.physicsBody = SKPhysicsBody(rectangleOf:image.size)
image.physicsBody?.affectedByGravity = false
image.physicsBody?.categoryBitMask = imageCategory
image.physicsBody?.contactTestBitMask = pipeCategory
實現節點碰撞的代理事件:func didBegin(_ contact: SKPhysicsContact)
func didBegin(_ contact: SKPhysicsContact) {
print("發生碰撞")
}
到這裏,還需要把潛水艇節點的位置和人臉識別位置聯繫起來,這裏我在ARSKViewDelegate裏面獲取到node的識別位置,然後用閉包回調,暫時沒想到其他好方法。
//Scene中添加閉包
var positionCallBack:((_ point:CGPoint)->())?
//didMove方法中處理閉包返回的位置
positionCallBack = {
point in
self.image.position = CGPoint(x: point.x-60, y: point.y)
}
//ViewController中 回調位置
func view(_ view: ARSKView, didUpdate node: SKNode, for anchor: ARAnchor) {
scene.positionCallBack?(node.position)
}
運行可以看到image節點跟隨人臉移動,與移動的柱子接觸後出發碰撞方法,在此方法中做遊戲結束的處理。
源碼地址:地址