[Swift]SpriteKit實現相似像素鳥的小遊戲 - Crashy Plane

像素鳥曾經很是火爆,遊戲簡單,頗有趣味性,仿寫一個叫 crashy plane 的遊戲,它的原理跟像素鳥是同樣的,接下來用 SpriteKit 來實現它node

同時推薦一個不錯的學習 Swift 的網站,這個 Crashy Plane 就是從那裏偷來的git

hackingwithswiftgithub

demo 地址swift

目錄:

開始:

0.建立項目

  • a.建立項目 選擇 Game bash

  • b.找到 GameScene.sks,將 helloWorld 的 label刪除掉,而後將寬高調整爲 W:375 H:667, 錨點設置爲 X:0 Y:0,重力設置爲X:0 Y:-5 app

  • c.打開GameScene.swift 刪除多餘的代碼,只留下 override func didMove(to view: SKView) ,override func update(_ currentTime: TimeInterval)override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)dom

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.生成動態效果

飛機只是一張圖片,後面的山也沒用動起來,彆着急,接下來讓全部的節點都動起來ide

  • 飛機
    • 素材庫一共三張飛機的圖片,每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 了

相關文章
相關標籤/搜索