[SceneKit專題]26-如何製做一個像Stack的遊戲

說明

SceneKit系列文章目錄node

更多iOS相關知識查看github上WeekWeekUpProjectgit

在本教程中,你將會學習如何製做一個相似Stack這樣的遊戲. github

392x696bb.jpg

本教程將包含如下內容:編程

  • 可視化建立3D場景.
  • 編程加載並呈現3D場景.
  • 使用節點的物理形體.
  • 結合使用UIKit與SceneKit.
  • 在SceneKit遊戲中播放音頻.

開始

下載初始項目starter project. 在初始項目裏面,你會發現SceneKit目錄文件中帶有音頻和場景文件.另外,還有一些SCNVector3類擴展來執行簡單的向量算術運算及生成漸變圖片.還有App Icon也已經添加進去了!花點時間熟悉一下項目吧.swift

你將會建立一個相似於Stack的遊戲.這個遊戲的目標是在一塊方塊上疊放另外一個方塊.須要當心的是:方塊疊放時稍微偏一點,多餘部分就會被切掉.徹底沒對齊,那就game over了!app

創建場景

你將從創建你的遊戲場景開始.打開GameScene.scn. 編輯器

game_scene-1.png
拖拽一個新的 camera到場景中,而後選擇 Node Inspector並重命名節點爲 Main Camera.設置位置爲 X: 3, Y: 4.5, Z: 3,旋轉爲 X: -40, Y: 45, Z:0:
camera_node_inspector.png

切換到Attributes Inspector並切換相機的Projection typeOrthographic. 下一步,添加燈光到場景中. 從對象庫中拖拽一個新的方向光到場景中,命名爲Directional Light.由於相機只看到了場景的一側,你沒必要去照亮看不見的令一側.回到Attributes Inspector,設置位置爲X: 0, Y: 0, Z: 0,旋轉爲X: -65, Y: 20, Z:-30: post

directional_light.png

神奇,亮起來了!學習

如今回到塔的頂部.你須要一個基礎方塊來支承這個塔,來讓玩家在上面建造.拖拽一個盒子到場景中,設置屬性:字體

  • 在Node Inspector中,更更名字爲Base Block,並設置位置爲X:0,Y:-4,Z:0.
  • 在Attributes Inspector中,更改尺寸爲Width: 1, Height: 8, Length: 1.
  • 在Material Inspector中,更改漫反射顏色爲 #434343.
    base_block_diffuse_color-e1486870178228.png

你須要添加一個動態形體到基礎方塊,切換到Physics Inspector中,並將物理形體改成Static.

base_block_physics.png

如今讓咱們配上漂亮的背景顏色!在選中基礎方塊的同時,切換到Scene Inspector,並拖拽文件Gradient.png到背景選擇框中:

scene_background-650x307.png

你須要一個方法來顯示給玩家,他們的塔已經堆放了多高.打開Main.storyboard;看到它已經有一個SCNView.添加一個label在SCNView頂部並設置文本爲0.而後添加一個約束將label對齊到中心,像這樣:

center_constraint.png

添加另外一個約束將label頂部與屏幕頂部對齊.

top_constraint.png

而後切換到Attributes Inspector中,切換字體爲Custom, Thonburi, Regular, 50.

label_text_settings.png

而後使用assistant editor來添加一個從label到控制器的引用,命名爲scoreLabel:

score_label.gif

編譯運行,看看如今有什麼了.

build_and_run_1-1.png

添加你的第一塊方塊

知道怎麼讓塔愈來愈高麼?對,建立更多方塊. 建立一些屬性來幫你追蹤正在使用的方塊.爲此,打開ViewController.swift() 並在viewDidLoad() 以前添加下面變量:

//1
var direction = true
var height = 0

//2
var previousSize = SCNVector3(1, 0.2, 1)
var previousPosition = SCNVector3(0, 0.1, 0)
var currentSize = SCNVector3(1, 0.2, 1)
var currentPosition = SCNVector3Zero

