21-3D打磚塊游戲Breaker

文章選自掘金蘋果API搬運(yùn)工的文章[SceneKit專題]21-3D打磚塊游戲Breaker
主要記錄自己在學(xué)習(xí)ARKit的過程中看到的好的文章香府,避免到時(shí)候鏈接失效無法找到原文的情況辩涝,非常感謝原博主的辛勤付出,也在此分享出來跟大家一起學(xué)習(xí)迈螟。

創(chuàng)建游戲

打開Xcode,創(chuàng)建一個(gè)新項(xiàng)目,選擇iOS/Application/Game模板. 游戲名Breaker,語言選Swift,游戲技術(shù)SceneKit,設(shè)備支持Universal,取消勾選兩個(gè)測試選項(xiàng).

打開項(xiàng)目,刪除art.scnassets文件夾.并將GameViewController.swift中的內(nèi)容替換為下面:

import UIKit
import SceneKit
class GameViewController: UIViewController {
  var scnView: SCNView!
  override func viewDidLoad() {
    super.viewDidLoad()
// 1
    setupScene()
    setupNodes()
    setupSounds()
}
// 2
  func setupScene() {
      scnView = self.view as! SCNView
      scnView.delegate = self
  }
  func setupNodes() {
}
  func setupSounds() {
  }
  override var shouldAutorotate: Bool { return true }
  override var prefersStatusBarHidden: Bool { return true }
}
// 3
extension GameViewController: SCNSceneRendererDelegate {
  func renderer(_ renderer: SCNSceneRenderer,
    updateAtTime time: TimeInterval) {
  }
}

代碼含義:

  1. viewDidLoad()里調(diào)用一些空的占位方法.稍后,我們會(huì)向這些方法里添加代碼.
  2. 在創(chuàng)建場景方法里將self.view轉(zhuǎn)換為SCNView對象并儲(chǔ)存起來以便訪問,記self成為渲染循環(huán)的代理.
  3. GameViewController遵守SCNSceneRendererDelegate協(xié)議,并實(shí)現(xiàn)renderer(_: updateAtTime:)方法.

找到resources/AppIcon文件夾,里面有各種尺寸的應(yīng)用圖標(biāo).打開項(xiàng)目的Assets.xcassets并選擇AppIcon.將圖標(biāo)拖放到里面去.

選中Assets.xcassets,拖放resources/Logo_Diffuse.png到里面.然后打開LaunchScreen.storyboard,將背景顏色改為深藍(lán)色.在右下角的Media Library中找到Logo_Diffuse,拖放到啟動(dòng)屏幕里.設(shè)置圖片的Content ModeAspect Fit,并添加約束,讓它處在屏幕中間:

完成后:


下面還需要添加音效.找到resources/Breaker.scnassets文件夾,拖放到時(shí)項(xiàng)目中.注意選中Copy items if needed, Create groups及目標(biāo)項(xiàng)目Breaker.這里面有子文件夾,SoundsTextures分別是音頻和紋理圖片.

還需要一些游戲工具類.拖放resources/GameUtil到項(xiàng)目中. 打開GameViewController.swift,在scnView下面添加屬性:

var game = GameHelper.sharedInstance

加載場景

右擊Breaker.scnassets,創(chuàng)建一個(gè)新文件夾命名為Scenes,用來盛放所有場景.

選中Breaker項(xiàng)目,創(chuàng)建新文件,選擇iOS/Resource/ SceneKit Scene模板,命名為Game.scn.注意位置選擇在Breaker.scnassets下面的Scenes文件夾下面.

從右下角的物體對象庫中拖拽一個(gè)Box出來,隨便放在場景中:

GameViewController中添加一個(gè)新屬性:

var scnScene: SCNScene!

接下來,在setupScene()方法的底部,添加下面代碼:

 scnScene = SCNScene(named: "Breaker.scnassets/Scenes/Game.scn")
scnView.scene = scnScene

運(yùn)行一下:


測試完成后,就可以刪除立方體了.在左側(cè)的場景樹中,按Command-A選擇所有節(jié)點(diǎn),按Delete鍵全部刪除.

