[SceneKit專題]21-3D打磚塊遊戲Breaker

蘋果API搬運工發表於2017-12-25

說明

本系列文章是對<3D Apple Games by Tutorials>一書的學習記錄和體會

此書對應的程式碼地址

SceneKit系列文章目錄

更多iOS相關知識檢視github上WeekWeekUpProject

06-SceneKit Editor場景編輯器

建立遊戲

開啟Xcode,建立一個新專案,選擇iOS/Application/Game模板. 遊戲名Breaker,語言選Swift,遊戲技術SceneKit,裝置支援Universal,取消勾選兩個測試選項.

開啟專案,刪除art.scnassets資料夾.並將GameViewController.swift中的內容替換為下面:

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()裡呼叫一些空的佔位方法.稍後,我們會向這些方法裡新增程式碼.
  2. 在建立場景方法裡將self.view轉換為SCNView物件並儲存起來以便訪問,記self成為渲染迴圈的代理.
  3. GameViewController遵守SCNSceneRendererDelegate協議,並實現renderer(_: updateAtTime:)方法.

找到resources/AppIcon資料夾,裡面有各種尺寸的應用圖示.開啟專案的Assets.xcassets並選擇AppIcon.將圖示拖放到裡面去.

WX20171106-215541@2x.png

選中Assets.xcassets,拖放resources/Logo_Diffuse.png到裡面.然後開啟LaunchScreen.storyboard,將背景顏色改為深藍色.在右下角的Media Library中找到Logo_Diffuse,拖放到啟動螢幕裡.設定圖片的Content ModeAspect Fit,並新增約束,讓它處在螢幕中間:

WX20171106-221817@2x.png

完成後:

WX20171106-221905@2x.png

下面還需要新增音效.找到resources/Breaker.scnassets資料夾,拖放到時專案中.注意選中Copy items if needed, Create groups及目標專案Breaker.這裡面有子資料夾,SoundsTextures分別是音訊和紋理圖片.

還需要一些遊戲工具類.拖放resources/GameUtil到專案中. 開啟GameViewController.swift,在scnView下面新增屬性:

var game = GameHelper.sharedInstance
複製程式碼
載入場景

右擊Breaker.scnassets,建立一個新資料夾命名為Scenes,用來盛放所有場景.

WX20171106-222712@2x.png

選中Breaker專案,建立新檔案,選擇iOS/Resource/ SceneKit Scene模板,命名為Game.scn.注意位置選擇在Breaker.scnassets下面的Scenes資料夾下面.

WX20171106-222942@2x.png

從右下角的物體物件庫中拖拽一個Box出來,隨便放在場景中:

WX20171106-225141@2x.png

GameViewController中新增一個新屬性:

var scnScene: SCNScene!
複製程式碼

接下來,在setupScene()方法的底部,新增下面程式碼:

 scnScene = SCNScene(named: "Breaker.scnassets/Scenes/Game.scn")
scnView.scene = scnScene
複製程式碼

執行一下:

WX20171106-225545@2x.png

測試完成後,就可以刪除立方體了.在左側的場景樹中,按Command-A選擇所有節點,按Delete鍵全部刪除.

WX20171106-225812@2x.png

07-Cameras攝像機

新增攝像機

開啟GameViewController.swift,在setupNodes()中新增下面一行:

scnScene.rootNode.addChildNode(game.hudNode)
複製程式碼

然後,在renderer(_,updateAtTime)中新增一行:

game.updateHUD()
複製程式碼

選中Game.scn,以顯示編輯器. 在左下角點選 + 按鈕,建立一個空的節點預設命名為untitled.將其改名為Cameras.

WX20171108-215639@2x.png

從右下角的物件庫中拖放兩個Camera節點到場景中.

WX20171108-215828@2x.png

分別命名為VerticalCameraHorizontalCamera.稍後會講為什麼需要兩個攝像機.

TL/DR:雙攝像機能讓你更好地處理橫屏與豎屏狀態下的視角.

