apiapia / FlyingPenguinSpriteKitGameTutorial

SpriteKit Game Tutorial by iFIERO.com

Home Page:http://www.iFIERO.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

FlyingPenguinSpriteKitGameTutorial

SpriteKit Game Tutorial by iFIERO.com

动画演示 (https://upload-images.jianshu.io/upload_images/3896436-7bc99f99cf931ac8.gif)

开始场景

游戏过程

结束场景

  • *** 游戏元素使用条款及注意事项 ***
  • 游戏中的所有元素全部由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<UITouch>, 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
}

}

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

About

SpriteKit Game Tutorial by iFIERO.com

http://www.iFIERO.com


Languages

Language:Swift 100.0%