# FlyingPenguinSpriteKitGameTutorial **Repository Path**: iFIERO/FlyingPenguinSpriteKitGameTutorial ## Basic Information - **Project Name**: FlyingPenguinSpriteKitGameTutorial - **Description**: SpriteKit Game Tutorial by iFIERO.com - **Primary Language**: Swift - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2018-07-08 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 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, 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