讓兩個攝像機都成為Cameras的子節點:

WX20171108-221039@2x.png

選中VerticalCamera,在節點檢查器中設定Position(x:0, y:22, z:9),Euler(x:-70, y:0, z:0)

WX20171108-221410@2x.png

選中HorizontalCamera,在節點檢查器中設定Position(x:0, y:8.5, z:15),Euler(x:-40, y:0, z:0)

WX20171108-221819@2x.png

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

WX20171108-221912@2x.png

GameViewController.swift中新增兩個屬性:

 var horizontalCameraNode: SCNNode!
  var verticalCameraNode: SCNNode!
複製程式碼

setupNodes()方法的開頭新增下面程式碼:

horizontalCameraNode = scnScene.rootNode.childNode(withName:
"HorizontalCamera", recursively: true)!
verticalCameraNode = scnScene.rootNode.childNode(withName:
"VerticalCamera", recursively: true)!
複製程式碼

因為場景已經載入進來了,所以我們只需要用childNode(withName:recursively:)方法來找到攝像機節點就可以了.recursively設定為true會遞迴遍歷其中的子資料夾.

處理旋轉

設定在旋轉時,螢幕的顯示範圍也在跟著變.與其在兩個方向中找到"sweet-spot",倒不如使用兩個攝像機,每一個都可以最大化利用顯示範圍.

WX20171108-223028@2x.png

為了追蹤裝置方向,需要重寫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:)來執行切換方向的程式碼.
  2. 根據從UIDevice.current().orientation中獲取到的deviceOrientation來切換方向.如果將要切換到.portrait,則設定視點為verticalCameraNode.否則,切換視點到horizontalCameraNode.

執行一下:

WX20171108-223615@2x.png

08-Lights燈光

新增小球

選中Game.scn.在物件庫中,拖放一個Sphere到場景中.

WX20171108-223931@2x.png

確保球體節點仍處於選中狀態,然後選擇節點檢查器.將Name命名為Ball,將position設定為0,這樣球就在正中間了.

WX20171108-230307@2x.png

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

WX20171108-230522@2x.png

兩種球體sphere和geosphere本質上是同樣的.不同的是下面的geodesic核取方塊,決定了渲染引擎如何構建球體.一種是四邊形,一種是三角形.

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

WX20171108-230913@2x.png

繼續向下,找到Setting區域,將Shininess改為0.3.

WX20171108-231032@2x.png

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

WX20171108-231153@2x.png

下面,開啟GameViewController.swift,新增一個屬性:

var ballNode: SCNNode!
複製程式碼

setupNodes()末尾新增下面的程式碼:

 ballNode = scnScene.rootNode.childNode(withName: "Ball", recursively:true)!
複製程式碼
三點光照

首先,開啟Game.scn,點選 + 建立一個空節點,命名為Lights.它將用來盛放場景中的所有燈光.

WX20171109-212629@2x.png

從物件庫中,拖放一個Omni light到場景中,放到燈光節點下面.

WX20171109-213018@2x.png

選中燈光節點,開啟節點檢查器,重新命名節點為Back.設定Position(x:-15, y:-2, z:15)

WX20171109-213223@2x.png

選擇Attributes Inspector,設定泛光燈屬性.

WX20171109-213336@2x.png

再從物件庫中拖放一個Omni light光源到場景中.還是移動到Lights組節點下.

命名新節點為Front,設定Position(x:6, y:10, z:15).

WX20171109-213612@2x.png

再從物件庫中拖放一個Ambient light光源到場景中.還是移動到Lights組節點下.

WX20171109-220913@2x.png

命名新節點為Ambient,設定Position(x:0, y:0, z:0).

WX20171109-221045@2x.png

開啟屬性檢查器:

WX20171109-221205@2x.png

完成後的場景效果:

WX20171109-221251@2x.png

執行一下,效果如下:

WX20171109-221341@2x.png

