欢迎加入QQ讨论群258996829
来自星星的你 头像
苹果3袋
3
来自星星的你

Swift开发Sprite Kit游戏实践

发布时间:2015-05-28 18:47  回复:0  查看:4100   最后回复:2015-05-28 18:47  

本文作者Ajay Venkat是一名年仅13岁的iOS开发者,他非常喜欢用苹果的Sprite Kit 2D游戏框架来开发iOS游戏,在了解到很多同龄孩子也对学习如何使用Sprite Kit来开发iOS游戏非常感兴趣之后,他以自己用Swift语言所开发的一款名为“Space Monkey”的游戏为例,在Ray Wenderlich上写下了这篇指南,以下为译文: 

快速入门

首先要安装苹果的iOS和Mac应用免费开发工具——Xcode。如果还没有安装Xcode,可以从App Store上下载;如果已经安装好,须确保自己使用的是最新版本。安装好Xcode后,下载并解压starter project,双击SpaceMonkey.xcodeproj,文件就会在Xcode中打开。单击“Play”,接着你会看到一整片黑屏:

 

starter project给了一个好的起步,接下来就是着手开发自己的游戏了。我已经在project中添加了art(艺术效果)和sounds(音效),放在文件夹Sounds 和 sprites.atlas里。art能派上大用场,不妨用“Space Monkey”牛刀小试一下吧!

添加monkey

用Sprite Kit为游戏添加图片(比如space monkey)有三步:


  1. 创建sprite。第一步用SKSpriteNode 类创建一个sprite。sprite是图片的副本,可在游戏里随意移动。
  2. 定位sprite。第二步在屏幕上定位sprite,让space monkey从屏幕左边出场,逐渐移动到中间。
  3. 将sprite添加到场景中。创建sprite还不够,要展示sprite,需要把它添加到场景中。


一步一步来,打开GameScene.swift,将代码换成如下所示:

import SpriteKit 
class GameScene: SKScene {
  // 1 - Create the sprite
  let player = SKSpriteNode(imageNamed:"spacemonkey_fly02") 
  override func didMoveToView(view: SKView) {
    // 2 - Position the sprite
    player.position = CGPoint(x:frame.size.width * 0.1, y: frame.size.height * 0.5)
    // 3 - Add the sprite to the scene
    addChild(player)
    // 4 - Set scene background color to black
    backgroundColor = SKColor.blackColor()
  }
}

再回顾一下上述步骤:


  1. 用Art文件夹里的monkey图片通过SKSpriteNode(imageNamed:)创建SKSpriteNode 。注意:这一步将sprite存储在了类的一个property 里,方便开发者回头查看。
  2. 场景首次呈现时调用的是didMoveToView(_:),这里适合添加初始设置代码。这一步在屏幕的左边设定sprite的position 。
  3. 用sprite的名称调用 addChild(),再添加到场景里。
  4. 游戏场景的背景色设为黑色,贴合monkey在太空飞行的设定。


编写完成后运行,就能看见monkey飞翔在太空之中了:

 

接下来添加monkey的敌人!

添加敌人

monkey只有一个,而敌人有很多个,而且可能出现在屏幕的不同位置。

首先需要一些method(method)创建随机数字。将这些新method添加至GameScene.swift,放在 didMoveToView(_:)后(大括号之前):