添加攝像機(jī)

打開GameViewController.swift,在setupNodes()中添加下面一行:

scnScene.rootNode.addChildNode(game.hudNode)

然后,在renderer(_,updateAtTime)中添加一行:

game.updateHUD()

選中Game.scn,以顯示編輯器. 在左下角點(diǎn)擊 + 按鈕,創(chuàng)建一個(gè)空的節(jié)點(diǎn)默認(rèn)命名為untitled.將其改名為Cameras.

從右下角的對象庫中拖放兩個(gè)Camera節(jié)點(diǎn)到場景中.

分別命名為VerticalCameraHorizontalCamera.稍后會(huì)講為什么需要兩個(gè)攝像機(jī).

TL/DR:雙攝像機(jī)能讓你更好地處理橫屏與豎屏狀態(tài)下的視角.

讓兩個(gè)攝像機(jī)都成為Cameras的子節(jié)點(diǎn):

選中VerticalCamera,在節(jié)點(diǎn)檢查器中設(shè)置Position(x:0, y:22, z:9),Euler(x:-70, y:0, z:0)

選中HorizontalCamera,在節(jié)點(diǎn)檢查器中設(shè)置Position(x:0, y:8.5, z:15),Euler(x:-40, y:0, z:0)

對比來看,水平攝像機(jī)比豎直攝像機(jī)離得更近,角度也更小.


GameViewController.swift中添加兩個(gè)屬性:

 var horizontalCameraNode: SCNNode!
  var verticalCameraNode: SCNNode!

setupNodes()方法的開頭添加下面代碼:

horizontalCameraNode = scnScene.rootNode.childNode(withName:
"HorizontalCamera", recursively: true)!
verticalCameraNode = scnScene.rootNode.childNode(withName:
"VerticalCamera", recursively: true)!

因?yàn)閳鼍耙呀?jīng)加載進(jìn)來了,所以我們只需要用childNode(withName:recursively:)方法來找到攝像機(jī)節(jié)點(diǎn)就可以了.recursively設(shè)置為true會(huì)遞歸遍歷其中的子文件夾.

處理旋轉(zhuǎn)

設(shè)置在旋轉(zhuǎn)時(shí),屏幕的顯示范圍也在跟著變.與其在兩個(gè)方向中找到"sweet-spot",倒不如使用兩個(gè)攝像機(jī),每一個(gè)都可以最大化利用顯示范圍.


為了追蹤設(shè)備方向,需要重寫viewWillTransition(to size:, with coordinator:)方法:

// 1
override func viewWillTransition(to size: CGSize, with coordinator:
UIViewControllerTransitionCoordinator) {
// 2
  let deviceOrientation = UIDevice.current.orientation
  switch(deviceOrientation) {
  case .portrait:
    scnView.pointOfView = verticalCameraNode
  default:
    scnView.pointOfView = horizontalCameraNode
  }
}

代碼含義:

  1. 重寫viewWillTransition(to:with:)來運(yùn)行切換方向的代碼.
  2. 根據(jù)從UIDevice.current().orientation中獲取到的deviceOrientation來切換方向.如果將要切換到.portrait,則設(shè)置視點(diǎn)為verticalCameraNode.否則,切換視點(diǎn)到horizontalCameraNode.

運(yùn)行一下:


添加小球

選中Game.scn.在對象庫中,拖放一個(gè)Sphere到場景中.

確保球體節(jié)點(diǎn)仍處于選中狀態(tài),然后選擇節(jié)點(diǎn)檢查器.將Name命名為Ball,將position設(shè)置為0,這樣球就在正中間了.

接著打開屬性檢查器.將Radius改為0.25, Segment count17.

兩種球體sphere和geosphere本質(zhì)上是同樣的.不同的是下面的geodesic復(fù)選框,決定了渲染引擎如何構(gòu)建球體.一種是四邊形,一種是三角形.

下一步,選中材料檢查器.將Diffuse改為7F7F7F.將Specular改為White.

繼續(xù)向下,找到Setting區(qū)域,將Shininess改為0.3.

完成后,選中HorizontalCamera,場景看起來是這樣:

下面,打開GameViewController.swift,添加一個(gè)屬性:

var ballNode: SCNNode!

setupNodes()末尾添加下面的代碼:

 ballNode = scnScene.rootNode.childNode(withName: "Ball", recursively:true)!

三點(diǎn)光照

首先,打開Game.scn,點(diǎn)擊 + 創(chuàng)建一個(gè)空節(jié)點(diǎn),命名為Lights.它將用來盛放場景中的所有燈光.


從對象庫中,拖放一個(gè)Omni light到場景中,放到燈光節(jié)點(diǎn)下面.

選中燈光節(jié)點(diǎn),打開節(jié)點(diǎn)檢查器,重命名節(jié)點(diǎn)為Back.設(shè)置Position(x:-15, y:-2, z:15)

選擇Attributes Inspector,設(shè)置泛光燈屬性.

再從對象庫中拖放一個(gè)Omni light光源到場景中.還是移動(dòng)到Lights組節(jié)點(diǎn)下.

命名新節(jié)點(diǎn)為Front,設(shè)置Position(x:6, y:10, z:15).

再從對象庫中拖放一個(gè)Ambient light光源到場景中.還是移動(dòng)到Lights組節(jié)點(diǎn)下.


命名新節(jié)點(diǎn)為Ambient,設(shè)置Position(x:0, y:0, z:0).

打開屬性檢查器:


完成后的場景效果:


運(yùn)行一下,效果如下:


創(chuàng)建邊框

選擇Game.scn,點(diǎn)擊 + 按鈕添加一個(gè)空白節(jié)點(diǎn),命名為Barriers. 這將是用來盛放所有的邊框節(jié)點(diǎn)的:

從對象庫中,拖放一個(gè)Box,在場景樹中,將新的立方體節(jié)點(diǎn)拖放到Barriers組節(jié)點(diǎn)下面.


打開節(jié)點(diǎn)檢查器,命名為Top,設(shè)置位置為 (x:0,y:0,z:-10.5).開屬性檢查器,設(shè)置Sizewidth:13, height:2, length:1,設(shè)置Chamfer radius0.3. 打開

材料檢查器,將Diffuse改為暗灰色Hex Color333333,并將Specular改為White:

下面我們通過復(fù)制的方式來創(chuàng)建底部的邊框. 復(fù)制方法是:按住Option鍵,點(diǎn)擊要復(fù)制的節(jié)點(diǎn)并沿著藍(lán)色坐標(biāo)軸拖動(dòng):


復(fù)制成功后,重命名為Bottom,將設(shè)置為Barriers組的子節(jié)點(diǎn).

更改一下位置,Position(x:0, y:0, z:10.5).

最終效果,如圖:

還有一個(gè)重要的事:注意場景樹的結(jié)構(gòu),組節(jié)點(diǎn)是如何包含頂邊框/底邊框的. 選中新復(fù)制出的節(jié)點(diǎn)的Attributes Inspector屬性檢查器,在Geometry Sharing區(qū)下面,點(diǎn)擊Unshare按鈕.

因?yàn)閯?chuàng)建復(fù)本時(shí),復(fù)制出的節(jié)點(diǎn)仍然會(huì)共享原始節(jié)點(diǎn)的幾何體(Geometry).這個(gè)默認(rèn)設(shè)置是為了減少總的繪制調(diào)用(draw call)數(shù).

左側(cè)邊框的建立

左右兩側(cè)的邊框分別由兩根圓柱組成.先在Barriers組下面建立一個(gè)Left節(jié)點(diǎn),并放置到合適的位置.里面的子節(jié)點(diǎn)也會(huì)跟著發(fā)生位置變動(dòng).

建立左邊框的上半部分 拖放一個(gè)Cylinder,重命名為Top,放置到Barriers/Left下面:


在節(jié)點(diǎn)檢查器中,設(shè)置Position(x:0, y:0.5, z:0),Euler(x:90, y:0, z:0).

屬性檢查器中,設(shè)置Radius0.3,Height22.5.

材料檢查器中,設(shè)置DiffuseHex Color #B3B3B3 ,SpecularWhite:



建立左邊框的下半部分 選中Barrier/Left/Top節(jié)點(diǎn),按住Option鍵,沿藍(lán)色坐標(biāo)軸,點(diǎn)擊拖動(dòng).重命名為Bottom,放在Barriers/Left組下面.在節(jié)點(diǎn)檢查器中,設(shè)置Position(x:0,y:-0.5,z:0):



最終效果如圖:



建立右側(cè)邊框

選中Barriers/Left組,按住Command+Option并沿紅色坐標(biāo)軸點(diǎn)擊拖動(dòng),這樣就復(fù)制了一組節(jié)點(diǎn).重命名為Right,并設(shè)置位置為 (x:6, y:0, z:0)




最終效果如圖:

創(chuàng)建球拍擋板

點(diǎn)擊 + 按鈕創(chuàng)建新的節(jié)點(diǎn),命名為Paddle.打開節(jié)點(diǎn)檢查器,設(shè)置Position(x:0, y:0, z:8).



球拍擋板共有三個(gè)部分:左,中,右. 我們先創(chuàng)建中間部分,拖放一個(gè)圓柱體,命名為Center,放在Paddle組節(jié)點(diǎn)下面.


打開節(jié)點(diǎn)檢查器,設(shè)置Position0,設(shè)置Euler(x:0, y:0, z:90).

打開屬性檢查器,設(shè)置Radius0.25, Height1.5.

打開材料檢查器,設(shè)置DiffuseHex Color #333333, SpecularWhite.



創(chuàng)建左側(cè)部分

拖放一個(gè)圓柱體,命名為Left,放在Paddle組節(jié)點(diǎn)下面.


設(shè)置Position(x:-1, y:0, z:0), Euler(x:0, y:0, z:90).
打開屬性檢查器,設(shè)置Radius0.25, Height0.5.
打開材料檢查器,設(shè)置DiffuseHex Color #666666, SpecularWhite.



復(fù)制右側(cè)部分 選中Paddle/Left節(jié)點(diǎn),按住Command+Option并沿綠色坐標(biāo)軸點(diǎn)擊拖動(dòng),這樣就復(fù)制了一組節(jié)點(diǎn).重命名為Right,并設(shè)置位置為(x:1, y:0, z:0).還是要注意取消幾何體共享.


綁定球拍擋板,以便操作

打開GameViewController.swift,添加屬性:

var paddleNode: SCNNode!

setupNodes()方法的末尾,添加綁定球拍的代碼:

 paddleNode =
  scnScene.rootNode.childNode(withName: "Paddle", recursively: true)!

你可以在本章對應(yīng)代碼的projects/final/Breaker文件夾下,找到最終的完成版項(xiàng)目.