09-Geometric Shapes幾何形狀

建立邊框

選擇Game.scn,點選 + 按鈕新增一個空白節點,命名為Barriers. 這將是用來盛放所有的邊框節點的:

WX20171109-224809@2x.png

從物件庫中,拖放一個Box,在場景樹中,將新的立方體節點拖放到Barriers組節點下面.

WX20171109-224937@2x.png

開啟節點檢查器,命名為Top,設定位置為 (x:0,y:0,z:-10.5).開屬性檢查器,設定Sizewidth:13, height:2, length:1,設定Chamfer radius0.3. 開啟

WX20171111-110146@2x.png
材料檢查器,將Diffuse改為暗灰色Hex Color333333,並將Specular改為White:
WX20171109-231133@2x.png

WX20171111-105642@2x.png

下面我們通過複製的方式來建立底部的邊框. 複製方法是:按住Option鍵,點選要複製的節點並沿著藍色座標軸拖動:

WX20171111-110434@2x.png

複製成功後,重新命名為Bottom,將設定為Barriers組的子節點.

WX20171111-110514@2x.png

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

WX20171111-113425@2x.png

最終效果,如圖:

WX20171111-113510@2x.png

還有一個重要的事:注意場景樹的結構,組節點是如何包含頂邊框/底邊框的. 選中新複製出的節點的Attributes Inspector屬性檢查器,在Geometry Sharing區下面,點選Unshare按鈕.

因為建立複本時,複製出的節點仍然會共享原始節點的幾何體(Geometry).這個預設設定是為了減少總的繪製呼叫(draw call)數.

左側邊框的建立

左右兩側的邊框分別由兩根圓柱組成.先在Barriers組下面建立一個Left節點,並放置到合適的位置.裡面的子節點也會跟著發生位置變動.

WX20171111-115817@2x.png

WX20171111-115849@2x.png

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

WX20171111-120053@2x.png
WX20171111-120123@2x.png

在節點檢查器中,設定Position(x:0, y:0.5, z:0),Euler(x:90, y:0, z:0).

屬性檢查器中,設定Radius0.3,Height22.5.

材料檢查器中,設定DiffuseHex Color #B3B3B3 ,SpecularWhite:

WX20171111-120335@2x.png
WX20171111-120655@2x.png
WX20171111-120713@2x.png

建立左邊框的下半部分 選中Barrier/Left/Top節點,按住Option鍵,沿藍色座標軸,點選拖動.重新命名為Bottom,放在Barriers/Left組下面.在節點檢查器中,設定Position(x:0,y:-0.5,z:0):

WX20171111-125653@2x.png
WX20171111-125915@2x.png
WX20171111-125939@2x.png

最終效果如圖:

WX20171111-125954@2x.png

建立右側邊框

選中Barriers/Left組,按住Command+Option並沿紅色座標軸點選拖動,這樣就複製了一組節點.重新命名為Right,並設定位置為 (x:6, y:0, z:0)

WX20171111-130404@2x.png
WX20171111-130443@2x.png
WX20171111-130454@2x.png

最終效果如圖:

WX20171111-130609@2x.png

建立球拍擋板

點選 + 按鈕建立新的節點,命名為Paddle.開啟節點檢查器,設定Position(x:0, y:0, z:8).

WX20171111-132831@2x.png
WX20171111-132841@2x.png

球拍擋板共有三個部分:左,中,右. 我們先建立中間部分,拖放一個圓柱體,命名為Center,放在Paddle組節點下面.

WX20171111-133129@2x.png
WX20171111-133141@2x.png

開啟節點檢查器,設定Position0,設定Euler(x:0, y:0, z:90).

開啟屬性檢查器,設定Radius0.25, Height1.5.

開啟材料檢查器,設定DiffuseHex Color #333333, SpecularWhite.

WX20171111-133213@2x.png
WX20171111-133225@2x.png
WX20171111-133239@2x.png

建立左側部分

