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

說明

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

此書對應的代碼地址git

SceneKit系列文章目錄github

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

06-SceneKit Editor場景編輯器

建立遊戲

打開Xcode,建立一個新項目,選擇iOS/Application/Game模板. 遊戲名Breaker,語言選Swift,遊戲技術SceneKit,設備支持Universal,取消勾選兩個測試選項.app

打開項目,刪除art.scnassets文件夾.並將GameViewController.swift中的內容替換爲下面:dom

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.將圖標拖放到裏面去.ide

WX20171106-215541@2x.png

選中Assets.xcassets,拖放resources/Logo_Diffuse.png到裏面.而後打開LaunchScreen.storyboard,將背景顏色改成深藍色.在右下角的Media Library中找到Logo_Diffuse,拖放到啓動屏幕裏.設置圖片的Content ModeAspect Fit,並添加約束,讓它處在屏幕中間: 工具

WX20171106-221817@2x.png

完成後: post

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文件夾裏.

相關文章
相關標籤/搜索