Sprite Kit Swift 初学者教程

Tags: sprite-kit swift

这是我们的网站上迅速更新的热门教程,作为iOS8教程的一部分发布。

就像蝙蝠侠和罗宾还是超人和Lois Lane一样,Sprite和Swift是一个不可思议的组合:

  • Sprite Kit是iOS游戏开发的最佳工具之一。它简单易学,功能强大,并且完全支持IOS。

  • Swift是一个简单的开发语言,特别是如果你是一个iOS平台开发的初学者。

在本教程中,您将学习如何运用Swift语言使用苹果的2D游戏框架Sprite Kit来创建一个简单的2D游戏!
你可以跟随本教程,或者只是直接跳到在底部的示例项目。

注:本教程原来的版本是发布在我们的网站上的第一个教程,是针对一个完全不同的语言(Objective-C)和一个完全不同的游戏框架(cocos2d)编写的。呵呵,时代变了!

Sprite Kit对比Unity

最流行的在Sprite Kit之外可选的游戏框架是unity。unity最初是作为一个3D引擎开发的,但它最近得到了全面的2D的支持。
在你开始之前,我建议你把学习一下为什么Sprite Kit或Unity是你的游戏开发的最佳选择。

Sprite Kit的优势

    1. 它内置于iOS的。不需要下载额外的库或者有外部依赖。您也可以无缝地使用它就像使用其他iOS的API一样,比如:iAd、应用内购买等,而不必依赖于额外的插件。
    2. 它利用现有的技术。如果你已经知道swift和iOS开发,你可以非常快熟悉sprite kit。
    3. 它是apple开发的。这说明框架将得到很好的支持。
    4. 它是免费的。你将得到所有的sprite kit的功能,而不需要任何费用。unity确实有一个免费版本,但并没有全部的Pro版本功能(你需要升级,如果你想避免的unity闪屏)。

unity的优势
    1. 跨平台的。这是其中的大的一个优势。如果您使用sprite kit那么你只能使用iOS系统。使用Unity,你可以方便地移植你的游戏到Android,Windows和其他平台。
    2. 视觉场景设计。使用unity非常容易实时设计和测试你的游戏。Sprite key在iOS8平台下有一个非常基本的场景编辑器,但相比unity来说这个编辑器只提供非常基本的功能。
    3. 资源商店。unity配备了一个内置的资源商店,在哪里可以为你的游戏买到各种组件。其中一些组件可以为您节省大量开发时间!
    4. 更强大。在一般情况下,unity比sprite kit/scene kit组合拥有更多的特性和功能。

我该选择哪个?

看完上面的对比你可能会想,“好吧,2D框架我应该选择哪一个?”

答案取决于你的目标,这里是我的建议:

  • 如果你是一个初学者,或者只专注于iOS的:使用sprite kit 因为它是内置的,简单易学,并且将完成你的工作。

  • 如果你想跨平台或者需要制作更复杂的游戏:使用unity它更加强大和灵活。

如果您认为unity就是你需要的,看看我们的一些unity教程以及我们新制作的unity的系列视频教程

否则,请继续阅读,我们将开始使用sprite kit!

你好,Sprite Kit

我们先从简单的Hello World项目开始学习Sprite Kit(xcode6内置了sprite kit支持)

启动Xcode, 选择File\New\Project, 选择iOS\Application\Game template, 点击Next:

001_New_Game

输入产品名称为SpriteKitSimpleGame,选择Swift作为开发语言,选择SpriteKit作为游戏开发框架,选择目标设备为iPhone, 点击Next:

002_Options

保存刚才的项目,点击Create选择你的Iphone6模拟器,在模拟器中运行刚才的项目,稍等会你将看到下面的界面:

003_Hello_World

Sprite Kit是用场景进行组织,有些像游戏的“levels”或“screens”的概念。例如,你可能有一个场景做为主要游戏区,另有一个场景作为各个关卡间的转换。

仔细看下你的项目,你会看到模板已经为你默认创建了一个场景- :GameScene。打开GameScene.swift,你会看到,它包含了一些代码用户将label放置在屏幕上,并且当你点击屏幕的某个地方是会出现一个旋转飞船。

在本教程中,您将主要在GameScene上工作。不过在开始之前,你必须做出一些调整,因为这个游戏需要横向运行而不是纵向的。

初始调整

为您提供的模板有两个问题。首先,它设置游戏界面是纵向的,但你想要的是横向界面。其次,它是目前使用Sprite Kit的场景编辑器,你在这个教程中不需要使用。让我们来解决这些问题。

