SceneKit系列文章目錄node
更多iOS相關知識查看github上WeekWeekUpProjectgit
在本教程中,你將會學習如何製做一個相似Stack這樣的遊戲. github
本教程將包含如下內容:編程
下載初始項目starter project. 在初始項目裏面,你會發現SceneKit目錄文件中帶有音頻和場景文件.另外,還有一些SCNVector3類擴展來執行簡單的向量算術運算及生成漸變圖片.還有App Icon也已經添加進去了!花點時間熟悉一下項目吧.swift
你將會建立一個相似於Stack的遊戲.這個遊戲的目標是在一塊方塊上疊放另外一個方塊.須要當心的是:方塊疊放時稍微偏一點,多餘部分就會被切掉.徹底沒對齊,那就game over了!app
你將從創建你的遊戲場景開始.打開GameScene.scn. 編輯器
拖拽一個新的 camera到場景中,而後選擇 Node Inspector並重命名節點爲 Main Camera.設置位置爲 X: 3, Y: 4.5, Z: 3,旋轉爲 X: -40, Y: 45, Z:0:切換到Attributes Inspector並切換相機的Projection type爲Orthographic. 下一步,添加燈光到場景中. 從對象庫中拖拽一個新的方向光到場景中,命名爲Directional Light.由於相機只看到了場景的一側,你沒必要去照亮看不見的令一側.回到Attributes Inspector,設置位置爲X: 0, Y: 0, Z: 0,旋轉爲X: -65, Y: 20, Z:-30: post
神奇,亮起來了!學習
如今回到塔的頂部.你須要一個基礎方塊來支承這個塔,來讓玩家在上面建造.拖拽一個盒子到場景中,設置屬性:字體
你須要添加一個動態形體到基礎方塊,切換到Physics Inspector中,並將物理形體改成Static.
如今讓咱們配上漂亮的背景顏色!在選中基礎方塊的同時,切換到Scene Inspector,並拖拽文件Gradient.png到背景選擇框中:
你須要一個方法來顯示給玩家,他們的塔已經堆放了多高.打開Main.storyboard;看到它已經有一個SCNView.添加一個label在SCNView頂部並設置文本爲0.而後添加一個約束將label對齊到中心,像這樣:
添加另外一個約束將label頂部與屏幕頂部對齊.
而後切換到Attributes Inspector中,切換字體爲Custom, Thonburi, Regular, 50.
而後使用assistant editor來添加一個從label到控制器的引用,命名爲scoreLabel:
編譯運行,看看如今有什麼了.
知道怎麼讓塔愈來愈高麼?對,建立更多方塊. 建立一些屬性來幫你追蹤正在使用的方塊.爲此,打開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
複製代碼
這段代碼含義:
如今,是時間添加方塊到場景中了.在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)
複製代碼
代碼含義:
建立並運行,會看到你的新方塊出如今屏幕上!
如今已經有一條新的方塊用來放置.可是,我想若是方塊是移動的會更好玩. 要實現這個移動,須要設置控制器做爲場景渲染代理,並實現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
}
}
}
複製代碼
代碼含義:
默認狀況下,SceneKit會暫停場景.爲了看到場景中物體的移動,在viewDidLoad的底部添加下面代碼:
scnView.isPlaying = true
scnView.delegate = self
複製代碼
這段代碼中,將這個控制器設置爲場景的渲染代理,這樣就能執行剛纔寫的代理方法了. 建立運行,查看運動!
如今,咱們已經讓方塊移動了,還須要在玩家點擊屏幕時添加一個新方塊並重設老方塊的尺寸.切換到Main.storyboard並添加一個tap gesture recognizer到SCNView,像這樣:
如今在控制器裏面用輔助編輯器建立一個動做並命名爲handleTap.
切換到標準編輯區,並打開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.而後計算偏移及新方塊的尺寸.從而改變方塊的尺寸和位置,並給它一個靜態物理形體.
偏移等於上一層和當前層位置的差值.經過從當前尺寸上減去差值的絕對值,就獲得了新尺寸. 注意到,把當前節點設置位置到偏移處,方塊的邊緣完美對齊了上一個層的邊緣.這創造出一種切掉方塊的錯覺.
下一步,你須要一個方法來建立塔上的下一個方塊.在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.而後增長高度.
建立並運行.一切看起來堆垛地很完美!
遊戲正確地重設了方塊尺寸,可是若是被砍掉的部分能從塔上掉落,遊戲會看起來更酷.
在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,由於等於時不會產生一個方塊碎片.
你根據當前位置偏移量的一半來獲得碎片的位置.而後,根據方塊位置的正負,添加或扣除當前尺寸減去偏移的一半.
在handleTap(_:) 裏面的addNewBlock(_:) 以前調用該方法:
addBrokenBlock(currentBoxNode)
複製代碼
當碎片節點掉落出視線時,還在不停掉落,並無銷燬.在renderer(_:updateAtTime:) 裏面最上方添加代碼:
for node in scnScene.rootNode.childNodes {
if node.presentation.position.y <= -20 {
node.removeFromParentNode()
}
}
複製代碼
這段代碼會刪除Y值小於-20的全部節點. 運行看看切下的方塊!
如今遊戲機制的核心部分已經完成了,還有一些收尾工做.當玩家完美對齊上一層時應該有獎勵.還有,如今尚未輸贏判斷,當你失敗後也沒法開始一個新遊戲! 遊戲尚未聲音,須要添加一些聲音.
在處理完美對齊的狀況,在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.
下一步,設置對齊方式align爲中心對齊center,並固定底邊constant爲100.
拖拽引線到控制器命名爲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並拖拽進一個圖像視圖.四周對齊屏幕:
更改圖片爲Gradient.png
如今咱們已經將白屏替換爲了漂亮的漸變圖!
恭喜你,你已經完成了!你能夠從這裏下載最終完成版final project
end