拖放一個圓柱體,命名為Left,放在Paddle組節點下面.

WX20171111-133904@2x.png

設定Position為**(x:-1, y:0, z:0)**, Euler(x:0, y:0, z:90).

開啟屬性檢查器,設定Radius0.25, Height0.5.

開啟材料檢查器,設定DiffuseHex Color #666666, SpecularWhite.

WX20171111-134208@2x.png
WX20171111-134218@2x.png
WX20171111-134235@2x.png

複製右側部分 選中Paddle/Left節點,按住Command+Option並沿綠色座標軸點選拖動,這樣就複製了一組節點.重新命名為Right,並設定位置為**(x:1, y:0, z:0)**.還是要注意取消幾何體共享.

WX20171111-141015@2x.png
WX20171111-141028@2x.png

繫結球拍擋板,以便操作

開啟GameViewController.swift,新增屬性:

var paddleNode: SCNNode!
複製程式碼

setupNodes()方法的末尾,新增繫結球拍的程式碼:

 paddleNode =
  scnScene.rootNode.childNode(withName: "Paddle", recursively: true)!
複製程式碼

你可以在本章對應程式碼的projects/final/Breaker資料夾下,找到最終的完成版專案.

新增磚塊,挑戰專案
  • 首先,建立一個組節點命名為Bricks,用來放置所有的磚塊.

  • 設定Bricks節點的位置為 (x:0, y:0, z:-3.0).

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

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

    WX20171111-142007@2x.png

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

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

  • 複製更多列出來.(按住OptionCommand)

  • 複製時,記得使用材料檢查器下面的Unshare按鈕,以免改變了原始節點的顏色.

  • 複製填滿整個區域.

最終效果如圖:

WX20171111-142642@2x.png

執行程式

WX20171111-142655@2x.png

你可以在本章對應程式碼的projects/challenge/Breaker資料夾下,找到最終的完成版專案.

10-Basic Collision Detection碰撞檢測基礎

物理效果

先給小球新增物理效果. 開啟Game.scn並選中Ball.開啟Physics Inspector物理效果檢查器.將Physics BodyType改為Dynamic. 並按下圖設定各個專案:

WX20171111-143239@2x.png

給邊框新增物理效果 一次性選中左右邊框的四個部分,可以有兩種方法:

  1. 按住Command在場景樹中點選每個節點.
  2. 類似於資料夾多選操作,先選中Top節點,按住Shift,點選Right,兩者之間的節點會被全部選中.
    WX20171111-143739@2x.png

保持選中狀態,開啟物理效果檢查器,在Physics Body區域,將Type改為Static,在新展開的設定項裡按下圖設定:

WX20171111-143930@2x.png

點選工具條上的播放按鈕,就可以預覽物理效果:

WX20171111-144621@2x.png

接著給磚塊新增物理效果 全選磚塊節點:

WX20171111-144805@2x.png

設定為Static形體,其餘如下圖:

WX20171111-144821@2x.png

給球拍擋板新增物理效果 選中球拍三個節點,開啟物理效果檢查器,設定TypeKinematic,其餘專案設定如下:

WX20171111-150415@2x.png
WX20171111-150430@2x.png

執行一下,小球會瘋狂地到處碰撞,包括與球拍的碰撞:

WX20171111-151240@2x.png

碰撞檢測

碰撞檢測用到的是SCNPhysicsContactDelegate協議. 開啟GameViewController.swift,新增一個新屬性:

var lastContactNode: SCNNode!
複製程式碼

它的作用有兩個:

  1. 當兩個節點發生互相滑動時,就相當於和同一個節點不停發生碰撞,而我們只關心第一次碰撞.
  2. 在這個遊戲中,儘管碰撞可能會持續,但小球不能和同一個節點兩次發生接觸事件,直到小球碰到了其它節點.所以我們需要確保只處理一次碰撞.

GameViewController.swift底部新增類擴充套件:

// 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. 擴充套件GameViewController類以實現SCNPhysicsContactDelegate協議,方便組織程式碼.
  2. 實現physicsWorld(_:didBegin:).預設不觸發,需要設定接觸掩碼.
  3. 傳入一個SCNPhysicsContact引數,可以判斷並找到哪個是小球.
  4. 防止和同一個節點多次碰撞.

使用位掩碼來檢測接觸事件. 我們已經給遊戲中的不同元素設定了Category bitmask分類掩碼,這個值是二進位制的,各分類如下:

Ball:     1 (Decimal) = 00000001 (Binary)
Barrier:  2 (Decimal) = 00000010 (Binary)
Brick:    4 (Decimal) = 00000100 (Binary)
Paddle:   8 (Decimal) = 00001000 (Binary)
複製程式碼

GameViewController頂部定義一個列舉:

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
複製程式碼

這樣,你就告訴了物理引擎,當小球和分類掩碼為2, 4, 8的節點碰撞時,呼叫physicsWorld(_:didBegin:)方法通知我. 2,4,8也就是指barrier邊框, brick磚塊和paddle球拍.

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

// 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來判斷小球是不是和邊框節點碰撞了.再根據名字判斷,如果是和底部邊框碰撞,則需要扣掉一個生命值.
  2. 檢查並判斷小球是不是和磚塊碰撞了.讓對應磚塊消失120秒,再皇親出現,這樣遊戲就能一直玩下去.
  3. 判斷小球是不是和球拍碰撞了.如果遇到了中間部分,不改變物理效果,由引擎自動控制反彈.如果是碰到了左邊或右邊,則給小球增加一個20度的水平偏轉.
  4. 將小球速度強制限制在5,以防物理引擎出現偏差而失控.

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

scnScene.physicsWorld.contactDelegate = self
複製程式碼

執行一下,可以打掉磚塊了!

WX20171111-160202@2x.png

觸控控制球拍

GameViewController新增兩個屬性:

 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. 當觸控位置移動時,根據相對初始觸控位置的偏移touchX來更新球拍的位置.
  2. 限制球拍的移動,確保在邊框之間.

執行一下,可以來回移動球拍了:

WX20171111-163506@2x.png

攝像機追蹤

touchesMoved(_:with:)方法的底部,新增下面程式碼,讓攝像機水平位置和球拍一致:

 verticalCameraNode.position.x = paddleNode.position.x
 horizontalCameraNode.position.x = paddleNode.position.x
複製程式碼

GameViewController中新增一個新屬性來依舊在地板節點:

var floorNode: SCNNode!
複製程式碼

setupNodes()底部新增程式碼:

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

這段程式碼含義:找到名為Floor的節點,繫結到floorNode.給場景中的兩個攝像機新增SCNLookAtConstraint約束,能讓攝像機始終對準目標節點,也就是遊戲區域的中央.

可以執行試玩一下了:

WX20171111-164815@2x.png

粒子效果

選中場景Game.scn.從物件庫中拖放一個Particle System粒子系統到場景中,命名為Trail,並放在Ball節點中

WX20171111-165921@2x.png
:
WX20171111-165743@2x.png

開啟節點檢查器,設定position(x:0, y:0, z:0).

WX20171111-170628@2x.png

開啟屬性檢查器,配置粒子系統的屬性:

WX20171111-171334@2x.png

完成後,點選播放按鈕預覽一下:

WX20171111-171439@2x.png

正式執行一下,可以玩起來了!

WX20171111-171501@2x.png

該部分最終完成的專案,放在程式碼中對應章節的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")
複製程式碼

可以在碰撞的時候,播放對應的音效:

  1. 使用game.playSound(node: scnScene.rootNode, name: "SoundToPlay")來播放已載入好的音效.
  2. Block新增音效時使用隨機值,用random() % 3來產生0~2的隨機數.

最終完成的專案,放在程式碼中對應章節的projects/challenge/Breaker資料夾裡.

相關文章