添加磚塊,挑戰(zhàn)項(xiàng)目

  • 首先,創(chuàng)建一個(gè)組節(jié)點(diǎn)命名為Bricks,用來放置所有的磚塊.

  • 設(shè)置Bricks節(jié)點(diǎn)的位置為 (x:0, y:0, z:-3.0).

  • 每個(gè)磚塊都是使用一個(gè)Box,尺寸為width:1, height:0.5, length: 0.5,Chamfer Radius:0.05.

  • 先創(chuàng)建一列各種顏色的磚塊,顏色分別使用white (#FFFFFF), red (#FF0000), yellow (#FFFF00), blue (#0000FF), purple (#8000FF), green (#00FF80):


  • 為了方便定位,白色磚塊可以放置在(x: 0, y:0, z:-2.5),綠色磚塊應(yīng)該在(x:0, y:0, z:0).

  • 將磚塊用自己的顏色命名.

  • 復(fù)制更多列出來.(按住OptionCommand)

  • 復(fù)制時(shí),記得使用材料檢查器下面的Unshare按鈕,以免改變了原始節(jié)點(diǎn)的顏色.

  • 復(fù)制填滿整個(gè)區(qū)域.

最終效果如圖:



運(yùn)行程序


你可以在本章對應(yīng)代碼的projects/challenge/Breaker文件夾下,找到最終的完成版項(xiàng)目.

物理效果

先給小球添加物理效果. 打開Game.scn并選中Ball.打開Physics Inspector物理效果檢查器.將Physics BodyType改為Dynamic. 并按下圖設(shè)置各個(gè)項(xiàng)目:

給邊框添加物理效果 一次性選中左右邊框的四個(gè)部分,可以有兩種方法:

  1. 按住Command在場景樹中點(diǎn)擊每個(gè)節(jié)點(diǎn).
  2. 類似于文件夾多選操作,先選中Top節(jié)點(diǎn),按住Shift,點(diǎn)擊Right,兩者之間的節(jié)點(diǎn)會(huì)被全部選中.

保持選中狀態(tài),打開物理效果檢查器,在Physics Body區(qū)域,將Type改為Static,在新展開的設(shè)置項(xiàng)里按下圖設(shè)置:

點(diǎn)擊工具條上的播放按鈕,就可以預(yù)覽物理效果:


接著給磚塊添加物理效果 全選磚塊節(jié)點(diǎn):


設(shè)置為Static形體,其余如下圖:


給球拍擋板添加物理效果 選中球拍三個(gè)節(jié)點(diǎn),打開物理效果檢查器,設(shè)置TypeKinematic,其余項(xiàng)目設(shè)置如下:

運(yùn)行一下,小球會(huì)瘋狂地到處碰撞,包括與球拍的碰撞:


碰撞檢測

碰撞檢測用到的是SCNPhysicsContactDelegate協(xié)議. 打開GameViewController.swift,添加一個(gè)新屬性:

var lastContactNode: SCNNode!

它的作用有兩個(gè):

  1. 當(dāng)兩個(gè)節(jié)點(diǎn)發(fā)生互相滑動(dòng)時(shí),就相當(dāng)于和同一個(gè)節(jié)點(diǎn)不停發(fā)生碰撞,而我們只關(guān)心第一次碰撞.
  2. 在這個(gè)游戲中,盡管碰撞可能會(huì)持續(xù),但小球不能和同一個(gè)節(jié)點(diǎn)兩次發(fā)生接觸事件,直到小球碰到了其它節(jié)點(diǎn).所以我們需要確保只處理一次碰撞.

GameViewController.swift底部添加類擴(kuò)展:

// 1
extension GameViewController: SCNPhysicsContactDelegate {
  // 2
  func physicsWorld(_ world: SCNPhysicsWorld,
    didBegin contact: SCNPhysicsContact) {
    // 3
    var contactNode: SCNNode!
    if contact.nodeA.name == "Ball" {
      contactNode = contact.nodeB
} else {
      contactNode = contact.nodeA
    }
// 4
    if lastContactNode != nil &&
        lastContactNode == contactNode {
return
}
    lastContactNode = contactNode
  }
}

代碼含義:

  1. 擴(kuò)展GameViewController類以實(shí)現(xiàn)SCNPhysicsContactDelegate協(xié)議,方便組織代碼.
  2. 實(shí)現(xiàn)physicsWorld(_:didBegin:).默認(rèn)不觸發(fā),需要設(shè)置接觸掩碼.
  3. 傳入一個(gè)SCNPhysicsContact參數(shù),可以判斷并找到哪個(gè)是小球.
  4. 防止和同一個(gè)節(jié)點(diǎn)多次碰撞.

使用位掩碼來檢測接觸事件. 我們已經(jīng)給游戲中的不同元素設(shè)置了Category bitmask分類掩碼,這個(gè)值是二進(jìn)制的,各分類如下:

Ball:     1 (Decimal) = 00000001 (Binary)
Barrier:  2 (Decimal) = 00000010 (Binary)
Brick:    4 (Decimal) = 00000100 (Binary)
Paddle:   8 (Decimal) = 00001000 (Binary)

GameViewController頂部定義一個(gè)枚舉:

enum ColliderType: Int {
  case ball     = 0b0001
  case barrier  = 0b0010
  case brick    = 0b0100
  case paddle   = 0b1000
}

setupNodes()方法的末尾添加下面代碼來處理碰撞:

ballNode.physicsBody?.contactTestBitMask =
  ColliderType.barrier.rawValue |
    ColliderType.brick.rawValue |
      ColliderType.paddle.rawValue

這樣,你就告訴了物理引擎,當(dāng)小球和分類掩碼為2, 4, 8的節(jié)點(diǎn)碰撞時(shí),調(diào)用physicsWorld(_:didBegin:)方法通知我. 2,4,8也就是指barrier邊框, brick磚塊和paddle球拍.

physicsWorld(_:didBegin:)方法的末尾繼續(xù)寫:

// 1
if contactNode.physicsBody?.categoryBitMask ==
ColliderType.barrier.rawValue {
  if contactNode.name == "Bottom" {
    game.lives -= 1
    if game.lives == 0 {
      game.saveState()
      game.reset()
    }
} }
// 2
if contactNode.physicsBody?.categoryBitMask ==
ColliderType.brick.rawValue {
  game.score += 1
  contactNode.isHidden = true
  contactNode.runAction(
    SCNAction.waitForDurationThenRunBlock(duration: 120) {
    (node:SCNNode!) -> Void in
       node.isHidden = false
  })
}
// 3
if contactNode.physicsBody?.categoryBitMask ==
ColliderType.paddle.rawValue {
  if contactNode.name == "Left" {
    ballNode.physicsBody!.velocity.xzAngle -=
      (convertToRadians(angle: 20))
  }
  if contactNode.name == "Right" {
    ballNode.physicsBody!.velocity.xzAngle +=
      (convertToRadians(angle: 20))
  }
}
// 4
ballNode.physicsBody?.velocity.length = 5.0

代碼含義:

  1. 檢查categoryBitMask來判斷小球是不是和邊框節(jié)點(diǎn)碰撞了.再根據(jù)名字判斷,如果是和底部邊框碰撞,則需要扣掉一個(gè)生命值.
  2. 檢查并判斷小球是不是和磚塊碰撞了.讓對應(yīng)磚塊消失120秒,再皇親出現(xiàn),這樣游戲就能一直玩下去.
  3. 判斷小球是不是和球拍碰撞了.如果遇到了中間部分,不改變物理效果,由引擎自動(dòng)控制反彈.如果是碰到了左邊或右邊,則給小球增加一個(gè)20度的水平偏轉(zhuǎn).
  4. 將小球速度強(qiáng)制限制在5,以防物理引擎出現(xiàn)偏差而失控.

還要記得成為接觸代理.在setupScene()底部添加一行:

scnScene.physicsWorld.contactDelegate = self

運(yùn)行一下,可以打掉磚塊了!


觸摸控制球拍

GameViewController添加兩個(gè)屬性:

 var touchX: CGFloat = 0
 var paddleX: Float = 0

下一步,給GameViewController添加下面的方法:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
  for touch in touches {
    let location = touch.location(in: scnView)
    touchX = location.x
    paddleX = paddleNode.position.x
  } 
}