//3
var offset = SCNVector3Zero
var absoluteOffset = SCNVector3Zero
var newSize = SCNVector3Zero

//4
var perfectMatches = 0
複製代碼

這段代碼含義:

  1. direction用來表示方塊的位置是上升仍是降低,height變量表示塔有多高.
  2. previousSizepreviousPosition變量表示當前層的尺寸和位置.
  3. 你須要使用offset,absoluteOffset,newSize變量來計算新層的尺寸.
  4. perfectMatches變量表示玩家完美對齊上一層的次數.

如今,是時間添加方塊到場景中了.在viewDidLoad() 底部添加下面代碼:

//1
let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
blockNode.position.z = -1.25
blockNode.position.y = 0.1
blockNode.name = "Block\(height)"
    
//2
blockNode.geometry?.firstMaterial?.diffuse.contents =
      UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(blockNode)
複製代碼

代碼含義:

  1. 你用SCNNode的立方體建立一個新的方塊,放置在Z軸和Y軸,並根據放置在塔上的height屬性對其命名.
  2. 根據不斷增加的高度,計算得出漫反射顏色的紅色份量.最後,將節點添加到場景上.

建立並運行,會看到你的新方塊出如今屏幕上!

build_and_run_2.png

移動方塊

如今已經有一條新的方塊用來放置.可是,我想若是方塊是移動的會更好玩. 要實現這個移動,須要設置控制器做爲場景渲染代理,並實現SCNSceneRendererDelegate協議中的方法. 在類的底部添加這個擴展:

extension ViewController: SCNSceneRendererDelegate {
  func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {

  }
}
複製代碼

這裏咱們須要實現SCNSceneRendererDelegate協議,添加renderer(_:updateAtTime:). 在renderer(_:updateAtTime:) 裏面添加下面代碼:

// 1
if let currentNode = scnScene.rootNode.childNode(withName: "Block\(height)", recursively: false) {
      // 2
      if height % 2 == 0 {
        // 3
        if currentNode.position.z >= 1.25 {
          direction = false
        } else if currentNode.position.z <= -1.25 {
          direction = true
        }
        
        // 4
        switch direction {
        case true:
          currentNode.position.z += 0.03
        case false:
          currentNode.position.z -= 0.03
        }
      // 5
      } else {
        if currentNode.position.x >= 1.25 {
          direction = false
        } else if currentNode.position.x <= -1.25 {
          direction = true
        }
       
        switch direction {
        case true:
          currentNode.position.x += 0.03
        case false:
          currentNode.position.x -= 0.03
        }
      }
    }
複製代碼

代碼含義:

  1. 根據名字找到場景中的方塊.
  2. 根據層的位置,沿X軸或Z軸移動方塊.奇數層沿Z軸運動,偶數層沿X軸運動.用求餘操做符(%)來獲得餘數,判斷奇偶.
  3. 若是方塊的位置到了1.25或者-1.25,改變其方向,向另外一方向運動.
  4. 根據方向,沿Z軸先後移動.
  5. 重複相同代碼,只改成沿X軸.

默認狀況下,SceneKit會暫停場景.爲了看到場景中物體的移動,在viewDidLoad的底部添加下面代碼:

scnView.isPlaying = true
scnView.delegate = self
複製代碼

這段代碼中,將這個控制器設置爲場景的渲染代理,這樣就能執行剛纔寫的代理方法了. 建立運行,查看運動!

032.png

處理點擊

如今,咱們已經讓方塊移動了,還須要在玩家點擊屏幕時添加一個新方塊並重設老方塊的尺寸.切換到Main.storyboard並添加一個tap gesture recognizerSCNView,像這樣:

Screen-Shot-2017-04-20-at-5.29.44-PM-650x357.png

如今在控制器裏面用輔助編輯器建立一個動做並命名爲handleTap.

