(一)FlyingPenguin 飞吧企鹅 — StateMachine

(一)FlyingPenguin 飞吧企鹅 — StateMachine

课程开始前,请先下载游戏源码 …

动画演示
开始场景
游戏过程
结束场景

/*

*  *** 游戏元素使用条款及注意事项 ***

*

*  游戏中的所有元素全部由iFIERO所原创(除注明引用之外),包括人物、音乐、场景等,

*  创作的初衷就是让更多的游戏爱好者可以在开发游戏中获得自豪感 — 让手机游戏开发变得简单。

*  秉着开源分享的原则,iFIERO发布的游戏都尽可能的易懂实用,并开放所有源码,

*  任何使用者都可以使用游戏中的代码块,也可以进行拷贝、修改、更新、升级,无须再经过iFIERO的同意。

*  但这并不表示可以任意复制、拆分其中的游戏元素:

*  用于[商业目的]而不注明出处,

*  用于[任何教学]而不注明出处,

*  用于[游戏上架]而不注明出处;

*  另外,iFIERO有商用授权游戏元素,获得iFIERO官方授权后,即无任何限制!

*  请尊重帮助过你的iFIERO的知识产权,非常感谢!

*

*  Created by VANGO杨 && ANDREW陈

*  Copyright © 2018 iFiero. All rights reserved.

*  www.iFIERO.com

*  iFIERO — 为游戏开发深感自豪

*

*  FlyingPenguin 飞吧企鹅 在此游戏中您将获得如下技能:

*

*  1、LaunchScreen       学习如何设置游戏启动画面;

*  2、Endless Background 无限循环背景;

*  3、Scene Edit         直接使用可见即所得操作,注意scebe场景的中心点是anchor0.5*0.5;

*  4、UserDefaults       保存游戏分数、最高分;

*  5、Random+moveBy      利用可复用的随机函数生成Obstacle障碍物;

*  6、Juice:Particle     粒子特效;

*  7、ScreenShot+Share   截屏+分享链接GameScene传值给ViewController;

*  8、Protocol代理        代理传值保存图片时须设置程序读取手机图片的权限info.plist中的Privicy;

*  9、StateMachine       GameplayKit 运用之场景;        (**** 中级技能)

* 10、Entity+Component   Entity对象+Component组件的运用; (**** 中级技能)

* 11、Velocity+Rotate    Velocity向量(速度+方向)及角度计算;

* 12、Wobbling           利用moveBy+reverse制作出企鹅上下舞动的效果;

* 12B、Juice:ScreenShake  Juice特效:学习企鹅撞到障碍物后,整个屏幕发生抖动;(**** 高级技能)

*

*/



import SpriteKit
import GameplayKit

// 为何设置代理:ViewController须弹出分享View
protocol GameSceneDelegate:class {
    func screenShot() -> UIImage  // 截屏代理
    func shareUrl(_ textString:String,url:URL,image:UIImage) // 分享链接代理、传递图片、说明文字
}

class GameScene: SKScene,SKPhysicsContactDelegate {
    
    weak var gameSceneDelegate:GameSceneDelegate?
    var moveAllowed = false // 场景是否可以移动
    
    //MARK: - StateMachine 场景中各个舞台State
    lazy var stateMachine:GKStateMachine = GKStateMachine(states: [
        WaitingForTapState(scene: self),
        PlayingState(scene: self),
        FallingState(scene:self),
        GameOverState(scene: self)])
    
    let appStoreLink = "http://www.iFiero.com" //游戏上线后换成app store上的游戏下载地址;
    var score = 0           // 游戏分数
    var dt:TimeInterval = 0 // 每一frame的时间差
    var lastUpdateTimeInterval:TimeInterval = 0 // 最后更新的时间
    
    /* UI元素中的场景移动速度 注意:用真机运行FPS 60s,模拟器simular的FPS 10s */
    let cloudDistance:CGFloat     = 5   // 白云
    let treeDistance:CGFloat      = 8   // 树
    let mounationDistance:CGFloat = 2   // 山
    let groundSpeed: CGFloat      = 9   // 地板的移动速度;
    let obstacleSpeed:CGFloat     = 450 // 值越大,移动速度越快
    let obstacleDelayTime:CGFloat = 2.2 // 每次生成障碍物的时间间隔 2.0s
    let firstSpawnDelay: TimeInterval = 0.8  // 首次生成 Obstacle
    var everySpawnDelay: TimeInterval = 4.0 // 每个Obstacle生成的时间间隔 值越大 游戏难度越小;
    let numberOfScenes:CGFloat = 2 // 有二个场景
    
