抖音的潜水艇小游戏只能玩一会儿,不尽兴,于是想着自己开发一个。

ARKit的各种入门介绍这里就不说了,网上一堆都是,自己注意甄别。

第一步,创建一个具有增强现实功能AR的项目:

swift小游戏 swift设计小游戏_swift

swift小游戏 swift设计小游戏_游戏开发_02

选择语言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节点跟随人脸移动,与移动的柱子接触后出发碰撞方法,在此方法中做游戏结束的处理。

源码地址:地址