tap_action-650x374.png

切換到標準編輯區,並打開ViewController.swift,而後在handlTap(_:) 內部添加代碼:

if let currentBoxNode = scnScene.rootNode.childNode(
  withName: "Block\(height)", recursively: false) {
      currentPosition = currentBoxNode.presentation.position
      let boundsMin = currentBoxNode.boundingBox.min
      let boundsMax = currentBoxNode.boundingBox.max
      currentSize = boundsMax - boundsMin
      
      offset = previousPosition - currentPosition
      absoluteOffset = offset.absoluteValue()
      newSize = currentSize - absoluteOffset
      
      currentBoxNode.geometry = SCNBox(width: CGFloat(newSize.x), height: 0.2, 
        length: CGFloat(newSize.z), chamferRadius: 0)
      currentBoxNode.position = SCNVector3Make(currentPosition.x + (offset.x/2),
        currentPosition.y, currentPosition.z + (offset.z/2))
      currentBoxNode.physicsBody = SCNPhysicsBody(type: .static, 
        shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
}
複製代碼

這裏咱們從場景中獲得currentBoxNode.而後計算偏移及新方塊的尺寸.從而改變方塊的尺寸和位置,並給它一個靜態物理形體.

resize_current_block.png

偏移等於上一層和當前層位置的差值.經過從當前尺寸上減去差值的絕對值,就獲得了新尺寸. 注意到,把當前節點設置位置到偏移處,方塊的邊緣完美對齊了上一個層的邊緣.這創造出一種切掉方塊的錯覺.

下一步,你須要一個方法來建立塔上的下一個方塊.在handleTap(_:) 下面添加代碼:

func addNewBlock(_ currentBoxNode: SCNNode) {
  let newBoxNode = SCNNode(geometry: currentBoxNode.geometry)
  newBoxNode.position = SCNVector3Make(currentBoxNode.position.x, 
    currentPosition.y + 0.2, currentBoxNode.position.z)
  newBoxNode.name = "Block\(height+1)"
  newBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
    colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
    
  if height % 2 == 0 {
    newBoxNode.position.x = -1.25
  } else {
    newBoxNode.position.z = -1.25
  }
    
  scnScene.rootNode.addChildNode(newBoxNode)
}
複製代碼

這裏咱們建立了一個和上一個方塊相同尺寸的新節點.放置在當前方塊上方,並根據層高改變X或Z軸的位置.最後,改變漫反射顏色並將其添加到場景中.

你須要使用handleTap(_:) 來保持屬性爲最新.在handleTap(_:) 裏的if else語句中添加代碼:

addNewBlock(currentBoxNode)

if height >= 5 {
  let moveUpAction = SCNAction.move(by: SCNVector3Make(0.0, 0.2, 0.0), duration: 0.2)
  let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
  mainCamera.runAction(moveUpAction)
}
      
scoreLabel.text = "\(height+1)"
      
previousSize = SCNVector3Make(newSize.x, 0.2, newSize.z)
previousPosition = currentBoxNode.position
height += 1
複製代碼

要作的第一件事就是調用addNewBlock(_:).若是塔的尺寸大於或等於5,將相機上移.

還須要更新分數,設置前一個尺寸和位置等於當前尺寸和位置.你可使用newSize由於你設置當前節點的尺寸爲newSize.而後增長高度.

建立並運行.一切看起來堆垛地很完美!

build_and_run_4.png

實現物理效果

遊戲正確地重設了方塊尺寸,可是若是被砍掉的部分能從塔上掉落,遊戲會看起來更酷.

addNewBlock(_:) 下面定義新方法:

func addBrokenBlock(_ currentBoxNode: SCNNode) {
    let brokenBoxNode = SCNNode()
    brokenBoxNode.name = "Broken \(height)"
    
    if height % 2 == 0 && absoluteOffset.z > 0 {
      // 1
      brokenBoxNode.geometry = SCNBox(width: CGFloat(currentSize.x), 
        height: 0.2, length: CGFloat(absoluteOffset.z), chamferRadius: 0)
      
      // 2
      if offset.z > 0 {
        brokenBoxNode.position.z = currentBoxNode.position.z - 
          (offset.z/2) - ((currentSize - offset).z/2)
      } else {
        brokenBoxNode.position.z = currentBoxNode.position.z - 
          (offset.z/2) + ((currentSize + offset).z/2)
      }
      brokenBoxNode.position.x = currentBoxNode.position.x
      brokenBoxNode.position.y = currentPosition.y
      
      // 3
      brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
        shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
      brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 * 
        Float(height), green: 0, blue: 1, alpha: 1)
      scnScene.rootNode.addChildNode(brokenBoxNode)

    // 4
    } else if height % 2 != 0 && absoluteOffset.x > 0 {
      brokenBoxNode.geometry = SCNBox(width: CGFloat(absoluteOffset.x), height: 0.2, 
        length: CGFloat(currentSize.z), chamferRadius: 0)
      
      if offset.x > 0 {
        brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) - 
          ((currentSize - offset).x/2)
      } else {
        brokenBoxNode.position.x = currentBoxNode.position.x - (offset.x/2) + 
          ((currentSize + offset).x/2)
      }
      brokenBoxNode.position.y = currentPosition.y
      brokenBoxNode.position.z = currentBoxNode.position.z
      
      brokenBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
        shape: SCNPhysicsShape(geometry: brokenBoxNode.geometry!, options: nil))
      brokenBoxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(
        colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
      scnScene.rootNode.addChildNode(brokenBoxNode)
    }
  }