func random() -> CGFloat {
  return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(#min: CGFloat, max: CGFloat) -> CGFloat {
  return random() * (max - min) + min
}

random()返回一个介于0到1之间的十进制值。random(min:max:)返回一个固定范围的随机值。指南不详述这些步骤的原理,读者只需用好这些method就够了。

接下来,将这个新的method直接添加在 random(min:max:)之后:

// 1
func spawnEnemy() {
  // 2
  let enemy = SKSpriteNode(imageNamed: "boss_ship")
  // 3
  enemy.name = "enemy"
  // 4
  enemy.position = CGPoint(x: frame.size.width, y: frame.size.height * random(min: 0, max: 1))
  // 5
  addChild(enemy)
}

再回顾一下上述步骤:


  1. 创建一个method,命名为spawnEnemy()。
  2. 跟添加monkey差不多,通过调用SKSpriteNode(imageNamed:)和添加图片名称创建一个sprite。
  3. 为sprite添加名称,方便以后辨认。
  4. 在屏幕右边设定敌人在X轴位置,而Y轴位置用random()设定一个随机值。
  5. 最后只要用addChild()把sprite添加到parent/场景中。

剩下的就是多次调用这个method了!调用前先创建actions的序列,使敌人以固定时间间隔出现在屏幕上。将这一行添加到didMoveToView(_:)末尾:

runAction(SKAction.repeatActionForever( SKAction.sequence([ SKAction.runBlock(spawnEnemy),
        SKAction.waitForDuration(1.0)])))
这里调用spawnEnemy(),用不断重复的序列创建一个action,等待时长为1秒,即敌人出现的间隔。


这时代码看起来如下所示:

import SpriteKit
class GameScene: SKScene {
  let player = SKSpriteNode(imageNamed:"spacemonkey_fly02")
  override func didMoveToView(view: SKView) {
    player.position = CGPoint(x:frame.size.width * 0.1, y: frame.size.height * 0.5)
    addChild(player)
    backgroundColor = SKColor.blackColor()
    runAction(SKAction.repeatActionForever(
      SKAction.sequence([
        SKAction.runBlock(spawnEnemy),
        SKAction.waitForDuration(1.0)])))
  }
  func random() -> CGFloat {
    return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
  }
  func random(#min: CGFloat, max: CGFloat) -> CGFloat {
    return random() * (max - min) + min
  } 
  func spawnEnemy() {
    let enemy = SKSpriteNode(imageNamed: "boss_ship")
    enemy.name = "enemy"
    enemy.position = CGPoint(x: frame.size.width, y: frame.size.height * random(min: 0, max: 1))
    addChild(enemy)
  }
}

编写完成后运行,敌人就出现在屏幕右边的随机位置上了:

 

让敌人们动起来

先对“敌人”的代码进行微调,使它们在出现之前,完全隐藏在屏幕后(而非仅仅隐藏一半)。从天而降的设定为游戏增加了难度,也增添了乐趣。

那么首先要做的就是更新spawnEnemy()那一行代码,敌人sprite的position编写如下:

enemy.position = CGPoint(x: frame.size.width + enemy.size.width/2, 
  y: frame.size.height * random(min: 0, max: 1))

现在,用更多action让敌人从屏幕一端移动到另一端,游戏变得更有趣了。

将这一行代码添加到spawnEnemy()末尾:

enemy.runAction(
  SKAction.moveByX(-size.width - enemy.size.width, y: 0.0, 
    duration: NSTimeInterval(random(min: 1, max: 2))))

此处稍作解释:


  1. 敌人sprite运行一个SKAction。
  2. SKAction包含一个moveByX() method,用于控制敌人在X轴上移动的固定距离。将整个屏幕画面设置为左移(-size.width),还要设置完整尺寸的sprite (-enemy.size.width)。
  3. SKAction有个规定sprite移动速度的时间参数;此处设定SKAction后,每1-2秒就改变一个随机值,加快了敌人的移动速度。


编写完成后运行,结果应该是敌人移动到了屏幕另一端,然后完全消失。如下所示:

物理移动sprite

游戏的设定是:不点击屏幕时,monkey会落下来;点击时,monkey会跳起。

这里用SKAction移动monkey,就像之前移动敌人一样。推荐用Sprite Kit内置的物理引擎,更加简单。

好,现在来试试看。还是GameScene.swift,在didMoveToView(_:)后添加如下代码:

player.physicsBody = SKPhysicsBody(circleOfRadius:player.frame.size.width * 0.3)
player.physicsBody?.allowsRotation = false

第一行为monkey创建了一个physics body,在物理引擎的作用下,monkey因引力和其他外力而落下”。

注意:physics body(物理实体)的形状是圆的,仅跟monkey的形状近似而已。无需做到精确,只要凑效就好。同时将physics body设定为不旋转。

编写完成后运行,就能看到monkey在屏幕上时而落下,时而消失,很酷吧?

物理推力

为了避免monkey“落下”,需要用物理推力让它重新跳起来。

这时要在spawnEnemy()后添加一个新的method:

func jumpPlayer() {
  // 1
  let impulse =  CGVector(dx: 0, dy: 75)
  // 2
  player.physicsBody?.applyImpulse(impulse)
}

再回顾一下上述步骤:


  1. 首先创建一个固定数值推动力的CGVector,规定monkey跳起的距离。我也是尝试了多次才总结出具体数值的。
  2. 用applyImpulse()制造推力,再转化为线速度和角速度推力。理论上,monkey在穿行太空的时候还会旋转,所以刚刚才要将physics body设定为不旋转。


代码在被调用之前,monkey是不能跳起来的;要使monkey跳起来,就要重写点击屏幕时调用的那个method。在jumpPlayer()底下复制这些代码:

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
  jumpPlayer()
}