    /*物理重力*/
    let impluse:CGFloat = 600         // 每次拍打的向上动力
    let gravity:CGFloat = -1800       // 向下的重力
    var initVelocity = CGPoint.zero   // 速度+方向
    var velocityModifier:CGFloat = 1000.0 //数值转化为弧度;
    var angularVelocity:CGFloat = 0.0 // 角度;
    var lastTouchY:CGFloat = 0.0       // 最新点击的Y轴位置;
    let minDegree:CGFloat = -25 // 水平线 :向下的角度(左到右)
    let maxDegree:CGFloat = 25  // 向上的角度
    
    /*场景中的所有SpriteNode*/
    private var worldNode:SKSpriteNode!
    private var groundNode:SKSpriteNode!
    
    private var playerNode:SKSpriteNode!
    private var crownNode:SKSpriteNode!
    // let obstacle = ObstacleEntity(imageName: "obstacle") // 障碍物;
    
    /*
     * 若有取得场景中的白云节点,需命名场景中的每一个节点的名称 Attritubes inspector面板命名;
     */
    private var cloud1_1:SKSpriteNode!  // 白云1
    private var cloud1_2:SKSpriteNode!  //
    private var cloud1_3:SKSpriteNode!  //
    private var cloud1_4:SKSpriteNode!  //
    private var cloud2_1:SKSpriteNode!  // 白云2
    private var cloud2_2:SKSpriteNode!  //
    private var cloud2_3:SKSpriteNode!  //
    private var cloud2_4:SKSpriteNode!  //
    
    
    
    var playableHeight:CGFloat = 0  // 企鹅的可飞行区域
    var playableStart:CGFloat  = 0  // 地板的位置
    var playerTextureAtlas = SKTextureAtlas()
    var playerTextures     = [SKTexture]()
    
    override func didMove(to view: SKView) {
        
        physicsWorld.gravity = CGVector.zero   // 物理世界的重力
        physicsWorld.contactDelegate = self    // 碰撞代理;
        
        setupBgSound()    // ** 加入背景音乐
        worldNode = childNode(withName: "worldNode") as! SKSpriteNode
        //MARK:- 分享按钮 注意观察GameScene.sks的层级 shareNode是属于worldNode的下一级
        setupBackground() /// 场景
        setupSceneUI()    /// 取得可视化Scene编辑下的UI元素
        setupPlayer()     /// 加入玩家企鹅
        startWobble()     /// 上下浮动 + 拍打翅膀
        
        stateMachine.enter(WaitingForTapState.self) /// 进入场景后 直接进入WaitingForTap State
    }
    
    //MARK:- 场景总节点
    func setupBackground(){
        // 注意:用可视化拖拉sprite到scene时,有二个节点,需要用enumerateChildNodes找到所有的ground;
        groundNode = worldNode.childNode(withName: "ground") as! SKSpriteNode
        
        worldNode.enumerateChildNodes(withName: "ground") { (node, error) in
            let groundNode = node as! SKSpriteNode
            let topLeft  = CGPoint(x: 0, y: groundNode.size.height)
            let topRight = CGPoint(x: self.size.width, y: groundNode.size.height)
            groundNode.physicsBody = SKPhysicsBody(edgeFrom: topLeft, to: topRight)
            groundNode.physicsBody?.affectedByGravity  = true   /// 不受重力影响
            groundNode.physicsBody?.isDynamic = false
            groundNode.physicsBody?.categoryBitMask    = PhysicsCategory.Ground
            groundNode.physicsBody?.contactTestBitMask = PhysicsCategory.Player | PhysicsCategory.Crown
            groundNode.physicsBody?.collisionBitMask   = PhysicsCategory.Crown
        }
        
        // playableStart  = groundNode.size.height      // 从地板高度height的开始点;
        // playableHeight = size.height - playableStart // 企鹅的可飞行区域;
    }
    //MARK: - 取得可视化Scene编辑下的UI元素
    func setupSceneUI(){
        // 白云
        // 场景1的白云 cloud1_1.position.x + size.width = 场景2的白云位置 cloud2_1.position.x (Y轴不变)
        cloud1_1 = worldNode.childNode(withName: "cloud1_1")  as! SKSpriteNode
        cloud1_2 = worldNode.childNode(withName: "cloud1_2")  as! SKSpriteNode
        cloud1_3 = worldNode.childNode(withName: "cloud1_3")  as! SKSpriteNode
        cloud1_4 = worldNode.childNode(withName: "cloud1_4")  as! SKSpriteNode
        
        cloud2_1 = worldNode.childNode(withName: "cloud2_1")  as! SKSpriteNode
        cloud2_2 = worldNode.childNode(withName: "cloud2_2")  as! SKSpriteNode
        cloud2_3 = worldNode.childNode(withName: "cloud2_3")  as! SKSpriteNode
        cloud2_4 = worldNode.childNode(withName: "cloud2_4")  as! SKSpriteNode
    }
    