複製代碼

這裏你建立了一個新節點並用height來命名.你使用了if語句來肯定座標軸,並確保偏移大於0,由於等於時不會產生一個方塊碎片.

  1. 剛纔,你減去偏移量來設置新尺寸.這裏,你無需計算,所需尺寸正是偏移量.
  2. 改變碎片部分的位置.
  3. 添加物理形體到該碎片上來讓它掉落.還須要改變顏色並添加到場景中.
  4. 在X軸上重複一樣操做.

你根據當前位置偏移量的一半來獲得碎片的位置.而後,根據方塊位置的正負,添加或扣除當前尺寸減去偏移的一半.

create_broken_block.png

handleTap(_:) 裏面的addNewBlock(_:) 以前調用該方法:

addBrokenBlock(currentBoxNode)
複製代碼

當碎片節點掉落出視線時,還在不停掉落,並無銷燬.在renderer(_:updateAtTime:) 裏面最上方添加代碼:

for node in scnScene.rootNode.childNodes {
  if node.presentation.position.y <= -20 {
    node.removeFromParentNode()
  }
}
複製代碼

這段代碼會刪除Y值小於-20的全部節點. 運行看看切下的方塊!

build_and_run_5.png

結束觸摸

如今遊戲機制的核心部分已經完成了,還有一些收尾工做.當玩家完美對齊上一層時應該有獎勵.還有,如今尚未輸贏判斷,當你失敗後也沒法開始一個新遊戲! 遊戲尚未聲音,須要添加一些聲音.

處理完美對齊狀況

在處理完美對齊的狀況,在addBrokenBlock(_:) 中添加下面方法:

func checkPerfectMatch(_ currentBoxNode: SCNNode) {
    if height % 2 == 0 && absoluteOffset.z <= 0.03 {
      currentBoxNode.position.z = previousPosition.z
      currentPosition.z = previousPosition.z
      perfectMatches += 1
      if perfectMatches >= 7 && currentSize.z < 1 {
        newSize.z += 0.05
      }
      
      offset = previousPosition - currentPosition
      absoluteOffset = offset.absoluteValue()
      newSize = currentSize - absoluteOffset
    } else if height % 2 != 0 && absoluteOffset.x <= 0.03 {
      currentBoxNode.position.x = previousPosition.x
      currentPosition.x = previousPosition.x
      perfectMatches += 1
      if perfectMatches >= 7 && currentSize.x < 1 {
        newSize.x += 0.05
      }
      
      offset = previousPosition - currentPosition
      absoluteOffset = offset.absoluteValue()
      newSize = currentSize - absoluteOffset
    } else {
      perfectMatches = 0
    }
  }
