抖音的潜水艇小游戏只能玩一会儿,不尽兴,于是想着自己开发一个。
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节点跟随人脸移动,与移动的柱子接触后出发碰撞方法,在此方法中做游戏结束的处理。
源码地址:地址