www.2527.com_澳门新葡8455手机版_新京葡娱乐场网址_
做最好的网站

www.2527.com用Swift做个游戏Lecture06,用Swift做个游戏

2019-09-16 08:44 来源:未知

系列:用Swift作个游戏作者:pmst(1345614869)微博:PPPPPPMST

系列:用Swift作个游戏作者:pmst(1345614869)微博:PPPPPPMST

系列:用Swift作个游戏作者:pmst(1345614869)微博:PPPPPPMST

Lecture08课程结束,我们已经走过了90%,剩下的10%是对游戏体验的改进罢了。就比如,刚启动游戏,“Player”就出现在屏幕中Flap一下翅膀,然后还没等用户清楚这个游戏是什么情况的时候,“Player”已经坠地阵亡了。这种游戏体验可谓是差到极致,试想一个用户下载游戏并启动,此时还对游戏没有一丝认知,渴求先看看帮助说明或者玩法介绍之类吧!

前文已经为各个精灵新增了Physics Body,设置了三个掩码:

Flappy Bird整个项目临近尾声,要做的只是对游戏体验的优化,本文先解决两个,分别是:

因为本课程中,将剔除早前的直接进入游戏的弊端,通过添加主菜单供用户选择开始一次游戏亦或是查看游戏帮助说明等选项。如下:

  • categoryBitMask表明了分属类别。
  • collisionBitMask告知能与哪些物体碰撞。
  • contactTestBitMask则告知能与哪些物体接触。
  1. 实现Player 静态时的动画,修改早前掉落时直上直下的问题。
  2. Player撞击障碍物时,给出一个shake摇晃动画。

www.2527.com 1

现在遗留的问题是如何检测碰撞?难道是在update()方法进行检测:遍历所有的节点,通过判断节点的位置是否有交集吗?天呐!这也太麻烦了。确实,如果通过自己实时检测实在过于劳累,何不让Sprite Kit来帮你代劳,每当物体之间发生碰撞了,立马通知你来处理事件。Bingo!! 显然这里要用协议 代理了,设置场景为代理,每当Sprite Kit检测到碰撞事件发生,就通知GameScene来处理,当前哪里事情都是在协议(Protocol)中声明了。

游戏最后实现的效果是这样的:

前文已经给出了游戏状态有如下几种:

01.游戏状态

在正式开始今天的碰撞检测课程之前,谈谈如何划分游戏各时的状态,仅以Flappy bird游戏为例,简单划分如下:

  • MaiMenu。开始一次游戏、查看排名以及游戏帮助。
  • Tutorial。考虑到新手对于新游戏的上手,在选择进行一次新游戏时,展示玩法教程显然是一个明确且友好的措施。
  • Play。正处于游戏的状态。
  • FallingPlayer因为不小心碰到障碍物失败下落时刻。注意:接触障碍物,失败掉落才算!
  • ShowingScore。显示得分。
  • GameeOver。告知游戏结束。

为此请打开Lecture05的完成工程,打开GameScene.swift文件,新增游戏状态的枚举声明到enum Layer{}下方:

enum GameState{ case MainMenu case Tutorial case Play case Falling case ShowingScore case GameOver}

当然,我们还需要声明一个变量用于存储游戏场景的状态,请找到GameScene类中let sombrero = SKSpriteNode(imageNamed: "Sombrero")这条代码,在下方新增三个新变量:

//1var hitGround = false//2var hitObstacle = false//3var gameState: GameState = .Play
  1. 标识符,记录Player是否掉落至地面。
  2. 标识符,记录Player是否碰撞了仙人掌。
  3. 游戏状态,默认是正在玩。

www.2527.com 2

enum GameState{ case MainMenu case Tutorial case Play case Falling case ShowingScore case GameOver}

02.碰撞检测

正如前面提及的协议 代理方式检测物体之间的碰撞情况。首先请使得类GameScene遵循SKPhysicsContactDelegate协议:

class GameScene: SKScene,SKPhysicsContactDelegate{...}

接着在didMoveToView()方法中设置代理为self,找到physicsWorld.gravity = CGVector(dx: 0, dy: 0)这行代码,添加该行代码physicsWorld.contactDelegate = self

SKPhysicsContactDelegate协议中定义了两个可选方法,分别是:

  • optional public func didBeginContact(contact: SKPhysicsContact)
  • optional public func didEndContact(contact: SKPhysicsContact)

分别用于反馈两个物体开始接触、结束接触两个时刻。本文采用第一个方法用户处理物体接触事件。