複製代碼

若是玩家放置位置與上一塊在0.03以內,就認爲是完美匹配.只要偏差足夠近,就設置當前方塊的位置等於上一個方塊的位置.

經過設置當前和上一次位置相等,讓它們在數值上徹底匹配並從新計算偏移和新尺寸.在handleTap(_:) 裏面計算偏移和新尺寸以後,調用這個方法:

checkPerfectMatch(currentBoxNode)
複製代碼

處理徹底錯位狀況

如今已經處理了完美對齊的狀況和部分對齊的狀況,但你還須要處理徹底錯失的狀況.

handleTap(_:)checkPerfectMatch(_:) 以前,添加下面代碼:

if height % 2 == 0 && newSize.z <= 0 {
        height += 1
        currentBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
          shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
        return
      } else if height % 2 != 0 && newSize.x <= 0 {
        height += 1
        currentBoxNode.physicsBody = SCNPhysicsBody(type: .dynamic, 
          shape: SCNPhysicsShape(geometry: currentBoxNode.geometry!, options: nil))
        return
      }
複製代碼

若是玩家錯失了方塊,計算出的新尺寸應該是負的,檢查這個值就知道玩家是否錯失了方塊.若是玩家錯失了,你將高度增長一,這樣移動的代碼就再也不移動移動當前方塊了.而後你再添加一個動態物理形體讓方塊掉落.

最後,return,這樣代碼就再也不運行了,如checkPerfectMatch(_:),和addBrokenBlock(_:).

添加音效

由於音頻文件很短,能夠預先加載進來.在屬性聲明中添加一個新的字典屬性,命名爲sounds:

var sounds = [String: SCNAudioSource]()
複製代碼

下一步,在viewDidLoad下面添加兩個方法:

func loadSound(name: String, path: String) {
  if let sound = SCNAudioSource(fileNamed: path) {
    sound.isPositional = false
    sound.volume = 1
    sound.load()
    sounds[name] = sound
  }
}
  
func playSound(sound: String, node: SCNNode) {
  node.runAction(SCNAction.playAudio(sounds[sound]!, waitForCompletion: false))
}
複製代碼

第一個方法從指定目錄加載音頻文件並儲存到sounds字典中.第二個方法播放儲存在sounds字典中的方法.

viewDidload() 中間添加下面代碼:

loadSound(name: "GameOver", path: "HighRise.scnassets/Audio/GameOver.wav")
loadSound(name: "PerfectFit", path: "HighRise.scnassets/Audio/PerfectFit.wav")
loadSound(name: "SliceBlock", path: "HighRise.scnassets/Audio/SliceBlock.wav")
複製代碼

有好幾個地方須要播放音效.在handleTap(_:) 中,在每個檢查玩家是否錯失方塊的if語句中,添加下面的代碼:

playSound(sound: "GameOver", node: currentBoxNode)
複製代碼

在調用addNewBlock以後,添加一行:

playSound(sound: "SliceBlock", node: currentBoxNode)
複製代碼

滾動到checkPerfectMatch(_:),在兩個if語句中分支中添加一行:

playSound(sound: "PerfectFit", node: currentBoxNode)
複製代碼

建立並運行---有音效的遊戲更有意思了,對吧?

處理輸贏條件

遊戲如何結束呢?如今咱們來處理這個問題!

