欢迎光临
我们一直在努力

SpirteKit游戏 BreakOutGameVansVTutorial 弹潮V – By iFIERO.com

建站超值云服务器,限时71元/月

SpirteKit游戏 BreakOutGameVansVTutorial 弹潮V - By iFIERO.com

项目介绍:

注意:因为网站上传大小的限制,上传的压缩包中缺少音乐文件,要获得完成的代码,请在文章底部的GitHub下载!

iFIERO.com

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

赞(0)
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com 特别注意:本站所有转载文章言论不代表本站观点! 本站所提供的图片等素材,版权归原作者所有,如需使用,请与原作者联系。未经允许不得转载:IDC资讯中心 » SpirteKit游戏 BreakOutGameVansVTutorial 弹潮V – By iFIERO.com
分享到: 更多 (0)