func didBeginContact(contact: SKPhysicsContact) { let other = contact.bodyA.categoryBitMask == PhysicsCategory.Player ? contact.bodyB : contact.bodyA if other.categoryBitMask == PhysicsCategory.Ground { hitGround = true } if other.categoryBitMask == PhysicsCategory.Obstacle { hitObstacle = true }}

contact包含了接触的所有信息,其中bodyAbodyB代表两个碰撞的物体,显然发生碰撞的结果只有两种可能:1.Player和地面;2.Player和障碍物。可惜我们无法确实bodyA就是Player,亦或是bodyB就是它。这是有不确定性的,我们需要通过categoryBitMask来区分“阵营”。一旦确定哪个是Player之后,我们就能取到与之发生接触的other,通过判断其类别来分别置为标志位。

一旦标志位设置之后,我们需要在update()方法中进行处理了!

Player动画实现

当游戏状态为.Tutorial的时候,Player是静态呈现在教程界面上的,为此我们想要实现一个动画,让其挥动翅膀。而实现方法也很简单,动画由多张图片组成,指定一定时间播放完毕,具体用SKTexture实例化每一个图片,然后放到数组中;紧接着调用animateWithTextures(_:timePerFrame:)播放动画。

www.2527.com 3

找到setupTutorial()方法,再其下方新增一个方法:

func setupPlayerAnimation() { var textures: Array<SKTexture> = [] // 我们有4张图片 for i in 0..<4 { textures.append(SKTexture(imageNamed: "Bird } // 4=3-1 for i in 3.stride(through: 0, by: -1) { textures.append(SKTexture(imageNamed: "Bird } let playerAnimation = SKAction.animateWithTextures(textures, timePerFrame: 0.07) player.runAction(SKAction.repeatActionForever(playerAnimation)) }

正如前面所说,我们采用for-in循环实例化了4个SKTexture实例存储于数组中,接着调用方法播放动画。现在请将该方法添加到switchToMainMenu()以及switchToTutorial()方法中的最后,点击运行,看看Player是否挥动翅膀了。

在玩游戏的时候我们会注意到Player掉落时是直上直下,有些呆板,这里需要替换掉,动画效果如图:

www.2527.com 4

在开始实现Player旋转机制前,先定义几个常量以及变量,请在GameScene()类中添加如下属性

// 新增常量let kMinDegrees: CGFloat = -90 // 定义Player最小角度为-90let kMaxDegrees: CGFloat = 25 // 定义Player最大角度为25let kAngularVelocity: CGFloat = 1000.0 // 定义角速度// 新增变量var playerAngularVelocity: CGFloat = 0.0 // 实时更新player的角度var lastTouchTime: NSTimeInterval = 0 // 用户最后一次点击时间var lastTouchY: CGFloat = 0.0 // 用户最后一次点击坐标

请找到flapPlayer方法,这个方法是在游戏状态下,用户点击一次屏幕需要调用的方法(具体请跳到touchesBegan方法),为此我们将在这里进行lastTouchTimelastTouchY变量的更新,替换后的内容如下:

func flapPlayer(){ // 发出一次煽动翅膀的声音 runAction(flapAction) // 重新设定player的速度!! playerVelocity = CGPointMake(0, kImpulse) //===========新增内容============ playerAngularVelocity = kAngularVelocity.degreesToRadians() lastTouchTime = lastUpdateTime lastTouchY = player.position.y //============================== // 使得帽子下上跳动 let moveUp = SKAction.moveByX(0, y: 12, duration: 0.15) moveUp.timingMode = .EaseInEaseOut let moveDown = moveUp.reversedAction() sombrero.runAction(SKAction.sequence([moveUp,moveDown]))}

如此每次用户点击一次屏幕,就会重新计算Player应该旋转多少。那么什么时候去真正更新Player的状态呢?答案是update()方法。这里我们要更新的是Player的信息,请找到updatePlayer()方法,新增如下内容到最后:

if player.position.y < lastTouchY { playerAngularVelocity = -kAngularVelocity.degreesToRadians()}// Rotate playerlet angularStep = playerAngularVelocity * CGFloatplayer.zRotation  = angularStepplayer.zRotation = min(max(player.zRotation, kMinDegrees.degreesToRadians, kMaxDegrees.degreesToRadians

点击运行!不出意味应该和预期效果一样。

当我们开始一个游戏的时候,必须制定当前新游戏的状态,比如是MainMenu显示主菜单呢还是直接进入正题Play开始进行游戏。为此我们自定义一个构造函数init(size: CGSize, gameState: GameState)传入gameState设置新游戏初识状态。请添加如下两个方法到GameScene.swift中的GameScene类中。

03.根据游戏状态来处理事件

请定位到update()方法,修改其中的内容:

override func update(currentTime: CFTimeInterval) { if lastUpdateTime > 0 { dt = currentTime - lastUpdateTime } else { dt = 0 } lastUpdateTime = currentTime switch gameState { case .MainMenu: break case .Tutorial: break case .Play: updateForeground() updatePlayer() //1 checkHitObstacle() //Play状态下检测是否碰撞了障碍物 //2 checkHitGround() //Play状态下检测是否碰撞了地面 break case .Falling: updatePlayer() //3 checkHitGround() //Falling状态下检测是否掉落至地面 此时已经失败了 break case .ShowingScore: break case .GameOver: break }}

其中1,2,3中三个方法均是通过状态标志位来处理碰撞事件,请添加checkHitObstacle()以及checkHitGround()方法到updateForeground()方法下方:

// 与障碍物发生碰撞func checkHitObstacle() { if hitObstacle { hitObstacle = false switchToFalling() }}// 掉落至地面func checkHitGround() { if hitGround { hitGround = false playerVelocity = CGPoint.zero player.zRotation = CGFloat.degreesToRadians() player.position = CGPoint(x: player.position.x, y: playableStart   player.size.width/2) runAction(hitGroundAction) switchToShowScore() }}// MARK: - Game States// 由Play状态变为Falling状态func switchToFalling() { gameState = .Falling runAction(SKAction.sequence([ whackAction, SKAction.waitForDuration, fallingAction ])) player.removeAllActions() stopSpawning() }// 显示分数状态func switchToShowScore() { gameState = .ShowingScore player.removeAllActions() stopSpawning()}// 重新开始一次游戏func switchToNewGame() { runAction(popAction) let newScene = GameScene(size: size) let transition = SKTransition.fadeWithColor(SKColor.blackColor(), duration: 0.5) view?.presentScene(newScene, transition: transition) }

完成后自然你发现stopSpawning()方法并未实现,因为我打算好好讲讲这个。早前在didMoveToView()方法中调用startSpawning()源源不断地产生障碍物,但是一旦游戏结束,我们所要做的事情有两个:1.停止继续产生障碍物;2.已经在场景中的障碍物停止移动。那么如何制定某个动作Action停止呢?答案是先为这个动作命名(简单来说设置一个Key而已),然后用removeActionForKey()来移除。

OK,找到startSpawning()方法,将runAction(overallSequence)替换成runAction(overallSequence, withKey: "spawn");定位到spawnObstacle()方法,分别设置bottomObstacletopObstacle精灵的名字,方便之后找到它们并进行操作:

...bottomObstacle.name = "BottomObstacle"worldNode.addChild(bottomObstacle)...topObstacle.name = "TopObstacle"worldNode.addChild(topObstacle)...

现在来实现stopSpawning()www.2527.com,方法,在startSpawning()下方添加就好:

func stopSpawning() { removeActionForKey worldNode.enumerateChildNodesWithName("TopObstacle", usingBlock: { node, stop in node.removeAllActions worldNode.enumerateChildNodesWithName("BottomObstacle", usingBlock: { node, stop in node.removeAllActions}

点击运行,我擦!还没来得及点就掉地上了......好吧,只能在游戏进入一瞬间先让Player向上蹦跶下。添加flapPlayer()didMoveToView()方法的最下方。

点击运行,Nice!!Player顺利穿过了障碍,不小心碰到了障碍物,再点击,等等!怎么还能动...好吧,看来touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)点击事件中我们并未根据游戏状态来处理,是时候修改了。

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { switch gameState { case .MainMenu: break case .Tutorial: break case .Play: flapPlayer() break case .Falling: break case .ShowingScore: switchToNewGame() break case .GameOver: break }}

点击运行,失败重新开始游戏...等等貌似还有问题,怎么点击想重新开始游戏会突然掉落到地面上...好吧,请看lecture02中的时间间隔图,匆忙的你找找原因,试试解决吧。

Shake动画

先前说到Player撞击障碍物后要有一个摇晃的动画以及闪烁的小锅,那样显得更有真实感不是吗,这里需要调用screenShakeWithNode来实现,摇晃对象是谁?自然是worldNode喽。

由于内容简单,请直接定位到switchToFalling()方法,替换早前内容:

enum Layer: CGFloat { case Background case Obstacle case Foreground case Player case UI case Flash //新增一个层}func switchToFalling() { gameState = .Falling // Screen shake let shake = SKAction.screenShakeWithNode(worldNode, amount: CGPoint(x: 0, y: 7.0), oscillations: 10, duration: 1.0) worldNode.runAction // Flash let whiteNode = SKSpriteNode(color: SKColor.whiteColor(), size: size) whiteNode.position = CGPoint(x: size.width/2, y: size.height/2) whiteNode.zPosition = Layer.Flash.rawValue worldNode.addChild(whiteNode) whiteNode.runAction(SKAction.removeFromParentAfterDelay runAction(SKAction.sequence([ whackAction, SKAction.waitForDuration, fallingAction ])) player.removeAllActions() stopSpawning()}

哦对了,请注释掉GameViewController.swift中的几行代码,去掉所有调试信息,这样才是一个完整的游戏;

// 4.设置一些调试参数//skView.showsFPS = true // 显示帧数//skView.showsNodeCount = true // 显示当前场景下节点个数//skView.showsPhysics = true // 显示物理体//skView.ignoresSiblingOrder = true // 忽略节点添加顺序

点击运行,享受你的劳动果实吧!

init(size: CGSize, gameState: GameState) { self.gameState = gameState super.init(size: size)}

结尾

这个游戏系列文章终于连载完成,当时可能是一时兴起,最后还是坚持下来了。文章更多是在叙述整个游戏是如何开发出来,并未在一些基础知识以及实现原理上细说,这是之后我要补充的,最后谢谢大家的支持。如果觉得不错,请点击喜欢并关注我,同时将我的文章推荐给你的朋友。8~

添加完毕之后,你会发现编译器报错,这也是情理之中,毕竟修改了构造方法导致早前的初始化方法都不能使用了。不急,慢慢修改。请定位到switchToNewGame()方法,要知道早前我们开始一个新游戏就是调用该函数,但是未指定新游戏的状态,为此我们要大刀阔斧地小改一番...如下:

func switchToNewGame(gameState: GameState) { //改动1 添加了一个传入参数 runAction(popAction) let newScene = GameScene(size: size,gameState:gameState)//修改传入参数 let transition = SKTransition.fadeWithColor(SKColor.blackColor(), duration: 0.5) view?.presentScene(newScene, transition: transition)}

wo ca!!这下早前所有调用switchToNewGame()方法的地方都报错了。请不要着急,凡是循序渐进,首先找到touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)方法,这次真要大改一番了:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { //1 let touch = touches.first let touchLocation = touch?.locationInNode switch gameState { case .MainMenu: //2 if touchLocation?.y < size.height * 0.15 { //TODO: 之后添加 } else if touchLocation?.x < size.width * 0.6 { //3 switchToNewGame(.Tutorial) } break case .Tutorial: //TODO: 之后添加 break case .Play: flapPlayer() break case .Falling: break case .ShowingScore: break case .GameOver: //4 if touchLocation?.x < size.width * 0.6 { switchToNewGame(.MainMenu) } break }}

改动还是蛮大的,起码现在需要根据你点击的位置来执行相应的点击事件:

  1. 获得第一个点击,然后得到在场景中的位置Position,自然就是点Point:包括x坐标值和y坐标值了。
  2. 这里我们只是简单判断点击位置的范围,比如点击位置下偏下方时,就装作点击了"Learn to make this game的按钮"。
  3. 倘若通过位置判断,你点击了Play按钮,则新建一个初始游戏状态为.Tutorial的新游戏,此时并不会立刻开始游戏,而是显示一个教程画面,只有当再次点击时才会开始游戏。
  4. 此时处于游戏结束状态,通过点击OK按钮开启一个新游戏,但是游戏状态为.Menu。

此时还有个报错来自于"GameViewController.switf文件",请找到let scene = GameScene(size:CGSizeMake(320, 320 * aspectRatio))这一行,改为我们定义的构造方法let scene = GameScene(size:CGSizeMake(320, 320 * aspectRatio),gameState:.MainMenu)即可。

点击运行,我去!! 咋不灵了.....

貌似didMoveToView()方法中 我们并没有根据游戏初始状态来初始化游戏场景...请转到GameScene类中,定位到didMoveToView(),将其中内容替换成如下内容:

override func didMoveToView(view: SKView) { physicsWorld.gravity = CGVector(dx: 0, dy: 0) physicsWorld.contactDelegate = self addChild(worldNode) // 以下为替换内容 if gameState == .MainMenu { switchToMainMenu() } else { switchToTutorial() }}//MARK: Game States //添加剩余两个场景切换方法func switchToMainMenu() { gameState = .MainMenu setupBackground() setupForeground() setupPlayer() setupSomebrero() //TODO: 实现setupMainMenu()主界面布局 之后把注释去掉 }func switchToTutorial() { gameState = .Tutorial setupBackground() setupForeground() setupPlayer() setupSomebrero() setupLabel() //TODO: 实现setupTutorial()教程界面布局 之后把注释去掉}

其中我们还未实现对主界面的布局,以及教程界面的布局,这也是接下来所要干的事了。

实现主界面的布局:

代码貌似很长,但内容很熟悉不是吗,当年你在配置ScoreCard界面的时候不也这么做过?先布局几个button,然后执行几个动画罢了,请边码边回忆是怎么对精灵位置放置,添加动作的。

func setupMainMenu() { let logo = SKSpriteNode(imageNamed: "Logo") logo.position = CGPoint(x: size.width/2, y: size.height * 0.8) logo.zPosition = Layer.UI.rawValue worldNode.addChild // Play button let playButton = SKSpriteNode(imageNamed: "Button") playButton.position = CGPoint(x: size.width * 0.25, y: size.height * 0.25) playButton.zPosition = Layer.UI.rawValue worldNode.addChild(playButton) let play = SKSpriteNode(imageNamed: "Play") play.position = CGPoint.zero playButton.addChild // Rate button let rateButton = SKSpriteNode(imageNamed: "Button") rateButton.position = CGPoint(x: size.width * 0.75, y: size.height * 0.25) rateButton.zPosition = Layer.UI.rawValue worldNode.addChild(rateButton) let rate = SKSpriteNode(imageNamed: "Rate") rate.position = CGPoint.zero rateButton.addChild // Learn button let learn = SKSpriteNode(imageNamed: "button_learn") learn.position = CGPoint(x: size.width * 0.5, y: learn.size.height/2   kMargin) learn.zPosition = Layer.UI.rawValue worldNode.addChild // Bounce button let scaleUp = SKAction.scaleTo(1.02, duration: 0.75) scaleUp.timingMode = .EaseInEaseOut let scaleDown = SKAction.scaleTo(0.98, duration: 0.75) scaleDown.timingMode = .EaseInEaseOut learn.runAction(SKAction.repeatActionForever(SKAction.sequence([ scaleUp, scaleDown ]))) }

实现教程界面设置:

反观这个教程界面就显得简单多了,只需要添加一章玩法帮助的图就ok了,如下:

func setupTutorial() { let tutorial = SKSpriteNode(imageNamed: "Tutorial") tutorial.position = CGPoint(x: size.width * 0.5, y: playableHeight * 0.4   playableStart) tutorial.name = "Tutorial" tutorial.zPosition = Layer.UI.rawValue worldNode.addChild let ready = SKSpriteNode(imageNamed: "Ready") ready.position = CGPoint(x: size.width * 0.5, y: playableHeight * 0.7   playableStart) ready.name = "Tutorial" ready.zPosition = Layer.UI.rawValue worldNode.addChild }

好了,定位到switchToMainMenu()switchToTutorial()方法,把TODO字样的之后方法进行调用

点击运行项目,恩...出来了,而且再次点击Play会转到教程界面。不过再点击的话,貌似没反应了,聪明的你肯定会转到touchesBegan()方法,定位到.Tutorial状态,你会发现此时名下啥都没有,怎么可能开始愉快的玩耍呢???

为此在下方添加一个switchToPlay()方法并在.Tutorial下调用。

func switchToPlay() { // 从.Tutorial 状态转到.Play状态 gameState = .Play // 移除Tutorial精灵 worldNode.enumerateChildNodesWithName("Tutorial", usingBlock: { node, stop in node.runAction(SKAction.sequence([ SKAction.fadeOutWithDuration, SKAction.removeFromParent // 开始产生障碍物 从右向左移动 startSpawning() // 让Player 向上蹦跶一次... flapPlayer()}

点击运行项目,请尽情享受成功的果实吧!

倘若你对游戏某一部分不太熟悉,请到github下载所有课程的代码和课件。

此教程已接近尾声,博主忙于工作,文章更新速度不快,请见谅! 请期待下文对游戏的进一步优化。

TAG标签:
版权声明:本文由澳门新葡8455手机版发布于www.2527.com,转载请注明出处:www.2527.com用Swift做个游戏Lecture06,用Swift做个游戏