本文作者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”牛刀小试一下吧!
用Sprite Kit为游戏添加图片(比如space monkey)有三步:
一步一步来,打开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() } }
再回顾一下上述步骤:
编写完成后运行,就能看见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) }
再回顾一下上述步骤:
剩下的就是多次调用这个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))))
此处稍作解释:
编写完成后运行,结果应该是敌人移动到了屏幕另一端,然后完全消失。如下所示:
游戏的设定是:不点击屏幕时,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) }
再回顾一下上述步骤:
代码在被调用之前,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飞出或者坠落在太空中。现在回顾一下上述代码:
编写完成后运行,就能看到如下场景:
一只蹦蹦跳跳的小monkey出现啦!
到目前为止,如果monkey遇到敌人,可以跳过去;但是跟敌人相撞的话,什么效果都没有,所以需要在游戏中添加碰撞检测(collision detection),有如下几步:
还记得怎么给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
此处稍稍解释一下:
将如下所示添加到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和敌人相撞,如果是,就将两者从屏幕上移除。
编写完成后运行,效果如下:
如果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)再回顾一下上述步骤:
下一步创建名为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) }
再回顾一下上述步骤:
现在在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) } }
回顾一下:
接下来在代码中添加如下方法:
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) } } } }
回顾一下:
现在添加名为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)" } }
再回顾一下上述步骤:
现在需要稍稍改变下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) }
现在来回顾一下:
现在用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")编写好之后运行,美妙的背景音乐出现啦!太棒了!