点击屏幕时,自动调用这些method。

现在就差一步了——在 didMoveToView(_:)后添加如下代码:

// 1
let collisionFrame = CGRectInset(frame, 0, -self.size.height * 0.2)
// 2
physicsBody = SKPhysicsBody(edgeLoopFromRect: collisionFrame)

代码会在屏幕边缘产生一个特殊的physics body,避免monkey飞出或者坠落在太空中。现在回顾一下上述代码:


  1. 首先创造一个可通过CGRectInset()扩大或缩小至多20%的矩形,即monkey的活动范围。monkey的轮廓可以稍微消失在屏幕外,但不能完全消失不见。
  2. 然后设定场景本身的physics body。刚刚创建的physics body是圆的,此处将它变为一个循环边,即“矩形的边缘”,不过听上去更简洁些。


编写完成后运行,就能看到如下场景:

 

一只蹦蹦跳跳的小monkey出现啦!

碰撞检测

到目前为止,如果monkey遇到敌人,可以跳过去;但是跟敌人相撞的话,什么效果都没有,所以需要在游戏中添加碰撞检测(collision detection),有如下几步:


  1. 为所有sprite创建physics body:现在monkey已经有了一个physics body,但是敌人还没有,所以先完成这一步。
  2. 为每个physics body设定category(类别)和contact(接触点):这一步为sprite分类,比如为monkey和敌人设定不同的类别;同样可以在特定的physics body和其他类别的physics body之间设定“contact(接触点)”。
  3. 设置Contact Delegate:设定两个physics body何时接触。要搞清不同的physics body属于哪个类别,是敌人还是monkey。大功告成!


还记得怎么给monkey添加physics body吗?现在轮到为敌人的sprite添加physics body了,来制造碰撞效果。

首先将如下所示添加至GameScene.swift最顶端:

enum BodyType: UInt32 {
  case player = 1
  case enemy = 2
  case ground = 4
}

这里要做的就是为每个sprite创建类别。ground number不是针对sprite,而是针对应用边框设定的,所以当monkey碰到屏幕边缘时会弹起,而不是落到屏幕之外!

接下来,执行SKPhysicsContactDelegate协定,标记GameScene(游戏场景):

class GameScene: SKScene, SKPhysicsContactDelegate {
协议的作用是保证代码执行特定的method。此处执行针对两个physics body相撞的method。然后调整contactDelegate的值,将如下代码添加到didMoveToView(_:)末尾:

physicsWorld.contactDelegate = self

完成后,两个physics body碰撞时,物理世界就会自动调用代码中的method。

在spawnEnemy()末尾添加如下代码:

// 1
enemy.physicsBody = SKPhysicsBody(circleOfRadius: enemy.size.width/4)
// 2
enemy.physicsBody?.dynamic = false
// 3
enemy.physicsBody?.affectedByGravity = false
// 4
enemy.physicsBody?.allowsRotation = false
// 5
enemy.physicsBody?.categoryBitMask = BodyType.enemy.rawValue
// 6
enemy.physicsBody?.contactTestBitMask = BodyType.player.rawValue
// 7
enemy.physicsBody?.collisionBitMask = 0

此处稍稍解释一下:


  1. 为敌人创建physics body。physics body不一定要跟sprite的形状完全吻合,近似就好。这里用的是圆形,半径设为sprite的1/4,免得碰撞效果太猛。
  2. 把dynamic关掉,实现物理控制sprite。
  3. 防止引力对sprite的影响。这一步不言自明,主要让敌人的sprite避免物理引力的干扰。
  4. 这一步是为了避免sprite在physics body碰撞时旋转。
  5. 将类别位掩码设为之前设置过的敌人类别。
  6. 敌人和monkey接触时,Sprite Kit发出提醒。
  7. 为monkey设置 collisionBitMask后,当接触到敌人时,两者会互相弹开;如果不想要这种效果,将值设为0。


将如下所示添加到didMoveToView(_:)的后面:

physicsBody?.categoryBitMask = BodyType.ground.rawValue
 
player.physicsBody?.categoryBitMask = BodyType.player.rawValue
player.physicsBody?.contactTestBitMask = BodyType.enemy.rawValue
player.physicsBody?.collisionBitMask = BodyType.ground.rawValue

这里为monkey和ground设置类别和碰撞位掩码,让两者彼此碰撞;在monkey和敌人之间设置“contact(接触点)”。

现在到了最重要的一步,完善碰撞检测,执行之前提到的method来处理“contacts”:

func didBeginContact(contact: SKPhysicsContact) {
  let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
  switch(contactMask) {
  case BodyType.player.rawValue | BodyType.enemy.rawValue:
    let secondNode = contact.bodyB.node
    secondNode?.removeFromParent()
    let firstNode = contact.bodyA.node
    firstNode?.removeFromParent()
  default:
    return
  }
}

因为之前已将场景设置为物理世界的contactDelegate,两个physics body碰撞时会自动调用这个method。

它将两个位掩码结合成一个单个的接触点掩码,检验是否是monkey和敌人相撞,如果是,就将两者从屏幕上移除。

编写完成后运行,效果如下:

Game Over!

如果monkey跟敌人相撞,或落出屏幕的话,就会显示”Game Over“,接着出现重新点击开始游戏的画面。

首先将以下所有变量添加到GameScene顶端,在 let player = SKSpriteNode(imageNamed:"spacemonkey_fly02")之后:

// 1 var gameOver = false // 2 let endLabel = SKLabelNode(text: "Game
            Over") let endLabel2 = SKLabelNode(text: "Tap to restart!") let touchToBeginLabel
            = SKLabelNode(text: "Touch to begin!") let points = SKLabelNode(text: "0")
            // 3 var numPoints = 0 // 4 let explosionSound = SKAction.playSoundFileNamed("explosion.mp3",
            waitForCompletion: true) let coinSound = SKAction.playSoundFileNamed("coin.wav",
            waitForCompletion: false)
再回顾一下上述步骤:



  1. 首先创建一个gameOver布尔变量,不论游戏是否结束,都进行跟踪记录。
  2. 创建一些label node,好在Sprite Kit中设置屏幕上显示的字幕。
  3. 创建integer储存分数。注意,用var来标记 integer,不是let,方便之后进行修改。
  4. 最后创建一些action,随后制造音效。


下一步创建名为setupLabels()的新方法:

func setupLabels() {
  // 1
  touchToBeginLabel.position = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
  touchToBeginLabel.fontColor = UIColor.whiteColor()
  touchToBeginLabel.fontSize = 50
  addChild(touchToBeginLabel)
 
  // 2  
  points.position = CGPoint(x: frame.size.width/2, y: frame.size.height * 0.1)
  points.fontColor = UIColor.whiteColor()
  points.fontSize = 100
  addChild(points)
}

再回顾一下上述步骤:


  1. 将“touch to begin(点击开始)”标签放在屏幕中央,字体白色,大小50pt。
  2. 将position label设在屏幕底端,白色,大小100。


现在在didMoveToView(_:)里调用setupLabels():

setupLabels()
再删掉touchesBegan(_:withEvent:),添加如下代码:

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
  // 1
  if (!gameOver) {
    if player.physicsBody?.dynamic == false {
      player.physicsBody?.dynamic = true
      touchToBeginLabel.hidden = true
      backgroundColor = SKColor.blackColor()
 
      runAction(SKAction.repeatActionForever(
        SKAction.sequence([
          SKAction.runBlock(spawnEnemy),
          SKAction.waitForDuration(1.0)])))
    }
    // 2
    jumpPlayer()
 
  }
  // 3
  else if (gameOver) {
    let newScene = GameScene(size: size)
    newScene.scaleMode = scaleMode
    let reveal = SKTransition.flipHorizontalWithDuration(0.5)
    view?.presentScene(newScene, transition: reveal)
  }
}