記錄下觸摸的初始位置,球拍的初始位置

 override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
  for touch in touches {
    // 1
    let location = touch.location(in: scnView)
    paddleNode.position.x = paddleX +
      (Float(location.x - touchX) * 0.1)
    // 2
    if paddleNode.position.x > 4.5 {
      paddleNode.position.x = 4.5
    } else if paddleNode.position.x < -4.5 {
      paddleNode.position.x = -4.5
    }
  }
}

代碼含義:

  1. 當(dāng)觸摸位置移動(dòng)時(shí),根據(jù)相對初始觸摸位置的偏移touchX來更新球拍的位置.
  2. 限制球拍的移動(dòng),確保在邊框之間.

運(yùn)行一下,可以來回移動(dòng)球拍了:


攝像機(jī)追蹤

touchesMoved(_:with:)方法的底部,添加下面代碼,讓攝像機(jī)水平位置和球拍一致:

 verticalCameraNode.position.x = paddleNode.position.x
 horizontalCameraNode.position.x = paddleNode.position.x

GameViewController中添加一個(gè)新屬性來依舊在地板節(jié)點(diǎn):

var floorNode: SCNNode!

setupNodes()底部添加代碼:

floorNode =
  scnScene.rootNode.childNode(withName: "Floor",
    recursively: true)!