首先,点击Project Navigator上的SpriteKitSimpleGame项目,选择SpriteKitSimpleGame执行目标打开目标设定。然后,在部署信息部分,取消选择Portrait保证只检查横向左和右横向,如下图所示:

004_Landscape

第二部,选择删除GameScene.sks,当提示时选择移动到回收站。该文件允许你可视化的布局sprite和其他组件,但是,对于这个游戏它只是更容易以编程方式创建的组件,所以你并不需要它。

接着,打开GameViewController.swift并替换成以下内容:

import UIKitimport SpriteKit
 class GameViewController: UIViewController {   override func viewDidLoad() {
    super.viewDidLoad()
    let scene = GameScene(size: view.bounds.size)
    let skView = view as SKView
    skView.showsFPS = true
    skView.showsNodeCount = true
    skView.ignoresSiblingOrder = true
    scene.scaleMode = .ResizeFill
    skView.presentScene(scene)
  }   override func prefersStatusBarHidden() -> Bool {
    return true
  }}

GameViewController是一个普通的UIViewController,除了它的根视图是SKView(它是一个包含SpriteKit场景的视图)。

在这里,你需要实现viewDidLoad()方法,在启动时创建GameScene的新实例,保证实例的的大小与view相同。

这就是它的初始设置 - 现在让我们在屏幕上弄点东西出来!

增加sprite

首先,为项目下载资源并将其拖动到您的Xcode项目。确保“Copy items into destination group’s folder (if needed)”被选中,你的SpriteKitSimpleGame的目标已选择。

接着,打开GameScene.swift并替换为以下内容:

import SpriteKit
 
class GameScene: SKScene {
 
  // 1
  let player = SKSpriteNode(imageNamed: "player")
 
  override func didMoveToView(view: SKView) {
    // 2
    backgroundColor = SKColor.whiteColor()
    // 3
    player.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
    // 4
    addChild(player)
  }
}


让我们在这一步步来说明:

    1. 你声明一个私有常量player(如:ninja),这是sprite的一个例子。正如你看到的,创建一个sprite是很容易-只需传递需要使用的图像名称。

    2. 在Sprite Kit设置场景的背景颜色很简单,只要设置backgroundColor属性。在这里,你将其设置为白色。

    3. 您设置sprite的位置为10%跨越垂直居中和水平。

    4. 为了让sprite在场景中显示,你必须将其添加为场景的子节点。这类似于你创建view的子节点。

构建和运行,瞧 -先生们,女士们,忍者已经进入大楼!

005_Ninja

移动怪兽

接下来,您要添加一些怪物到场景中让忍者殴打。为了让事情更有趣,你需要让怪物可以移动-否则就不会有太大的挑战!因此,让我们在稍微偏离屏幕右侧的位置创造怪物,并设置为他们移动到左侧。

在GameScene.swift添加如下方法:

func random() -> CGFloat {
  return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
 
func random(#min: CGFloat, max: CGFloat) -> CGFloat {
  return random() * (max - min) + min
}
 
func addMonster() {
 
  // Create sprite
  let monster = SKSpriteNode(imageNamed: "monster")
 
  // Determine where to spawn the monster along the Y axis
  let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
 
  // Position the monster slightly off-screen along the right edge,
  // and along a random position along the Y axis as calculated above
  monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
 
  // Add the monster to the scene
  addChild(monster)
 
  // Determine speed of the monster
  let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
 
  // Create the actions
  let actionMove = SKAction.moveTo(CGPoint(x: -monster.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
  let actionMoveDone = SKAction.removeFromParent()
  monster.runAction(SKAction.sequence([actionMove, actionMoveDone]))
 
}

我尽可能以容易理解方式详细阐述如何完成这些工作。就像你刚才在sprite中配置player一样,你做一些简单的计算,以确定您要创建的对象,设置对象的位置,并将其添加到场景中。

给新的元素添加行为。Sprite Kit提供了很多非常方便的内置操作,可帮助您轻松地根据时间改变精灵的状态,如移动,旋转,淡化,动画,等等。在这里,您对怪物应用三个动作:


  • SKAction.moveTo(_:duration:):你用这个动作来直接将对象移出屏幕的左侧。请注意,您可以指定运动的持续时间,在这里你随机选择2-4秒不等的速度。

  • SKAction.removeFromParent():Sprite kit自带的,可以方便的从父节点移除一个节点,有效地从场景“删除它”。在这里,当怪物不再可见时您可以使用这个动作删除怪物。这是很重要的,否则你的怪物就源源不绝,并最终将消耗掉所有设备资源。

  • SKAction.sequence(_:):顺序操作,将动作连接在一起,一次一个执行的顺序。通过这种方式,你可以在完成执行“从父节点删除”的动作后执行“移动”动作

在你开始工作之前的最后一件事是,你需要调用方法来创建怪物!并把事情变得好玩,让我们不断的随着时间的推移产生怪物。

只需将以下代码添加到didMoveToView()的尾部:

runAction(SKAction.repeatActionForever(
  SKAction.sequence([
    SKAction.runBlock(addMonster),
    SKAction.waitForDuration(1.0)
  ])
))

在这里,您运行一系列动作去调用代码块(),然后等待1秒。然后,不停的重复的动作顺序。

就这么多!生成并运行该项目,现在你应该可以看到怪物愉快地在屏幕上移动:

006_Monsters

射击子弹

在这忍者需要开始执行一些动作-所以,让我们从射击开始!你可以实现射击的方法有很多,但对于这个游戏你需要在用户点击屏幕时,由忍者向目标抛射子弹。

我想用“移动”这个动作来实现这个功能可以使代码保持简单,但为了使用这个动作,你必须做一点数学作业。

这是因为“移动”动作需要你给出子弹的目的地,但你不能只使用触摸点,因为触摸点代表相对于玩家射击方向。实际上,你想保留子弹从触摸点开始直至离开屏幕的移动轨迹。

下面的图片描述了这个想法:

Projectile Triangle

因此,大家可以看到,你的x和y从原点到触摸点的偏移创造了一个小三角。你只需要做出一个大的相似三角形 - 你知道你想要其中的一个端点是离开屏幕的。

为了运行这些计算,如果你有一些基本的向量数学例程可以调用(如向量的add、subtract)它真的会有些帮助。然而,Sprite Kit默认情况下不具备这类防范,所以你必须自己编写。

幸运的是,得益于Swift操作符重载的特性这些方法很容易写。在您的文件的顶部添加这些函数,在GameScene类的前面:

func + (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
 
func - (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
 
func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
  return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
 
func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
  return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
 
#if !(arch(x86_64) || arch(arm64))
func sqrt(a: CGFloat) -> CGFloat {
  return CGFloat(sqrtf(Float(a)))
}
#endif
 
extension CGPoint {
  func length() -> CGFloat {
    return sqrt(x*x + y*y)
  }
 
  func normalized() -> CGPoint {
    return self / length()
  }
}

这些都是一些向量数学函数的标准实现。如果你搞不清楚这里发生了什么或刚开始学习向量数学,看看这个快速向量数学解释。

接下来,向文件添加一个新的方法:

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
 
  // 1 - Choose one of the touches to work with
  let touch = touches.anyObject() as UITouch
  let touchLocation = touch.locationInNode(self)
 
  // 2 - Set up initial location of projectile
  let projectile = SKSpriteNode(imageNamed: "projectile")
  projectile.position = player.position
 
  // 3 - Determine offset of location to projectile
  let offset = touchLocation - projectile.position
 
  // 4 - Bail out if you are shooting down or backwards
  if (offset.x < 0) { return }
 
  // 5 - OK to add now - you've double checked position
  addChild(projectile)
 
  // 6 - Get the direction of where to shoot
  let direction = offset.normalized()
 
  // 7 - Make it shoot far enough to be guaranteed off screen
  let shootAmount = direction * 1000
 
  // 8 - Add the shoot amount to the current position
  let realDest = shootAmount + projectile.position
 
  // 9 - Create the actions
  let actionMove = SKAction.moveTo(realDest, duration: 2.0)
  let actionMoveDone = SKAction.removeFromParent()
  projectile.runAction(SKAction.sequence([actionMove, actionMoveDone]))
 
}

编译并运行代码,现在忍者可以在发现怪物时发射子弹了。

007_Shoot

碰撞检测和物理知识:概念

   所以,现在你有shurikens到处飞 - 但你的忍者真正要做的是放到他们。因此,让我们添加一些代码来检测子弹目标碰撞。
Sprite Kit套件优秀的地方之一是它带有内建的物理引擎!不仅仅只是是物理引擎它非常适合模拟真实的动作,但他们也非常适合用来进行碰撞检测。
让我们设置游戏中使用Sprite Kit套件的物理引擎,以确定何时怪物和子弹碰撞。简要来说,这就是你要做的事情:

  • 建立物理引擎。一个物理引擎是运行物理运算的模拟空间。一是设置长青的默认情况,你可能要在其上配置的一些属性,比如重力。

  • 为每个怪物创建物理机构为。在Sprite Kit套件,可以为每个精灵关联形状用于碰撞检测,并在其上设置某些属性。这就是所谓的物理身体。注意,在Sprite中物理身体不必是完全相同的形状。通常这是一个简单的,大致的形状,而不是像素完美的,这对大多数游戏和性能要求来说已经足够了。

  • 为每种精灵设定一个类别。首先你可以为一个物理体设置类的属性是,它是一个位掩码,表示组(或很多组)。在这个游戏中,你将有两大类 - 一个是子弹,一个是怪物。然后,当两个物理体碰撞后,你可以很容易地通过其类别判断需要进行什么处理处理。

  • 设置委托关系。请记住这是物理世界。嗯,你可以设置当两个物理身体碰撞通知相关委托。在那里,你会写一些代码来检查对象的类别,判断他们是怪物或子弹!

现在你明白了作战计划,是时候付诸行动了!

碰撞检测和物理知识:实现

在GameScene.swift中增加以下结构:

struct PhysicsCategory {
  static let None      : UInt32 = 0
  static let All       : UInt32 = UInt32.max
  static let Monster   : UInt32 = 0b1       // 1
  static let Projectile: UInt32 = 0b10      // 2
}

这是建立物理类的常数,你需要使用一个位


注意:您可能想了解上面奇怪的语法。请注意,Sprite Kit用一个32位整数来表示一个类别。这个奇特的表示方式表示32位整数中的每个位用来表示单个类别(因此你最大可以有32个类别)。在这里,你设置的第一个位来表示一个怪物,下一位来表示一个子弹。

接下来,修改GameScene类实现SKPhysicsContactDelegate:

class GameScene: SKScene, SKPhysicsContactDelegate {

在didMoveToView(_:) 方法中添加玩家到场景后增加以下代码:

physicsWorld.gravity = CGVectorMake(0, 0)
physicsWorld.contactDelegate = self

这将设置物理时间没有重力,并将场景定义为当两个物理身体碰撞时被的委托。

在addMonster()方法中创建了怪物精灵之后添加这些行:

monster.physicsBody = SKPhysicsBody(rectangleOfSize: monster.size) // 1
monster.physicsBody?.dynamic = true // 2
monster.physicsBody?.categoryBitMask = PhysicsCategory.Monster // 3
monster.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile // 4
monster.physicsBody?.collisionBitMask = PhysicsCategory.None // 5

接下来添加类似的代码到touchesEnded(_:withEvent:)方法中,在设定子弹的位置的代码之后:

projectile.physicsBody = SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.dynamic = true
projectile.physicsBody?.categoryBitMask = PhysicsCategory.Projectile
projectile.physicsBody?.contactTestBitMask = PhysicsCategory.Monster
projectile.physicsBody?.collisionBitMask = PhysicsCategory.None
projectile.physicsBody?.usesPreciseCollisionDetection = true

接下来,添加一个当子弹与怪物碰撞将要调用的方法。需要注意的是这里没有自动调用,你将在以后调用这个方法。

func projectileDidCollideWithMonster(projectile:SKSpriteNode, monster:SKSpriteNode) {
  println("Hit")
  projectile.removeFromParent()
  monster.removeFromParent()
}

所有你要做的就是当子弹和怪物发生冲突时从场景中取出他们。很简单吧!

现在是时候来实现接触的委托方法。增加以下新的方法到文件中:

func didBeginContact(contact: SKPhysicsContact) {
 
  // 1
  var firstBody: SKPhysicsBody
  var secondBody: SKPhysicsBody
  if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
    firstBody = contact.bodyA
    secondBody = contact.bodyB
  } else {
    firstBody = contact.bodyB
    secondBody = contact.bodyA
  }
 
  // 2
  if ((firstBody.categoryBitMask & PhysicsCategory.Monster != 0) &&
      (secondBody.categoryBitMask & PhysicsCategory.Projectile != 0)) {
    projectileDidCollideWithMonster(firstBody.node as SKSpriteNode, monster: secondBody.node as SKSpriteNode)
  }
 
}

因为你之前设置场景的物理代理为contactDelegate,当两个物理碰撞这个方法会被调用(并且contactTestBitMasks已经被适当配置)。

收尾

你现在快要有一个可执行的(但非常简单)的游戏。你只需要添加一些音效和音乐(什么样的游戏会没有声音?)和一些简单的游戏逻辑。

Sprite Kit不像Cocos2D一样附带音频引擎,不过好在它配备了一个简单的方法,通过action来播放音效,并且可以很方便地通过AVFoundation播放背景音乐。

我为本教程的项目做了一些很酷的背景音乐,你可以从资源中添加你想要的音乐到你的项目中。你只需要使用它们!

在GameScene.swift的顶部增加以下代码:

import AVFoundation
 
var backgroundMusicPlayer: AVAudioPlayer!
 
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()
}

这是一些AVFoundation代码循环播放一些音乐。

要试一下的话,只需在didMoveToView(_:):顶部添加以下代码

playBackgroundMusic("background-music-aac.caf")

至于声音效果,将以下行添加到touchesEnded(_:withEvent:):

runAction(SKAction.playSoundFileNamed("pew-pew-lei.caf", waitForCompletion: false))

构建、运行,开始享受你的音乐吧。

收尾

游戏结束了,伙计!

现在,让我们创建一个新的场景,将告诉您游戏结果“你赢了”或者“你输了”。从iOS\source\Swift File Template创建新文件,将文件命名为GameOverScene,然后单击创建。 

然后用下面的代码替换GameOverScene.swift:

import Foundation
import SpriteKit
 
class GameOverScene: SKScene {
 
  init(size: CGSize, won:Bool) {
 
    super.init(size: size)
 
    // 1
    backgroundColor = SKColor.whiteColor()
 
    // 2
    var message = won ? "You Won!" : "You Lose :["
 
    // 3
    let label = SKLabelNode(fontNamed: "Chalkduster")
    label.text = message
    label.fontSize = 40
    label.fontColor = SKColor.blackColor()
    label.position = CGPoint(x: size.width/2, y: size.height/2)
    addChild(label)
 
    // 4
    runAction(SKAction.sequence([
      SKAction.waitForDuration(3.0),
      SKAction.runBlock() {
        // 5
        let reveal = SKTransition.flipHorizontalWithDuration(0.5)
        let scene = GameScene(size: size)
        self.view?.presentScene(scene, transition:reveal)
      }
    ]))
 
  }
 
  // 6
  required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

有五个部分需要说明:

  1. 设置背景颜色为白色,像你的主场景一样。 

  2. 设置won参数为“你赢了”或“你输了”的。 

  3. 这是Sprite Kit显示文本标签的方式。正如你所看到的,这是很容易的 - 你只要选择你的字体,并设置一些参数。 

  4. 最后,建立并运行两个顺序动作。我已经将所有这些内嵌了(而不必为每个动作单独配置变量)。首先,它会等待3秒,然后使用runBlock动作来运行一些任意代码。 

  5. 你将使用这个方法在Sprite Kit中过渡到新场景。首先,你可以为场景显示选择不同的动画过渡效果-你选择了一个需要0.5秒的翻转过渡效果。然后创建要显示的场景,并对self.view属性使用presentScene(_:transition:)方法。 

  6. 如果需要覆写场景的初始化方法,你必须实现必要的init(coder:)方法。然而,这初始化将永远不会被调用,所以你只需要在fatalError(_:)中添加一个虚拟实现。

到目前为止,现在你只需要设置您的主要场景在适当的时候加载游戏结束的场景。 

切换回GameScene.swift在addMonster()方法中替换怪物actions的最后一行:

let loseAction = SKAction.runBlock() {
  let reveal = SKTransition.flipHorizontalWithDuration(0.5)
  let gameOverScene = GameOverScene(size: self.size, won: false)
  self.view?.presentScene(gameOverScene, transition: reveal)
}
monster.runAction(SKAction.sequence([actionMove, loseAction, actionMoveDone]))

这将创建一个新的“失败动作”当怪物超过一幕时显示游戏结束。如果你了解这里的每一行可以仔细看看代码,如果不是很了解可以参考前面的代码块。 

现在,你应该处理的胜利的情况。在GameScene的顶端player声明之后添加一个新的属性:

var monstersDestroyed = 0

在projectile(_:didCollideWithMonster:):方法底部增加以下代码:

monstersDestroyed++
if (monstersDestroyed > 30) {
  let reveal = SKTransition.flipHorizontalWithDuration(0.5)
  let gameOverScene = GameOverScene(size: self.size, won: true)
  self.view?.presentScene(gameOverScene, transition: reveal)
}

开始构建和运行,您现在应该已经具备赢与输的条件,并可以在适当的时候看到游戏结束的场景!

008_You_Won

本文链接:http://www.4byte.cn/learning/120056/sprite-kit-swift-chu-xue-zhe-jiao-cheng.html