[Swift]SpriteKit實現類似畫素鳥的小遊戲 - Crashy Plane
畫素鳥曾經非常火爆,遊戲簡單,很有趣味性,仿寫一個叫 crashy plane 的遊戲,它的原理跟畫素鳥是一樣的,接下來用 SpriteKit 來實現它
同時推薦一個不錯的學習 Swift 的網站,這個 Crashy Plane 就是從那裡偷來的
目錄:
開始:
0.建立專案
-
a.建立專案 選擇 Game
-
b.找到 GameScene.sks,將 helloWorld 的 label刪除掉,然後將寬高調整為 W:375 H:667, 錨點設定為 X:0 Y:0,重力設定為X:0 Y:-5
-
c.開啟GameScene.swift 刪除多餘的程式碼,只留下
override func didMove(to view: SKView)
,override func update(_ currentTime: TimeInterval)
和override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
1.生成 ui
- 飛機
func createPlayer() { let playerTexture = SKTexture(imageNamed: R.image.player1.name) player = SKSpriteNode(texture: playerTexture) player.position = CGPoint(x: frame.width / 6, y: frame.height * 0.75) player.zPosition = 10 addChild(player) } 複製程式碼
- 天空
func createSky() { let topSky = SKSpriteNode(color: UIColor(hue: 0.55, saturation: 0.14, brightness: 0.97, alpha: 1), size: CGSize(width: frame.width , height: 0.67 * frame.height)) topSky.anchorPoint = CGPoint(x: 0.5, y: 1) topSky.zPosition = -40 topSky.position = CGPoint(x: frame.midX, y: frame.maxY) let bottomSky = SKSpriteNode(color: UIColor(hue: 0.55, saturation: 0.16, brightness: 0.96, alpha: 1), size: CGSize(width: frame.width , height: 0.33 * frame.height)) bottomSky.anchorPoint = CGPoint(x: 0.5, y: 1) bottomSky.zPosition = -40 bottomSky.position = CGPoint(x: frame.midX, y: 0.33 * frame.height) addChild(topSky) addChild(bottomSky) } 複製程式碼
- 背景
- 生成兩個首位相接的背景圖,方便後期無限運動的效果
func createBackground() { let backgroundTexture = SKTexture(imageNamed: R.image.background.name) for i in 0 ... 1 { let background = SKSpriteNode(texture: backgroundTexture) background.zPosition = -30 background.anchorPoint = .zero background.position = CGPoint(x: CGFloat(i) * backgroundTexture.size().width, y: 100) addChild(background) } } 複製程式碼
- 地面
- 同背景,也是生成兩個首尾相接的地面
func createGround() { let groundTexture = SKTexture(imageNamed: R.image.ground.name) for i in 0...1{ let ground = SKSpriteNode(texture: groundTexture) ground.zPosition = -10 ground.position = CGPoint(x: (CGFloat(i) + 0.5) * groundTexture.size().width, y: groundTexture.size().height/2) addChild(ground) } } 複製程式碼
- 上下的兩個石頭(類似畫素鳥的兩個管道)
- 上下兩個石頭是一樣的 texture,只是上面的石頭是旋轉後再鏡面翻轉
- 兩個石頭起始位置在螢幕右側之外
- 兩個石頭的距離固定,位置隨機
- 兩個石頭後面新增一個得分檢測節點(SKNode),用於判斷飛機飛過石頭得分的
func createRocks() { let rockTexture = SKTexture(imageNamed: R.image.rock.name) let topRock = SKSpriteNode(texture: rockTexture) topRock.zRotation = .pi topRock.xScale = -1 topRock.zPosition = -20 let bottomRock = SKSpriteNode(texture: rockTexture) bottomRock.zPosition = -20 let rockCollision = SKSpriteNode(color: .red, size: CGSize(width: 32, height: frame.height)) rockCollision.name = scoreDetect addChild(topRock) addChild(bottomRock) addChild(rockCollision) let xPosition = frame.width + topRock.frame.width let max = CGFloat(frame.width/3) let yPosition = CGFloat.random(in: -50...max) let rockDistance: CGFloat = 70 topRock.position = CGPoint(x: xPosition, y: yPosition + topRock.size.height + rockDistance) bottomRock.position = CGPoint(x: xPosition, y: yPosition - rockDistance) rockCollision.position = CGPoint(x: xPosition + rockCollision.size.width * 2, y: frame.midY) } 複製程式碼
- 得分 Lable
func createScore() { scoreLabel = SKLabelNode(fontNamed: "Optima-ExtraBlack") scoreLabel.fontSize = 24 scoreLabel.position = CGPoint(x: frame.midX, y: frame.maxY - 60) scoreLabel.text = "SCORE: 0" scoreLabel.fontColor = UIColor.black addChild(scoreLabel) } 複製程式碼
- 效果圖:

2.生成動態效果
飛機只是一張圖片,後面的山也沒用動起來,彆著急,接下來讓所有的節點都動起來
- 飛機
- 素材庫一共三張飛機的圖片,每0.1秒改變一張圖片,然後一直迴圈
createPlayer方法新增程式碼
let frame2 = SKTexture(imageNamed: R.image.player2.name) let frame3 = SKTexture(imageNamed: R.image.player3.name) let animation = SKAction.animate(with: [playerTexture,frame2,frame3,frame2], timePerFrame: 0.1) let forever = SKAction.repeatForever(animation) player.run(forever) 複製程式碼
- 背景
- 每個 background 在20秒內移動一個 texture 寬度的距離
- 在0秒內把每個 background 復位
- 上面兩個動作重複 createBackground方法每個background 新增 如下action
let move = SKAction.moveBy(x: -backgroundTexture.size().width, y: 0, duration: 20) let reset = SKAction.moveBy(x: backgroundTexture.size().width, y: 0, duration: 0) let sequence = SKAction.sequence([move,reset]) let forever = SKAction.repeatForever(sequence) background.run(forever) 複製程式碼
- 地面
- 同背景的步驟一樣
let move = SKAction.moveBy(x: -groundTexture.size().width, y: 0, duration: 5) let reset = SKAction.moveBy(x: groundTexture.size().width, y: 0, duration: 0) let sequence = SKAction.sequence([move,reset]) let forever = SKAction.repeatForever(sequence) ground.run(forever) 複製程式碼
- 石頭
- 6.2秒內從螢幕右側移動到螢幕左側
- 移除
- 新增一個新方法迴圈新增新的石頭
let endPosition = frame.width + topRock.size.width * 2 let moveAction = SKAction.moveBy(x: -endPosition, y: 0, duration: 6.2) let sequence = SKAction.sequence([moveAction,SKAction.removeFromParent()]) topRock.run(sequence) bottomRock.run(sequence) rockCollision.run(sequence) 複製程式碼
func startRocks() { let createRocksAction = SKAction.run { [unowned self] in self.createRocks() } let sequence = SKAction.sequence([createRocksAction,SKAction.wait(forDuration: 3)]) let repeatAction = SKAction.repeatForever(sequence) run(repeatAction) } 複製程式碼
效果:

3.新增 physicsBody
畫面已經動起來了,接下來我希望我的飛機可以自由落體,然後可以和石頭地面發生碰撞
///給飛機新增 physicsBody player.physicsBody = SKPhysicsBody(texture: playerTexture, size: playerTexture.size()) player.physicsBody?.contactTestBitMask = player.physicsBody!.collisionBitMask player.physicsBody?.isDynamic = true ///給地面新增 physicsBody ground.physicsBody = SKPhysicsBody(texture: groundTexture, size: groundTexture.size()) ground.physicsBody?.isDynamic = false ///給石頭新增 physicsBody ///上面的石頭要在旋轉之前新增 physicsBody要讓 physicsBody 跟著圖形一起翻轉過去 topRock.physicsBody = SKPhysicsBody(texture: rockTexture, size: rockTexture.size()) topRock.physicsBody?.isDynamic = false bottomRock.physicsBody = SKPhysicsBody(texture: rockTexture, size: rockTexture.size()) bottomRock.physicsBody?.isDynamic = false rockCollision.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 32, height: frame.height)) rockCollision.physicsBody?.isDynamic = false 複製程式碼
4.新增互動
飛機已經可以自由落體了,接下來實現點選螢幕時,飛機向上飛起,當碰撞到紅色的區域時,獲得得分.
在 didMove(to view: SKView) 方法中新增如下程式碼
physicsWorld.contactDelegate = self 複製程式碼
GameScene 新增 score 屬性
var score = 0 { didSet { scoreLabel.text = "SCORE: \(score)" } } 複製程式碼
實現SKPhysicsContactDelegate協議的didBegin(_ contact: SKPhysicsContact) 方法,判斷飛機碰撞到得分判定區來加分,實際上這段應該放到碰撞裡面講,放到這裡是為了更好的看飛機互動的效果
extension GameScene: SKPhysicsContactDelegate { func didBegin(_ contact: SKPhysicsContact) { guard let nodeA = contact.bodyA.node,let nodeB = contact.bodyB.node else { return } if nodeA.name == scoreDetect || nodeB.name == scoreDetect { if nodeA == player { nodeB.removeFromParent() }else if nodeB == player { nodeA.removeFromParent() } score += 1 return } } } 複製程式碼
每次點選螢幕時 飛機施加向上衝力
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { player.physicsBody?.velocity = CGVector.zero player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 20)) } 複製程式碼
為了飛機上升和下降的過程更真實,我們根據飛機 Y 方向的速度來調整飛機頭的朝向
override func update(_ currentTime: TimeInterval) { guard player != nil else { return } let rotate = SKAction.rotate(toAngle: player.physicsBody!.velocity.dy * 0.001, duration: 1) player.run(rotate) } 複製程式碼
5.碰撞判斷
飛機的互動已經完成,接下來實現各種物體的碰撞判斷(得分判斷在4.新增互動已經實現)
在didBegin(_ contact: SKPhysicsContact)方法中 已經判斷的碰撞到得分點的情況,那麼其他的情況就是碰到了地面或者上下的兩個石頭,新增如下程式碼,當碰到地面或者石頭時,銷燬飛機,並新增飛機位置新增爆炸特效,同時將 scene 的 speed 設定為0,畫面就會停下了
guard let explosion = SKEmitterNode(fileNamed: R.file.playerExplosionSks.name) else {return} explosion.position = player.position addChild(explosion) player.removeFromParent() speed = 0 複製程式碼
效果:

6.新增音效
得分時爆炸時有音效,同時遊戲還要有背景音.
GameScene 新增一個 audio 節點 var backgroundMusic: SKAudioNode!
在 didMove 方法中 新增如下程式碼
if let url = R.file.musicM4a() { backgroundMusic = SKAudioNode(url: url) addChild(backgroundMusic) } 複製程式碼
爆炸和得分的音效程式碼加到相應的位置
///得分 let sound = SKAction.playSoundFileNamed(R.file.coinWav.name, waitForCompletion: false) run(sound) ///爆炸 let sound = SKAction.playSoundFileNamed(R.file.explosionWav.name, waitForCompletion: false) run(sound) 複製程式碼
7.完善遊戲週期
現在遊戲已經可以玩了,但是死亡後,沒有辦法重新開始,接下來,我們為遊戲新增 logo 和 game over 和重新開始遊戲的操作
宣告一個 GameState 的列舉
enum GameState { case showingLogo case playing case dead } 複製程式碼
GameScene 新增一個 gameState 的屬性,預設值為showingLogo
var gameState = GameState.showingLogo
新增 logo 和 gameOver 的節點屬性
var logo: SKSpriteNode! var gameOver: SKSpriteNode! 複製程式碼
生成 logo和 gameOver
func createLogo() { logo = SKSpriteNode(imageNamed: R.image.logo.name) logo.position = CGPoint(x: frame.midX, y: frame.midY) addChild(logo) gameOver = SKSpriteNode(imageNamed: R.image.gameover.name) gameOver.position = CGPoint(x: frame.midX, y: frame.midY) gameOver.alpha = 0 addChild(gameOver) } 複製程式碼
修改 touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
的邏輯
當遊戲處於 showingLogo 狀態點選螢幕執行隱藏 logo,恢復飛機的isDynamic屬性,開始生成石頭,將 gameState 改為 playing
處於playing狀態時給飛機增加向上衝力
處於死亡狀態時 重新生成 GameScene ,這樣比把所有的節點恢復到初始狀態要簡單的多,重新生成 Scene
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { switch gameState { case .showingLogo: gameState = .playing let fadeOut = SKAction.fadeOut(withDuration: 0.5) let wait = SKAction.wait(forDuration: 0.5) let activePlayer = SKAction.run { [weak self] in self?.player.physicsBody?.isDynamic = true self?.startRocks() } let sequence = SKAction.sequence([fadeOut,wait,activePlayer,SKAction.removeFromParent()]) logo.run(sequence) case .playing: player.physicsBody?.velocity = CGVector.zero player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 20)) case .dead: let scene = GameScene(fileNamed: R.file.gameSceneSks.name)! let transition = SKTransition.moveIn(with: .left, duration: 1) self.view?.presentScene(scene, transition: transition) } } 複製程式碼
碰撞到地面和石頭時 顯示 gameOver,同時gameState改為 dead
gameOver.alpha = 1 gameState = .dead 複製程式碼
最後,別忘更改了一些細節
createPlayer方法中player.physicsBody.isDynamic要改為 false player.physicsBody?.isDynamic = false
didMove 方法中移出 startRocks()
的呼叫,因為生成石頭是在遊戲開始後
createRock
方法中,得分判斷區的顏色要改為透明的
let rockCollision = SKSpriteNode(color: .clear, size: CGSize(width: 32, height: frame.height)) 複製程式碼
回到 GameViewController
中
把這3項設為 false view.showsFPS = false //是否顯示 FPS view.showsNodeCount = false//是否顯示節點數量 view.showsPhysics = false /// 是否顯示物理區域() 複製程式碼
這樣 一個簡單的遊戲就完成了,接下來就可以 enjoy your game 了