    //MARK: - 移动地板(注:另一方法为移动Camera)
    func moveEndlessGround(dt:TimeInterval){
        // 检测场景中名称为 tree的所有节点;
        worldNode.enumerateChildNodes(withName: "ground") { (node, error) in
            let groundNode = node as! SKSpriteNode
            let moveAmount = CGPoint(x: -self.groundSpeed,y: 0)
            groundNode.position.x += moveAmount.x
            if groundNode.position.x < -self.size.width {
                groundNode.position.x += SCENE_WIDTH * self.numberOfScenes
            }
        }
    }
    //MARK: - 移动白云(注意:此处白云没有一直生成并销毁)
    func moveEndlessCloud(dt:TimeInterval){
        //白云
        cloud1_1.position.x -= cloudDistance
        cloud1_2.position.x -= cloudDistance*1.2 //变化速度
        cloud1_3.position.x -= cloudDistance
        cloud1_4.position.x -= cloudDistance*0.7
        
        cloud2_1.position.x -= cloudDistance
        cloud2_2.position.x -= cloudDistance*1.2
        cloud2_3.position.x -= cloudDistance
        cloud2_4.position.x -= cloudDistance*0.7
        // 第一朵
        if cloud1_1.position.x < -SCENE_WIDTH {
            cloud1_1.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        if cloud2_1.position.x < -size.width {
            cloud2_1.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        // 第二朵
        if cloud1_2.position.x < -size.width {
            cloud1_2.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        if cloud2_2.position.x < -size.width {
            cloud2_2.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        // 第三朵
        if cloud1_3.position.x < -size.width {
            cloud1_3.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        if cloud2_3.position.x < -size.width {
            cloud2_3.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        // 第四朵
        if cloud1_4.position.x < -size.width {
            cloud1_4.position.x +=  SCENE_WIDTH * numberOfScenes
        }
        if cloud2_4.position.x < -size.width {
            cloud2_4.position.x +=  SCENE_WIDTH * numberOfScenes
        }
    }
    //MARK:- 移动TREE
    func moveEndlessTree(dt:TimeInterval){
        // 检测场景中名称为 tree的所有节点;
        worldNode.enumerateChildNodes(withName: "tree") { (node, error) in
            let treeNode = node as! SKSpriteNode
            let moveAmount = CGPoint(x: -self.treeDistance,y: 0)
            treeNode.position.x += moveAmount.x
            if treeNode.position.x < -self.size.width {
                treeNode.position.x += SCENE_WIDTH * self.numberOfScenes
            }
        }
    }
    //MARK:- 移动山
    func moveEndlessMountain(dt:TimeInterval){
        // 检测场景中名称为 mounation的所有节点;
        worldNode.enumerateChildNodes(withName: "mountain") { (node, error) in
            let mounNode = node as! SKSpriteNode
            let moveAmount = CGPoint(x: -self.mounationDistance,y: 0)
            mounNode.position.x += moveAmount.x
            if mounNode.position.x < -self.size.width { mounNode.position.x += SCENE_WIDTH * self.numberOfScenes } } } //MARK:- 加入玩家Penguin func setupPlayer(){ playerNode = worldNode.childNode(withName: "player") as! SKSpriteNode // 企鹅属于worldNode的子层级; playerTextureAtlas = SKTextureAtlas(named: "penguin") for i in 1...playerTextureAtlas.textureNames.count { let imageName = "penguin0\(i)" playerTextures.append(SKTexture(imageNamed: imageName)) } /* 建立物理体 * playerNode.zPosition = 6 直接在GameScene.sks设置 > 位于地板上层
         * 核心知识:
         * 1.原有sprite拖到scene后,只要大小有缩小(变化) scale=0.7,则texture也要相应缩小0.7;
         * 2.sprite的anchorPoint有变化,须重新设置物理体的center,中心点位于物理体的正中心,即x=playerNode.size.width/2;
         * 3.设置后物理体的碰撞就非常精确;
         */
        let width  = playerNode.size.width * 0.5 // 缩小物理体
        let height = playerNode.size.height * 0.7
        playerNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: width, height: height),center:CGPoint(x: playerNode.size.width/2/2, y:0)) // X轴:playerNode.size.width/2,Y轴:0
        // 监测碰撞
        //  print("playerNode.size:\(playerNode.size),向右移动:\(playerNode.size.width/2/2),碰撞width:\(width)")
        playerNode.physicsBody?.affectedByGravity  = false 
        playerNode.physicsBody?.categoryBitMask    = PhysicsCategory.Player  // 1.标识
        playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.Ground | PhysicsCategory.Obstacle  // 2.会和谁相撞发出通知
        playerNode.physicsBody?.collisionBitMask   = PhysicsCategory.None     // 3.会相撞(相互作用)吗
        playerNode.physicsBody?.usesPreciseCollisionDetection = true
    }
    
    //MARK:- 随机大量产生金币 Timer
    func spawningCoins(){
        Timer.scheduledTimer(timeInterval: TimeInterval(3.0), target: self, selector: #selector(spawnSingleCoin), userInfo: nil, repeats: true)
    }
    //MARK:- 加入金币 Timer 函数前加@objc
    @objc func spawnSingleCoin(){
        if moveAllowed {
            // 1.生成随机位置的coin
            let coinNode = CoinSprite.sharedInstance()
            let minY = groundNode.size.height  // 开始处
            let maxY = size.height
            let randomY = CGFloat.random(minY, max: maxY)
            
            let minX = SCENE_WIDTH * 0.1
            let maxX = SCENE_WIDTH * 1.2
            let randomX = CGFloat.random(minX, max: maxX)
            
            coinNode.position = CGPoint(x: size.width + randomX, y: randomY)
            worldNode.addChild(coinNode)
            // 2.移动coin 并销除;
            // MARK: -2.移动障碍物;
            let moveByX = SCENE_WIDTH * 2 + coinNode.size.width * 2
            let moveDuration = moveByX / obstacleSpeed
            
            let move = SKAction.moveBy(x: -moveByX, y: 0, duration: TimeInterval(moveDuration))
            let sequence = SKAction.sequence([move,SKAction.removeFromParent()])
            coinNode.run(sequence)
        }
    }
    
    // MARK:- 生成单个Obstcale障碍物并移动
    // Anchor(0.5,0)** Y轴位置如果不明白,可以拖一个ColorSprite到场景中,可以直观的进行Y轴的定位;
    // 高度为 1536 - Player 200 / 2 为总体的尺寸
    func spawnSingleObstacle(){
        // MARK: -1.生成一个障碍物对象
        //X轴的位置 位于屏幕的右侧+obstacle.width 产生后再移动 屏幕的左侧 并消除对象
        let bottomObstacle = createObstacle()
        let topObstacle = createObstacle()
        let wallObstacle = SKNode()
        
        let randomY =  CGFloat.random(0, max: groundNode.size.height)
        var startX = size.width
        //worldNode -> wallObstacle ->
        startX = startX + bottomObstacle.size.width // 位置位于屏幕的最右侧;
        bottomObstacle.position = CGPoint(x: startX, y: 0)
        wallObstacle.addChild(bottomObstacle)
        
        topObstacle.zRotation = CGFloat(180).degreesToRadians()  // 旋转180°  CGFloat(Double.pi)
        topObstacle.position.x = bottomObstacle.position.x
        topObstacle.position.y = self.size.height
        wallObstacle.addChild(topObstacle)
        
        wallObstacle.position.y += randomY // 返回在Y轴随机位置
        wallObstacle.name = "wallObstacle"
        worldNode.addChild(wallObstacle)
        
        // MARK: -2.移动障碍物;
        let moveByX = size.width + bottomObstacle.size.width * 2
        let moveDuration = moveByX / obstacleSpeed
        
        let moveAction = SKAction.moveBy(x: -moveByX, y: 0, duration: TimeInterval(moveDuration))
        let sequence   = SKAction.sequence([moveAction,SKAction.removeFromParent()])
        wallObstacle.run(sequence)
        
    }
    
    //MARK:-- 不断生成Obstcale 只执行一次didMove;(挑战,delay时间间隔不同 产生的obstacle的间距不同)
    // 区别于生成coins的Timer方法
    // PlayingState 调用
    func spawningObstcale(_ dt:TimeInterval){
        let spawn = SKAction.run(spawnSingleObstacle)  // 生成一个障碍物
        let delay = SKAction.wait(forDuration: TimeInterval(obstacleDelayTime))     // obstacleDelayTime间距
        
        let spawnSequence = SKAction.sequence([spawn,delay])
        let foreverSpawn = SKAction.repeatForever(spawnSequence)
        
        let firstDelay = SKAction.wait(forDuration: firstSpawnDelay)
        let overallSequence = SKAction.sequence([firstDelay, foreverSpawn])
        run(overallSequence, withKey: "spawn")
    }
    //MARK:-分享链接
    func shareScore(){
        let urlString = appStoreLink
        let url = URL(string: urlString)
        let screenShot = gameSceneDelegate?.screenShot() // 取得截图
        let textString = "嘿,我在Flying Peguin飞吧企鹅中取得了\(self.score)分数,你也快来挑战吧!"
        // 调用代理,把shareUrl传统到ViewController;
        gameSceneDelegate?.shareUrl(textString, url: url!, image: screenShot!)
    }
    
    //MARK:- option+command+<- 折叠
    func setupBgSound(){
        let bgSound = SKAudioNode(fileNamed: "jazzmusic.mp3")
        bgSound.autoplayLooped = true
        addChild(bgSound)
    }
    //MARK:- 开始拍打翅膀
    func startAnimation(){
        let playerAnimation = SKAction.animate(with: playerTextures, timePerFrame: 0.07)
        let repeatAction    = SKAction.repeatForever(playerAnimation)
        playerNode.run(repeatAction, withKey: "Flap")
    }
    
    func stopAnimation(_ name:String){
        playerNode.removeAction(forKey:name) // Player
    }
    // MARK: - 不再生成了;
    func stopSpawning(){
        
        playerNode.removeAction(forKey: "Flap")
        playerNode.removeAction(forKey: "Wobble-Flap")
        removeAction(forKey: "spawn")
        //停止产生 obstacle let wallObstacle = SKNode()
        worldNode.enumerateChildNodes(withName: "wallObstacle") { (node, error) in
            node.removeAllActions()
            
            node.enumerateChildNodes(withName: "Obstacle", using: { (node, error) in
                node.removeAllActions()
            })
        }
        worldNode.enumerateChildNodes(withName: "coin") { (node, error) in
            print("coin")
            node.removeAllActions()
        }
    }
    //MARK:- 上下浮动
    func startWobble(){
        let moveUp   = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
        moveUp.timingMode = .easeInEaseOut
        let moveDown = moveUp.reversed()
        let sequence = SKAction.sequence([moveUp,moveDown])
        let repeatWobble = SKAction.repeatForever(sequence)
        playerNode.run(repeatWobble, withKey: "Wobble")
        //MARK:- Emitter juice 加入果酱
        let trailNode = SKNode()
        trailNode.zPosition = 5
        worldNode.addChild(trailNode)
        let emitter = SKEmitterNode(fileNamed: "Trail")!
        emitter.targetNode = trailNode
        playerNode.addChild(emitter)
        
        let playerAnimation = SKAction.animate(with: playerTextures, timePerFrame: 0.07)
        let repeatAction    = SKAction.repeatForever(playerAnimation)
        playerNode.run(repeatAction, withKey: "Wobble-Flap")
    }
    //MARK:- 移动皇冠 (相对于Player的位置)
    func moveCrown(){
        crownNode = playerNode.childNode(withName: "crown") as! SKSpriteNode
        //皇冠上下跳动的时间 < Wobble 0.5s的时间
        let moveUp = SKAction.moveBy(x: 0, y: 30, duration: TimeInterval(0.15))
        moveUp.timingMode = .easeInEaseOut
        let moveDown = moveUp.reversed()
        let sequence = SKAction.sequence([moveUp,moveDown])
        crownNode.run(sequence)
    }
    
    func stopWobble(){
        stopAnimation("Wobble")
        stopAnimation("Wobble-Flap")
    }
    //MARK:- 初始化向上的速度 initVelocity的
    func applyInitialImpluse(){
        initVelocity = CGPoint(x: 0, y: impluse * 1.7)
    }
    
    //MARK:-每次点击touchesBegin时执行此函数;
    func applyImpluse(_ lastUpdateTime:TimeInterval){
        moveCrown()
        initVelocity = CGPoint(x:0,y:impluse)
        angularVelocity = velocityModifier.degreesToRadians()
        lastTouchY = playerNode.position.y
        // 运行拍打的声音
        let flapSoundAction = SKAction.playSoundFileNamed("flapping.wav", waitForCompletion: false)
        playerNode.run(flapSoundAction)
    }
    //MARK:- *** 时时更新游戏 update 游戏中每帧要更新的代码放在此处 ***
    func applyInstantlyMovement(_ seconds:TimeInterval){
        
        // 执行Gravity 重力 * 调用Utility CGPoint+Extension
        let gravityStep = CGPoint(x: 0, y: gravity) * CGFloat(seconds)
        initVelocity += gravityStep
        
        // 运行Velocity 方向+速度
        let velocityStep = initVelocity * CGFloat(seconds)
        playerNode.position += velocityStep
        
        //MARK:- 更新企鹅的角度
        //1.要转的角度
        if playerNode.position.y < lastTouchY {
            angularVelocity = -velocityModifier.degreesToRadians()
        }
        // 转化角度;
        let angularStep = angularVelocity * CGFloat(seconds)
        playerNode.zRotation += angularStep
        // 限制角度
        playerNode.zRotation = min(max(playerNode.zRotation, minDegree.degreesToRadians()), maxDegree.degreesToRadians())
        
        // 撞到地面了; didBegin 进行物理碰撞检测;
        // 物理体的Y值有变化,所以监测playerNode.position.y的高度是否 < (groundNode.size.height + playerNode.size.height / 2)
        if playerNode.position.y <  (groundNode.size.height + playerNode.size.height / 2) {
            playerNode.position = CGPoint(x: playerNode.position.x, y: (groundNode.size.height + playerNode.size.height / 2))
            // 进入游戏结束state
            stateMachine.enter(GameOverState.self)
        }
        
    }
    //MARK:- 收集金币 COINS
    func collectionCoins(nodeA:SKSpriteNode,nodeB:SKSpriteNode){
        // bodyB is Coin 查看Constant.swift的排序;
        let coinAction = SKAction.playSoundFileNamed("coin.wav", waitForCompletion: false)
        worldNode.run(coinAction)
        // 加入果酱 Juice
        /*
         let emitter = SKEmitterNode(fileNamed: "Coin")!
         emitter.position = nodeA.position
         worldNode.addChild(emitter)
         emitter.run(SKAction.sequence([
         SKAction.wait(forDuration: 0.3),
         SKAction.run {emitter.removeFromParent()}
         ])
         )
         */
        //MARK:-JUICE 建立一个路径,绕企鹅一圈
        //移出B节点;
        nodeB.removeFromParent()
    }
    //MARK: - 重新开始游戏;
    func restartGame(){
        let newScene = GameScene(fileNamed: "GameScene")!
        newScene.size = CGSize(width: SCENE_WIDTH, height: SCENE_HEIGHT)
        newScene.anchorPoint = CGPoint(x: 0, y: 0)
        newScene.scaleMode   = .aspectFill
        let transition = SKTransition.flipHorizontal(withDuration: 0.5)
        view?.presentScene(newScene, transition:transition)
    }
    
    //MARK:- 点击屏幕 stateMachines
    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        guard let touch = touches.first else {
            return
        }
        let touchLocation = touch.location(in: self) ///获得点击的位置
        
        /// 判断目前的GameScene场景舞台是哪个state
        switch stateMachine.currentState {
        case is WaitingForTapState:
            ///  stateMachine获取点击位置=>State场景要通过physicsWorld.body进行获得点击点
            guard let body = physicsWorld.body(at: touchLocation) else {
                return
            }
            let playButton = body.node?.childNode(withName: "worldNode")?.childNode(withName: "playButton")
            let startLogo  = body.node?.childNode(withName: "worldNode")?.childNode(withName: "startLogo")
            if (playButton?.contains(touchLocation))! {
                // Hide logo + PlayButton
                playButton?.isHidden = true
                startLogo?.isHidden = true
                stateMachine.enter(PlayingState.self) /// 进入开始游戏;
            }    
        case is PlayingState:
            applyImpluse(lastUpdateTimeInterval)      /// 移动;
        case is GameOverState:
            /// 游戏结束的state
            /// stateMachine获取点击位置
            guard let body = physicsWorld.body(at: touchLocation) else {
                return
            }
            // TapToPlay按钮;
            if let tapToPlay  = body.node?.childNode(withName: "worldNode")?.childNode(withName: "tapToPlay") {
                if tapToPlay.contains(touchLocation){
                    restartGame()
                }
            }
            
        default:
            break
        }
    }
    //MARK:- 物理碰撞 didBegin
    func didBegin(_ contact: SKPhysicsContact) {
        
        let bodyA:SKPhysicsBody
        let bodyB:SKPhysicsBody
        
        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask { bodyA = contact.bodyA bodyB = contact.bodyB }else{ bodyA = contact.bodyB bodyB = contact.bodyA } // 收集硬币 if bodyA.categoryBitMask == PhysicsCategory.Player && bodyB.categoryBitMask == PhysicsCategory.Coin { collectionCoins(nodeA: bodyA.node as! SKSpriteNode, nodeB: bodyB.node as! SKSpriteNode) } // 撞到obscatle if bodyA.categoryBitMask == PhysicsCategory.Player && bodyB.categoryBitMask == PhysicsCategory.Obstacle { print("scene:player hit the obstacles") stateMachine.enter(FallingState.self) } // 撞到地面 if bodyA.categoryBitMask == PhysicsCategory.Player && bodyB.categoryBitMask == PhysicsCategory.Ground { print("scene:player dropped down to the ground") stateMachine.enter(GameOverState.self) } if bodyA.categoryBitMask == PhysicsCategory.Ground && bodyB.categoryBitMask == PhysicsCategory.Crown { print("scene:crown dropped down to the ground") } } override func update(_ currentTime: TimeInterval) { // 获取时间差 if lastUpdateTimeInterval == 0 { lastUpdateTimeInterval = currentTime } dt = currentTime - lastUpdateTimeInterval lastUpdateTimeInterval = currentTime if moveAllowed { moveEndlessGround(dt: dt) // Endless 无限循环地板 moveEndlessTree(dt: dt) // 移动Tree moveEndlessMountain(dt: dt) // 山 moveEndlessCloud(dt: dt) // Endless 云 /* 一、此处可以直接调用 GameScene的applyImpluse */ // applyInstantlyMovement(dt) /* * 二、下列为学习如何调用stateMachine方法 * (1)、stateMachine.update 时时更新 * (2)、进入PlayingState的update * (3)、PlayingState.update调用 Scene的applyImpluse方法 */ } stateMachine.update(deltaTime: dt) } public func randomDelay() -> CGFloat {
        let random = CGFloat.random(CGFloat(everySpawnDelay), max: 8.0)// max值载大 间距越大;
        return random
    }
}

源码传送门:https://github.com/apiapia/FlyingPenguinSpriteKitGameTutorial

打赏

Leave a Reply