回顾一下:


  1. 如果游戏还没结束,monkey还处于非动态(受物理引擎控制),同样说明新游戏还没开始。这时将dynamic设置为true,隐藏label,大批敌人开始出现在屏幕上。
  2. 不管怎样都要调用 jumpPlayer ,因为只有dynamic 设置为true的时候,它才能被调用。
  3. 如果游戏结束了,要重新开始,那么创建一个新的GameScene ,显示在屏幕上。


接下来在代码中添加如下方法:

override func update(currentTime: CFTimeInterval) {
  //1
  if !gameOver {
    //2
    if player.position.y <= 0 {
      endGame()
    }
    //3
    enumerateChildNodesWithName("enemy") {
      enemy, _ in
      //4
      if enemy.position.x <= 0 {
        //5
        self.updateEnemy(enemy)
      }
    }
  }
}

回顾一下:


  1. 如果gameOver 为false的话,看看monkey是否消失了,如果是,调用endGame——这个随后再添加。
  2. 用enumerateChildNodesWithName 查看场景中的带有“enemy(敌人)“的对象。每当一个敌人消失,就会调用updateEnemy()——这个也是随后再添加。


现在添加名为updateEnemy()的method,在框架渲染前调用——产生的效果是:每当一个敌人消失,玩家就会得1分:

func updateEnemy(enemy: SKNode) { //1 if enemy.position.x < 0 { //2
                    enemy.removeFromParent() //3 runAction(coinSound) //4 numPoints++ //5 points.text
                    = "\(numPoints)" } }

再回顾一下上述步骤:


  1. 看一下敌人是否在在屏幕外的x轴上。
  2. 如果是,把敌人从parent上移除(从游戏中移除)。
  3. 将玩家分数储存在numPoints里。
  4. 将分数转化为分数条,再将分数条插入分数label。


现在需要稍稍改变下didBeginContact(_:)了。从parent移除第一个节点之后,添加这行代码:

endGame()
现在终于可以用endGamemethod了,久等了:

func endGame() {
  // 1
  gameOver = true
  // 2
  removeAllActions()
  // 3
  runAction(explosionSound)
 
  // 4
  endLabel.position = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
  endLabel.fontColor = UIColor.whiteColor()
  endLabel.fontSize = 50
  endLabel2.position = CGPoint(x: frame.size.width/2, y: frame.size.height/2 + endLabel.fontSize)
  endLabel2.fontColor = UIColor.whiteColor()
  endLabel2.fontSize = 20
  points.fontColor = UIColor.whiteColor()
  addChild(endLabel)
  addChild(endLabel2)
}

现在来回顾一下:


  1. 将gameOver设置为true。
  2. 移除场景中的所有action,终止所有动画效果。
  3. 运行爆炸声效。
  4. 定位,再为场景添加endLabel。


现在用didMoveToView(_:)移除这个代码块:

backgroundColor = SKColor.blackColor()
 
runAction(SKAction.repeatActionForever(
  SKAction.sequence([
    SKAction.runBlock(spawnEnemy),
    SKAction.waitForDuration(1.0)])))
最后添加一行:

player.physicsBody?.dynamic = false

这样设置之后,点击屏幕后游戏才会开始,否则monkey是不会移动的。

编写完成后运行,游戏大功告成了!

背景音乐



等会儿,还有一件事!打开ViewController.swift,添加一个新属性:

var backgroundMusicPlayer: AVAudioPlayer!
还要添加一个新method:

func playBackgroundMusic(filename: String) {
  let url = NSBundle.mainBundle().URLForResource(
    filename, withExtension: nil)
  if (url == nil) {
    println("Could not find file: \(filename)")
    return
  }
 
  var error: NSError? = nil
  backgroundMusicPlayer =
    AVAudioPlayer(contentsOfURL: url, error: &error)
  if backgroundMusicPlayer == nil {
    println("Could not create audio player: \(error!)")
    return
  }
 
  backgroundMusicPlayer.numberOfLoops = -1
  backgroundMusicPlayer.prepareToPlay()
  backgroundMusicPlayer.play()
}

这个method很方便,用来添加一些背景音乐。工作原理在此不详述。

用起来很简单,只要在skView.presentScene(scene)那一行后添加viewWillLayoutSubviews():

playBackgroundMusic("BackgroundMusic.mp3")
编写好之后运行,美妙的背景音乐出现啦!太棒了!

您还未登录,请先登录

热门帖子

最新帖子