本文作者Ajay Venkat是一名年僅13歲的iOS開發(fā)者,他非常喜歡用蘋果的Sprite Kit 2D游戲框架來開發(fā)iOS游戲,在了解到很多同齡孩子也對(duì)學(xué)習(xí)如何使用Sprite Kit來開發(fā)iOS游戲非常感興趣之后,他以自己用Swift語言所開發(fā)的一款名為“Space Monkey”的游戲?yàn)槔赗ay Wenderlich上寫下了這篇指南,以下為譯文:
首先要安裝蘋果的iOS和Mac應(yīng)用免費(fèi)開發(fā)工具——Xcode。如果還沒有安裝Xcode,可以從App Store上下載;如果已經(jīng)安裝好,須確保自己使用的是最新版本。安裝好Xcode后,下載并解壓starter project,雙擊SpaceMonkey.xcodeproj,文件就會(huì)在Xcode中打開。單擊“Play”,接著你會(huì)看到一整片黑屏:
starter project給了一個(gè)好的起步,接下來就是著手開發(fā)自己的游戲了。我已經(jīng)在project中添加了art(藝術(shù)效果)和sounds(音效),放在文件夾Sounds 和 sprites.atlas里。art能派上大用場(chǎng),不妨用“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() } }
再回顧一下上述步驟:
編寫完成后運(yùn)行,就能看見monkey飛翔在太空之中了:
接下來添加monkey的敵人!
monkey只有一個(gè),而敵人有很多個(gè),而且可能出現(xiàn)在屏幕的不同位置。
首先需要一些method(method)創(chuàng)建隨機(jī)數(shù)字。將這些新method添加至GameScene.swift,放在 didMoveToView(_:)后(大括號(hào)之前):
func random() -> CGFloat { return CGFloat(Float(arc4random()) / 0xFFFFFFFF) } func random(#min: CGFloat, max: CGFloat) -> CGFloat { return random() * (max - min) + min }
random()返回一個(gè)介于0到1之間的十進(jìn)制值。random(min:max:)返回一個(gè)固定范圍的隨機(jī)值。指南不詳述這些步驟的原理,讀者只需用好這些method就夠了。
接下來,將這個(gè)新的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) }
再回顧一下上述步驟:
剩下的就是多次調(diào)用這個(gè)method了!調(diào)用前先創(chuàng)建actions的序列,使敵人以固定時(shí)間間隔出現(xiàn)在屏幕上。將這一行添加到didMoveToView(_:)末尾:
runAction(SKAction.repeatActionForever( SKAction.sequence([ SKAction.runBlock(spawnEnemy), SKAction.waitForDuration(1.0)])))這里調(diào)用spawnEnemy(),用不斷重復(fù)的序列創(chuàng)建一個(gè)action,等待時(shí)長(zhǎng)為1秒,即敵人出現(xiàn)的間隔。
這時(shí)代碼看起來如下所示:
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) } }
編寫完成后運(yùn)行,敵人就出現(xiàn)在屏幕右邊的隨機(jī)位置上了:
先對(duì)“敵人”的代碼進(jìn)行微調(diào),使它們?cè)诔霈F(xiàn)之前,完全隱藏在屏幕后(而非僅僅隱藏一半)。從天而降的設(shè)定為游戲增加了難度,也增添了樂趣。
那么首先要做的就是更新spawnEnemy()那一行代碼,敵人sprite的position編寫如下:
enemy.position = CGPoint(x: frame.size.width + enemy.size.width/2, y: frame.size.height * random(min: 0, max: 1))
現(xiàn)在,用更多action讓敵人從屏幕一端移動(dòng)到另一端,游戲變得更有趣了。
將這一行代碼添加到spawnEnemy()末尾:
enemy.runAction( SKAction.moveByX(-size.width - enemy.size.width, y: 0.0, duration: NSTimeInterval(random(min: 1, max: 2))))
此處稍作解釋:
編寫完成后運(yùn)行,結(jié)果應(yīng)該是敵人移動(dòng)到了屏幕另一端,然后完全消失。如下所示:
游戲的設(shè)定是:不點(diǎn)擊屏幕時(shí),monkey會(huì)落下來;點(diǎn)擊時(shí),monkey會(huì)跳起。
這里用SKAction移動(dòng)monkey,就像之前移動(dòng)敵人一樣。推薦用Sprite Kit內(nèi)置的物理引擎,更加簡(jiǎn)單。
好,現(xiàn)在來試試看。還是GameScene.swift,在didMoveToView(_:)后添加如下代碼:
player.physicsBody = SKPhysicsBody(circleOfRadius:player.frame.size.width * 0.3) player.physicsBody?.allowsRotation = false
第一行為monkey創(chuàng)建了一個(gè)physics body,在物理引擎的作用下,monkey因引力和其他外力而落下”。
注意:physics body(物理實(shí)體)的形狀是圓的,僅跟monkey的形狀近似而已。無需做到精確,只要湊效就好。同時(shí)將physics body設(shè)定為不旋轉(zhuǎn)。
編寫完成后運(yùn)行,就能看到monkey在屏幕上時(shí)而落下,時(shí)而消失,很酷吧?
為了避免monkey“落下”,需要用物理推力讓它重新跳起來。
這時(shí)要在spawnEnemy()后添加一個(gè)新的method:
func jumpPlayer() { // 1 let impulse = CGVector(dx: 0, dy: 75) // 2 player.physicsBody?.applyImpulse(impulse) }
再回顧一下上述步驟:
代碼在被調(diào)用之前,monkey是不能跳起來的;要使monkey跳起來,就要重寫點(diǎn)擊屏幕時(shí)調(diào)用的那個(gè)method。在jumpPlayer()底下復(fù)制這些代碼:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) { jumpPlayer() }
點(diǎn)擊屏幕時(shí),自動(dòng)調(diào)用這些method。
現(xiàn)在就差一步了——在 didMoveToView(_:)后添加如下代碼:
// 1 let collisionFrame = CGRectInset(frame, 0, -self.size.height * 0.2) // 2 physicsBody = SKPhysicsBody(edgeLoopFromRect: collisionFrame)
代碼會(huì)在屏幕邊緣產(chǎn)生一個(gè)特殊的physics body,避免monkey飛出或者墜落在太空中?,F(xiàn)在回顧一下上述代碼:
編寫完成后運(yùn)行,就能看到如下場(chǎng)景:
一只蹦蹦跳跳的小monkey出現(xiàn)啦!
到目前為止,如果monkey遇到敵人,可以跳過去;但是跟敵人相撞的話,什么效果都沒有,所以需要在游戲中添加碰撞檢測(cè)(collision detection),有如下幾步:
還記得怎么給monkey添加physics body嗎?現(xiàn)在輪到為敵人的sprite添加physics body了,來制造碰撞效果。
首先將如下所示添加至GameScene.swift最頂端:
enum BodyType: UInt32 { case player = 1 case enemy = 2 case ground = 4 }
這里要做的就是為每個(gè)sprite創(chuàng)建類別。ground number不是針對(duì)sprite,而是針對(duì)應(yīng)用邊框設(shè)定的,所以當(dāng)monkey碰到屏幕邊緣時(shí)會(huì)彈起,而不是落到屏幕之外!
接下來,執(zhí)行SKPhysicsContactDelegate協(xié)定,標(biāo)記GameScene(游戲場(chǎng)景):
class GameScene: SKScene, SKPhysicsContactDelegate {協(xié)議的作用是保證代碼執(zhí)行特定的method。此處執(zhí)行針對(duì)兩個(gè)physics body相撞的method。然后調(diào)整contactDelegate的值,將如下代碼添加到didMoveToView(_:)末尾:
physicsWorld.contactDelegate = self
完成后,兩個(gè)physics body碰撞時(shí),物理世界就會(huì)自動(dòng)調(diào)用代碼中的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設(shè)置類別和碰撞位掩碼,讓兩者彼此碰撞;在monkey和敵人之間設(shè)置“contact(接觸點(diǎn))”。
現(xiàn)在到了最重要的一步,完善碰撞檢測(cè),執(zhí)行之前提到的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 } }
因?yàn)橹耙褜?chǎng)景設(shè)置為物理世界的contactDelegate,兩個(gè)physics body碰撞時(shí)會(huì)自動(dòng)調(diào)用這個(gè)method。
它將兩個(gè)位掩碼結(jié)合成一個(gè)單個(gè)的接觸點(diǎn)掩碼,檢驗(yàn)是否是monkey和敵人相撞,如果是,就將兩者從屏幕上移除。
編寫完成后運(yùn)行,效果如下:
如果monkey跟敵人相撞,或落出屏幕的話,就會(huì)顯示”Game Over“,接著出現(xiàn)重新點(diǎn)擊開始游戲的畫面。
首先將以下所有變量添加到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)再回顧一下上述步驟:
下一步創(chuàng)建名為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) }
再回顧一下上述步驟:
現(xiàn)在在didMoveToView(_:)里調(diào)用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) } } } }
回顧一下:
現(xiàn)在添加名為updateEnemy()的method,在框架渲染前調(diào)用——產(chǎn)生的效果是:每當(dāng)一個(gè)敵人消失,玩家就會(huì)得1分:
func updateEnemy(enemy: SKNode) { //1 if enemy.position.x < 0 { //2 enemy.removeFromParent() //3 runAction(coinSound) //4 numPoints++ //5 points.text = "\(numPoints)" } }
再回顧一下上述步驟:
現(xiàn)在需要稍稍改變下didBeginContact(_:)了。從parent移除第一個(gè)節(jié)點(diǎn)之后,添加這行代碼:
endGame()現(xiàn)在終于可以用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) }
現(xiàn)在來回顧一下:
現(xiàn)在用didMoveToView(_:)移除這個(gè)代碼塊:
backgroundColor = SKColor.blackColor() runAction(SKAction.repeatActionForever( SKAction.sequence([ SKAction.runBlock(spawnEnemy), SKAction.waitForDuration(1.0)])))最后添加一行:
player.physicsBody?.dynamic = false
這樣設(shè)置之后,點(diǎn)擊屏幕后游戲才會(huì)開始,否則monkey是不會(huì)移動(dòng)的。
編寫完成后運(yùn)行,游戲大功告成了!
等會(huì)兒,還有一件事!打開ViewController.swift,添加一個(gè)新屬性:
var backgroundMusicPlayer: AVAudioPlayer!還要添加一個(gè)新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() }
這個(gè)method很方便,用來添加一些背景音樂。工作原理在此不詳述。
用起來很簡(jiǎn)單,只要在skView.presentScene(scene)那一行后添加viewWillLayoutSubviews():
playBackgroundMusic("BackgroundMusic.mp3")編寫好之后運(yùn)行,美妙的背景音樂出現(xiàn)啦!太棒了!