進入Main.storyboard,拖拽一個新的按鈕到視圖中.改變文本的顏色爲 #FF0000,文本內容Play.而後改變字體爲Custom, Helvetica Neue, 66.

play_button_attribute.png

下一步,設置對齊方式align爲中心對齊center,並固定底邊constant100.

play_button_pin.png

拖拽引線到控制器命名爲playButton.而後建立一個動做命名爲playGame並寫入如下代碼:

playButton.isHidden = true
    
let gameScene = SCNScene(named: "HighRise.scnassets/Scenes/GameScene.scn")!
let transition = SKTransition.fade(withDuration: 1.0)
scnScene = gameScene
let mainCamera = scnScene.rootNode.childNode(withName: "Main Camera", recursively: false)!
scnView.present(scnScene, with: transition, incomingPointOfView: mainCamera, completionHandler: nil)
    
height = 0
scoreLabel.text = "\(height)"
    
direction = true
perfectMatches = 0
    
previousSize = SCNVector3(1, 0.2, 1)
previousPosition = SCNVector3(0, 0.1, 0)
    
currentSize = SCNVector3(1, 0.2, 1)
currentPosition = SCNVector3Zero
    
let boxNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
boxNode.position.z = -1.25
boxNode.position.y = 0.1
boxNode.name = "Block\(height)"
boxNode.geometry?.firstMaterial?.diffuse.contents = UIColor(colorLiteralRed: 0.01 * Float(height),
  green: 0, blue: 1, alpha: 1)
scnScene.rootNode.addChildNode(boxNode)
複製代碼

你注意到,你已經重置了遊戲中的全部變量爲默認值,並添加了第一個方塊.

由於已經添加了第一塊方塊,移除viewDidLoad(_:) 中下面的代碼,從聲明blockNode到添加到場景中.

//1
    let blockNode = SCNNode(geometry: SCNBox(width: 1, height: 0.2, length: 1, chamferRadius: 0))
    blockNode.position.z = -1.25
    blockNode.position.y = 0.1
    blockNode.name = "Block\(height)"
    
    //2
    blockNode.geometry?.firstMaterial?.diffuse.contents =
      UIColor(colorLiteralRed: 0.01 * Float(height), green: 0, blue: 1, alpha: 1)
    scnScene.rootNode.addChildNode(blockNode)  
複製代碼

在剛纔建立的方法下面定義一個新方法:

func gameOver() {
    let mainCamera = scnScene.rootNode.childNode(
      withName: "Main Camera", recursively: false)!
    
    let fullAction = SCNAction.customAction(duration: 0.3) { _,_ in
      let moveAction = SCNAction.move(to: SCNVector3Make(mainCamera.position.x, 
        mainCamera.position.y * (3/4), mainCamera.position.z), duration: 0.3)
      mainCamera.runAction(moveAction)
      if self.height <= 15 {
        mainCamera.camera?.orthographicScale = 1
      } else {
        mainCamera.camera?.orthographicScale = Double(Float(self.height/2) / 
          mainCamera.position.y)
      }
    }
    
    mainCamera.runAction(fullAction)
    playButton.isHidden = false
  }
複製代碼

這裏,你縮放攝像機鏡頭來露出整個塔.最後,設置play按鈕爲可見,這樣玩家就能夠開始一個新遊戲.

handleTap(_:) 內部,在徹底錯失方塊的if語句中,調用gameover(),放在return語句以前,兩個if語句裏面都放:

gameOver()
複製代碼

編譯運行.當你失敗時,就能從新開始一個新遊戲了.

啓動圖片

遊戲啓動時會有難看的白屏.打開LaunchScreen.storyboard並拖拽進一個圖像視圖.四周對齊屏幕:

img_view_pin.png

更改圖片爲Gradient.png

img_view_image.png

如今咱們已經將白屏替換爲了漂亮的漸變圖!

恭喜你,你已經完成了!你能夠從這裏下載最終完成版final project

end

相關文章
相關標籤/搜索