verticalCameraNode.constraints =
  [SCNLookAtConstraint(target: floorNode)]
horizontalCameraNode.constraints =
  [SCNLookAtConstraint(target: floorNode)]

這段代碼含義:找到名為Floor的節(jié)點(diǎn),綁定到floorNode.給場景中的兩個(gè)攝像機(jī)添加SCNLookAtConstraint約束,能讓攝像機(jī)始終對準(zhǔn)目標(biāo)節(jié)點(diǎn),也就是游戲區(qū)域的中央.

可以運(yùn)行試玩一下了:


粒子效果

選中場景Game.scn.從對象庫中拖放一個(gè)Particle System粒子系統(tǒng)到場景中,命名為Trail,并放在Ball節(jié)點(diǎn)中


打開節(jié)點(diǎn)檢查器,設(shè)置position(x:0, y:0, z:0).

打開屬性檢查器,配置粒子系統(tǒng)的屬性:



完成后,點(diǎn)擊播放按鈕預(yù)覽一下:


正式運(yùn)行一下,可以玩起來了!


該部分最終完成的項(xiàng)目,放在代碼中對應(yīng)章節(jié)的projects/final/Breaker文件夾里.

添加聲音效果

添加setupSounds()方法,并添加代碼:

game.loadSound(name: "Paddle",
  fileNamed: "Breaker.scnassets/Sounds/Paddle.wav")
game.loadSound(name: "Block0",
  fileNamed: "Breaker.scnassets/Sounds/Block0.wav")
game.loadSound(name: "Block1",
  fileNamed: "Breaker.scnassets/Sounds/Block1.wav")
game.loadSound(name: "Block2",
  fileNamed: "Breaker.scnassets/Sounds/Block2.wav")
game.loadSound(name: "Barrier",
  fileNamed: "Breaker.scnassets/Sounds/Barrier.wav")

可以在碰撞的時(shí)候,播放對應(yīng)的音效:

  1. 使用game.playSound(node: scnScene.rootNode, name: "SoundToPlay")來播放已加載好的音效.
  2. Block添加音效時(shí)使用隨機(jī)值,用random() % 3來產(chǎn)生0~2的隨機(jī)數(shù).

最終完成的項(xiàng)目,放在代碼中對應(yīng)章節(jié)的projects/challenge/Breaker文件夾里.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子汁胆,更是在濱河造成了極大的恐慌,老刑警劉巖霜幼,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫩码,死亡現(xiàn)場離奇詭異,居然都是意外死亡罪既,警方通過查閱死者的電腦和手機(jī)铸题,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琢感,“玉大人丢间,你說我怎么就攤上這事【哉耄” “怎么了烘挫?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柬甥。 經(jīng)常有香客問我饮六,道長,這世上最難降的妖魔是什么苛蒲? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任卤橄,我火速辦了婚禮,結(jié)果婚禮上撤防,老公的妹妹穿的比我還像新娘虽风。我一直安慰自己,他們只是感情好寄月,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布辜膝。 她就那樣靜靜地躺著,像睡著了一般漾肮。 火紅的嫁衣襯著肌膚如雪厂抖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天克懊,我揣著相機(jī)與錄音忱辅,去河邊找鬼七蜘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛墙懂,可吹牛的內(nèi)容都是我干的橡卤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼损搬,長吁一口氣:“原來是場噩夢啊……” “哼碧库!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起巧勤,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嵌灰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后颅悉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沽瞭,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年剩瓶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了驹溃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡儒搭,死狀恐怖吠架,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搂鲫,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布磺平,位于F島的核電站魂仍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拣挪。R本人自食惡果不足惜擦酌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望菠劝。 院中可真熱鬧赊舶,春花似錦、人聲如沸赶诊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舔痪。三九已至寓调,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锄码,已是汗流浹背夺英。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工晌涕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人痛悯。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓余黎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親载萌。 傳聞我的和親對象是個(gè)殘疾皇子驯耻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355