美文网首页
SpirteKit游戏 BreakOutGameVansVTut

SpirteKit游戏 BreakOutGameVansVTut

作者: 布袋的世界 | 来源:发表于2018-08-05 00:09 被阅读18次
    Simulator Screen Shot - iPhone 8 Plus - 2018-08-04 at 14.01.43.png Simulator Screen Shot - iPhone 8 Plus - 2018-08-04 at 23.18.19.png Simulator Screen Shot - iPhone 8 Plus - 2018-08-04 at 23.18.05.png Simulator Screen Shot - iPhone 8 Plus - 2018-08-04 at 23.18.16.png
    • *** 游戏元素使用条款及注意事项 ***
    • 游戏中的所有元素全部由iFIERO所原创(除注明引用之外),包括人物、音乐、场景等;
    • 创作的初衷就是让更多的游戏爱好者可以在开发游戏中获得自豪感 -- 让手机游戏开发变得简单;
    • 秉着开源分享的原则,iFIERO发布的游戏都尽可能的易懂实用,并开放所有源码;
    • 任何使用者都可以使用游戏中的代码块,也可以进行拷贝、修改、更新、升级,无须再经过iFIERO的同意;
    • 但这并不表示可以任意复制、拆分其中的游戏元素:
    • 用于[商业目的]而不注明出处;
    • 用于[任何教学]而不注明出处;
    • 用于[游戏上架]而不注明出处;
    • 另外,iFIERO有商用授权游戏元素,获得iFIERO官方授权后,即无任何限制;
    • 请尊重帮助过你的iFIERO的知识产权,非常感谢;
    • Created by VANGO杨 && ANDREW陈
    • Copyright © 2018 iFiero. All rights reserved.
    • www.iFIERO.com
    • iFIERO -- 让手机游戏开发变得简单
    • BreakOutGame 弹潮V 在此游戏中您将获得如下主要技能:
    • 1.GameScene Size 学习精确适配各种iPhone尺寸;
    • 2.GameplayKit 学习如何应用GameplayKit切换游戏状态;
    • 3.Velocity 三角函数求向量、判断球的速度;
    • 4.TouchBegan 学习触碰移动事件直接写在精灵中
    • 5.SoundManager 学习设置单例管理所有音乐;
    • 6.PhysicsBody 学习最基本物理碰撞特性 反弹 摩擦力;
    • 7.SKNode+SKScene 建立空节点+引入自定义Scene+node.copy+isPaused=false (重要技能)
    • 8.Convert 学习转换其它场景Scene的坐标到当前GameScene坐标;

    */

    
    
    import SpriteKit
    import GameplayKit
    
    class GameScene: SKScene,SKPhysicsContactDelegate {
        
        //MARK: - StateMachine 场景中各个舞台State
        lazy var stateMachine:GKStateMachine = GKStateMachine(states: [
            WaitingState(scene: self),
            PlayState(scene: self),
            GameOverState(scene: self)])
        
        private var fgNode = SKNode()
        private var ballNode = BallNode()
        private var shoseOverlay:SKSpriteNode! /// 鞋子精灵Parent 位于 shoseOverlay 节点下
        private var gameSceneOverlay:SKSpriteNode! //复制的鞋子精灵
        private var shoseNode:ShoseNodeClass!   /// 鞋子精灵
        private var skateboard = Skateboard()
        private var maxAspectRatio:CGFloat! /// 屏幕分辩率;
        private var ballMaxSpeed:CGFloat = 1500.00 // 最大速度;
        private var ballInitSpeed:CGFloat = 1000.00 // 初始速度;
        private var playableRect:CGRect!   /// 可视范围
        private var playableHeight:CGFloat  = 0.0   /// 可视范围的高度
        private var playableMargin:CGFloat = 0.0   /// 可视范围的起点
        var learnTemp:SKSpriteNode!
        var playButtonTemp:SKSpriteNode!
        
        private var dt:TimeInterval = 0  /// 每一frame的时间差
        private var lastUpdateTimeInterval:TimeInterval = 0
        
        
        
        override func didMove(to view: SKView) {
            self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)  /// 物理世界的重力
            self.physicsWorld.contactDelegate = self               /// 碰撞代理
            
            learnTemp   = childNode(withName: "learnTemp") as! SKSpriteNode
            playButtonTemp   = childNode(withName: "playButton") as! SKSpriteNode
            initCheckDevice()
            setupBall()       /// 球
            setupSkateboard() /// 滑板
            setupShose()      /// 鞋子
            setupBgMusic()    // 加入背景音乐;
            
            stateMachine.enter(WaitingState.self) // 进入WaitingState
            
        }
        // option+command+->展开
        // MARK: - 检测是哪种设备
        func initCheckDevice(){
            if UIDevice.current.isPhoneX() {
                maxAspectRatio = 2.16         /// iPhoneX 2.16 ratio
            }else {
                maxAspectRatio  = UIDevice.current.isPad() ? (4.0 / 3.0) : (16.0 / 9.0)  /// iPhone 16:9,iPad 4:3
            }
            /// 画出可视区域
            drawPayableArea(size: self.size,ratio: maxAspectRatio)
        }
        // MARK: - command + option + 左or右箭头 可以折叠/拓展函数
        // MARK: - 画出可视区域
        func drawPayableArea(size:CGSize,ratio:CGFloat){
            /*
             /// 安全区域即用户交互的区域,非可视区域 (iPhoneX的安全区域 < 可视区域)
             let safeInsetTop    =  self.size.height * AREA_INSET_WIDTH_TOP / iPhoneX_REAL_HEIGHT
             let safeInsetBottom =  self.size.height * AREA_INSET_WIDTH_BOTTOM / iPhoneX_REAL_HEIGHT
             let safeHeight = self.size.height - safeInsetTop - safeInsetBottom   // 安全区域的高度
             */
            
            playableHeight  = size.width / ratio
            playableMargin = (size.height - playableHeight ) / 2.0   /// P70
            playableRect = CGRect(x: 0, y: playableMargin, width: size.width, height:  playableHeight)  /// 注意 scene的anchorPoint(0,0)原点的位置;
            
            let shapeFrame = SKShapeNode(rect: playableRect)
            shapeFrame.zPosition = 1
            shapeFrame.strokeColor = SKColor.red
            shapeFrame.lineWidth = 5.0
            addChild(shapeFrame)
            
            /// 可视区域的物理状态
            let playableBody = SKPhysicsBody(edgeLoopFrom: playableRect)
            playableBody.friction = 0
            self.physicsBody = playableBody
            playableBody.categoryBitMask    = PhysicsCategory.Frame
            playableBody.contactTestBitMask = PhysicsCategory.Ball
            playableBody.collisionBitMask   = PhysicsCategory.Ball
            
            /// 地板
            setupFloor()
        }
        //MARK: - 地板
        func setupFloor(){
            let floor = SKNode()
            let startPoint = CGPoint(x: 0.0, y: playableMargin)
            let endPoint   = CGPoint(x: self.size.width, y: playableMargin)
            self.addChild(floor)
            floor.physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint)
            floor.physicsBody?.categoryBitMask = PhysicsCategory.Floor
            floor.physicsBody?.contactTestBitMask = PhysicsCategory.Ball /// 和球相碰
        }
        //MARK: - 球
        func setupBall(){    
            ballNode = childNode(withName: "ball") as! BallNode
            ballNode.setup(scene:self.scene!)  // 导入size与physicsBody
        }
        
        func runBallRotate(){
            ballNode.rotate()
        }
        func runBall(){
            ballNode.physicsBody?.applyImpulse(CGVector(dx: 100.0, dy: -ballInitSpeed)) /// dy的绝对值越大 球速越快; - 表示向下
        }
        func runBallInvincible(){
            // 即碰到底部不会lose掉game
            print("Invincible")
            ballNode.physicsBody?.categoryBitMask = PhysicsCategory.None
        }
        //MARK: - 滑板
        func setupSkateboard(){
            skateboard = childNode(withName: "skateboard") as! Skateboard
            skateboard.setup(scene: self.scene!)
        }
        //MARK: - 鞋子
        func setupShose(){
            // 一、直接在场景中加入节点,不利于重复利用,以后每一关卡都要重建,非常麻烦;
            // 1.代码创建精灵节点到GameScene
            //         let xPos = self.frame.width / 2
            //         let yPos = playableHeight - 100
            //         shoseNode = ShoseNode.newInstance(scene: self)
            //         shoseNode.position = CGPoint(x: xPos, y: yPos)
            //         self.addChild(shoseNode)
            // 2.用Class可视化加入精灵节点
            /*
             shoseNode = childNode(withName: "shose") as! ShoseNodeClass
             shoseNode.newInstance(scene: self.scene!)
             
             let emitter = SKEmitterNode(fileNamed: "Fire")!
             emitter.zPosition = 1
             emitter.targetNode = shoseNode
             emitter.setScale(3.0)
             shoseNode.addChild(emitter)
             */
            // 二、引用其它scene的节点到当前Scene中,需要convert转化到当前GameScene的坐标
            var sceneName = ""
            sceneName = (CGFloat.random(1, max: 100) > 50) ? "ShoseCrossScene" : "ShoseScene"
            let overlayScene = SKScene(fileNamed: sceneName)!
            let overlayShose = overlayScene.childNode(withName: "Overlay") as! SKSpriteNode
            gameSceneOverlay = overlayShose.copy() as! SKSpriteNode
            overlayShose.removeFromParent() // 移除旧的
            /* 留意SpirteKit的巨坑
             * When an overlay node with actions is copied  there is currently a SpriteKit bug
             * where the node’s isPaused property might be set to true
             * 一定要记得设置为 false 或者所有gamesceneOverlay内的子节点的所有action都不起作用
             */
            gameSceneOverlay.isPaused = false;
            gameSceneOverlay.enumerateChildNodes(withName: "shose") { (node, _) in
                let sprite = node as! ShoseNodeClass
                sprite.newInstance(scene: self.scene!) // 加入物理体;
            }
            gameSceneOverlay.zPosition = 2
            let yPos = self.frame.size.height - playableMargin - 150;
            gameSceneOverlay.position = CGPoint(x: 1024, y: yPos)
            self.addChild(gameSceneOverlay)
            
        }
        //MARK:背景音乐
        func setupBgMusic(){
            let music = SKAudioNode(fileNamed: "bgmusic.mp3")
            music.autoplayLooped = true
            self.addChild(music)
        }
        // 返回 -80.0 或 80.0 角度50 已经很小了;
        func randomDirection() -> CGFloat {
            var xSpeed:CGFloat = 80.0
            if CGFloat.random(1, max: 100) > 50 {
                xSpeed = -xSpeed
            }
            return xSpeed
        }
        //MARK: - 时时校验球的运动速度和方向
        func verifyBallSpeed(_ dt:TimeInterval){
            
            let xSpeed:CGFloat = abs((ballNode.physicsBody?.velocity.dx)!) /// 水平方向的dx
            let ySpeed:CGFloat = abs((ballNode.physicsBody?.velocity.dy)!)
            /// print("xSpeed:" , xSpeed)
            /// 为什么xSpeed是100,不是凭空乱猜的,可以根据打印出来的xSpeed进行查看(即快接近垂直时的角度差不多为50);
            if xSpeed < 100 { // xSpeed很小,表示球正在上下来回运动 必须赋一个值 让球再次向左右水平方向运动;
                ballNode.physicsBody?.applyImpulse(CGVector(dx: randomDirection(), dy: 0.0))
            }
            if ySpeed < 100 {
                ballNode.physicsBody?.applyImpulse(CGVector(dx: 0.0, dy: randomDirection()))
            }
            /// 三角函数 C(斜边)= sqrt(a*a + b*b) 得出球的运动速度
            let ballSpeed = sqrt((ballNode.physicsBody?.velocity.dx)! * (ballNode.physicsBody?.velocity.dx)! + (ballNode.physicsBody?.velocity.dy)! * (ballNode.physicsBody?.velocity.dy)!)
            /// 防止球的运动速度过快 把球的速度打印出来 就可以知道大概 maxSpeed要设置多少了;
            ballNode.physicsBody?.linearDamping = (ballSpeed > ballMaxSpeed) ? 0.2 : 0.0
            //print("ballSpeed",ballSpeed);
        }
        // 特效果汁
        func emitParticles(particleName: String, sprite: SKSpriteNode) {
            let scenePos = convert(sprite.position, from: sprite.parent!)
            let emitter = SKEmitterNode(fileNamed: "Fire")!
            emitter.zPosition = 5  // 位于鞋子的上方;
            emitter.position = scenePos
            emitter.setScale(3.0)
            self.addChild(emitter)
            
            let node = sprite as! ShoseNodeClass
            node.runShake(scene: self.scene!)
            
            sprite.run(SKAction.sequence([
                SKAction.wait(forDuration: TimeInterval(0.5)),
                //SKAction.scale(to: 0.0, duration: TimeInterval(0.08)),
                SKAction.fadeAlpha(to: 0.0, duration: TimeInterval(0.3)),
                SKAction.run {
                    emitter.removeFromParent()
                },
                SKAction.run {
                    sprite.removeFromParent()
                },
                SKAction.run {
                    // print ("设置copy()后的精灵节点的isPaused=false后,此行才会执行")
                }
                ]))
        }
        
        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.crossFade(withDuration: TimeInterval(0.5))
            view?.presentScene(newScene, transition:transition)
        }
        //MARK: - 实现物理碰撞代理SKPhysicsContactDelegate的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.Ball && bodyB.categoryBitMask == PhysicsCategory.Frame) {
            }
            
            /// 球和鞋子发生碰撞
            if (bodyA.categoryBitMask == PhysicsCategory.Ball && bodyB.categoryBitMask == PhysicsCategory.Shose) {
                // print(PhysicsCategory.Ball ,PhysicsCategory.Shose)
                emitParticles(particleName: "Fire", sprite: bodyB.node as! SKSpriteNode)
                bodyB.node?.physicsBody?.categoryBitMask = PhysicsCategory.None
            }
            
            /// 球和滑板发生碰撞
            if (bodyA.categoryBitMask == PhysicsCategory.Ball && bodyB.categoryBitMask == PhysicsCategory.Skateboard) {
                //print("Skateboard")
                self.run(SoundManager.shareInstanced.hitskateboard)
            }
            
            /// 球和地板发生碰撞
            if (bodyA.categoryBitMask == PhysicsCategory.Ball && bodyB.categoryBitMask == PhysicsCategory.Floor) {
                //print("Game Over")
                self.run(SoundManager.shareInstanced.gameover)
                bodyB.node?.physicsBody?.categoryBitMask = PhysicsCategory.None
                bodyA.node?.physicsBody?.linearDamping = 1.0 /// 阻力为1.0
                bodyA.node?.physicsBody?.restitution = 0.7  /// 反弹;
                self.physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8)
                stateMachine.enter(GameOverState.self)
            }
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let touch = touches.first else {
                return
            }
            // 当 physicsWorld.body(at: touchLocation)时
            // 采用atPoint 取得场景中的的精灵
            let touchLocation = touch.location(in: self) ///获得点击的位置
            let nodeAtPoint = self.atPoint(touchLocation) //返回SKNode
            /// 判断目前的GameScene场景舞台是哪个state
            switch stateMachine.currentState {
            case is WaitingState:
                
                /// 判断是否是点击了PlayButton
                if nodeAtPoint.name == "playButton" {
                    stateMachine.enter(PlayState.self)
                }
                
                if nodeAtPoint.name == "learnTemp" {
                     print("weird")
                    UIApplication.shared.open(URL(string: "http://www.iFIERO.com")!, options: [:], completionHandler: { (error) in
                        print("jump to http://www.iFiero.com")
                    })
                }
                
            case is PlayState:
                print("playing")
            case is GameOverState:
               
                if nodeAtPoint.name == "tapToPlay" {
                    restartGame()
                }
                
            default:
                break;
            }
        }
        
        func gameOver(){
            // 进入游戏结束的state
            stateMachine.enter(GameOverState.self)
            ballNode.physicsBody?.affectedByGravity = true 
            self.physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
        }
        
        func isGameWon() -> Bool {
            // 初始值 为 0 ,表示没有shose了;
            var numberOfShoses = 0
            // 这个是判断是不是还有shose,有则为 1
            gameSceneOverlay.enumerateChildNodes(withName: "shose") {  node, _ in
                numberOfShoses = 1
            }
            return numberOfShoses == 0  // 真或者假 numberOfShoses >0 返回假
        }
        //MARK: - 时时更新
        override func update(_ currentTime: TimeInterval) {
            /// 获取时间差
            if lastUpdateTimeInterval == 0 {
                lastUpdateTimeInterval = currentTime
            }
            dt = currentTime - lastUpdateTimeInterval
            lastUpdateTimeInterval = currentTime
            stateMachine.update(deltaTime: dt)  // 调用所有State内的update方法
            
        }
    }
    
    
    

    更多游戏教程:http://www.iFIERO.com

    实时更新源码请上GITHUB传送门:https://github.com/apiapia/BreakOutGameVansVTutorial

    相关文章

      网友评论

          本文标题:SpirteKit游戏 BreakOutGameVansVTut

          本文链接:https://www.haomeiwen.com/subject/wdhmvftx.html