說明
本系列文章是對(duì)<3D Apple Games by Tutorials>一書的學(xué)習(xí)記錄和體會(huì)此書對(duì)應(yīng)的代碼地址
更多iOS相關(guān)知識(shí)查看github上WeekWeekUpProject
06-SceneKit Editor場(chǎng)景編輯器
創(chuàng)建游戲
打開Xcode,創(chuàng)建一個(gè)新項(xiàng)目,選擇iOS/Application/Game模板.
游戲名Breaker,語(yǔ)言選Swift,游戲技術(shù)SceneKit,設(shè)備支持Universal,取消勾選兩個(gè)測(cè)試選項(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) {
}
}
代碼含義:
- 在
viewDidLoad()
里調(diào)用一些空的占位方法.稍后,我們會(huì)向這些方法里添加代碼. - 在創(chuàng)建場(chǎng)景方法里將
self.view
轉(zhuǎn)換為SCNView
對(duì)象并儲(chǔ)存起來以便訪問,記self
成為渲染循環(huán)的代理. -
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 Mode為Aspect Fit,并添加約束,讓它處在屏幕中間:
完成后:
下面還需要添加音效.找到resources/Breaker.scnassets文件夾,拖放到時(shí)項(xiàng)目中.注意選中Copy items if needed, Create groups及目標(biāo)項(xiàng)目Breaker.這里面有子文件夾,Sounds和Textures分別是音頻和紋理圖片.
還需要一些游戲工具類.拖放resources/GameUtil到項(xiàng)目中.
打開GameViewController.swift,在scnView
下面添加屬性:
var game = GameHelper.sharedInstance
加載場(chǎng)景
右擊Breaker.scnassets,創(chuàng)建一個(gè)新文件夾命名為Scenes,用來盛放所有場(chǎng)景.
選中Breaker項(xiàng)目,創(chuàng)建新文件,選擇iOS/Resource/ SceneKit Scene模板,命名為Game.scn.注意位置選擇在Breaker.scnassets下面的Scenes文件夾下面.
從右下角的物體對(duì)象庫(kù)中拖拽一個(gè)Box出來,隨便放在場(chǎng)景中:
在GameViewController
中添加一個(gè)新屬性:
var scnScene: SCNScene!
接下來,在setupScene()
方法的底部,添加下面代碼:
scnScene = SCNScene(named: "Breaker.scnassets/Scenes/Game.scn")
scnView.scene = scnScene
運(yùn)行一下:
測(cè)試完成后,就可以刪除立方體了.在左側(cè)的場(chǎng)景樹中,按Command-A選擇所有節(jié)點(diǎn),按Delete鍵全部刪除.
07-Cameras攝像機(jī)
添加攝像機(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.
從右下角的對(duì)象庫(kù)中拖放兩個(gè)Camera節(jié)點(diǎn)到場(chǎng)景中.
分別命名為VerticalCamera和HorizontalCamera.稍后會(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).
對(duì)比來看,水平攝像機(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)閳?chǎng)景已經(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
}
}
代碼含義:
- 重寫
viewWillTransition(to:with:)
來運(yùn)行切換方向的代碼. - 根據(jù)從
UIDevice.current().orientation
中獲取到的deviceOrientation
來切換方向.如果將要切換到.portrait
,則設(shè)置視點(diǎn)為verticalCameraNode
.否則,切換視點(diǎn)到horizontalCameraNode
.
運(yùn)行一下:
08-Lights燈光
添加小球
選中Game.scn.在對(duì)象庫(kù)中,拖放一個(gè)Sphere到場(chǎng)景中.
確保球體節(jié)點(diǎn)仍處于選中狀態(tài),然后選擇節(jié)點(diǎn)檢查器.將Name命名為Ball,將position設(shè)置為0,這樣球就在正中間了.
接著打開屬性檢查器.將Radius改為0.25, Segment count為17.
兩種球體sphere和geosphere本質(zhì)上是同樣的.不同的是下面的geodesic復(fù)選框,決定了渲染引擎如何構(gòu)建球體.一種是四邊形,一種是三角形.
下一步,選中材料檢查器.將Diffuse改為7F7F7F.將Specular改為White.
繼續(xù)向下,找到Setting區(qū)域,將Shininess改為0.3.
完成后,選中HorizontalCamera,場(chǎng)景看起來是這樣:
下面,打開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.它將用來盛放場(chǎng)景中的所有燈光.
從對(duì)象庫(kù)中,拖放一個(gè)Omni light到場(chǎng)景中,放到燈光節(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òu)膶?duì)象庫(kù)中拖放一個(gè)Omni light光源到場(chǎng)景中.還是移動(dòng)到Lights組節(jié)點(diǎn)下.
命名新節(jié)點(diǎn)為Front,設(shè)置Position為(x:6, y:10, z:15).
再?gòu)膶?duì)象庫(kù)中拖放一個(gè)Ambient light光源到場(chǎng)景中.還是移動(dòng)到Lights組節(jié)點(diǎn)下.
命名新節(jié)點(diǎn)為Ambient,設(shè)置Position為(x:0, y:0, z:0).
打開屬性檢查器:
完成后的場(chǎng)景效果:
運(yùn)行一下,效果如下:
09-Geometric Shapes幾何形狀
創(chuàng)建邊框
選擇Game.scn,點(diǎn)擊+按鈕添加一個(gè)空白節(jié)點(diǎn),命名為Barriers.
這將是用來盛放所有的邊框節(jié)點(diǎn)的:
從對(duì)象庫(kù)中,拖放一個(gè)Box,在場(chǎng)景樹中,將新的立方體節(jié)點(diǎn)拖放到Barriers組節(jié)點(diǎn)下面.
打開節(jié)點(diǎn)檢查器,命名為Top,設(shè)置位置為(x:0,y:0,z:-10.5).開屬性檢查器,設(shè)置Size為width:13, height:2, length:1,設(shè)置Chamfer radius為0.3.
打開
材料檢查器,將Diffuse改為暗灰色Hex Color為333333,并將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è)重要的事:注意場(chǎng)景樹的結(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è)置Radius為0.3,Height為22.5.
材料檢查器中,設(shè)置Diffuse為Hex Color #的B3B3B3 ,Specular為White:
建立左邊框的下半部分
選中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è)置Position為0,設(shè)置Euler為(x:0, y:0, z:90).
打開屬性檢查器,設(shè)置Radius為0.25, Height為1.5.
打開材料檢查器,設(shè)置Diffuse為Hex Color #的333333, Specular為White.
創(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è)置Radius為0.25, Height為0.5.
打開材料檢查器,設(shè)置Diffuse為Hex Color #的666666, Specular為White.
復(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)!
你可以在本章對(duì)應(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ù)制更多列出來.(按住Option和Command)
復(fù)制時(shí),記得使用材料檢查器下面的Unshare按鈕,以免改變了原始節(jié)點(diǎn)的顏色.
復(fù)制填滿整個(gè)區(qū)域.
最終效果如圖:
運(yùn)行程序
你可以在本章對(duì)應(yīng)代碼的projects/challenge/Breaker文件夾下,找到最終的完成版項(xiàng)目.
10-Basic Collision Detection碰撞檢測(cè)基礎(chǔ)
物理效果
先給小球添加物理效果.
打開Game.scn并選中Ball.打開Physics Inspector物理效果檢查器.將Physics Body的Type改為Dynamic.
并按下圖設(shè)置各個(gè)項(xiàng)目:
給邊框添加物理效果
一次性選中左右邊框的四個(gè)部分,可以有兩種方法:
- 按住Command在場(chǎng)景樹中點(diǎn)擊每個(gè)節(jié)點(diǎn).
- 類似于文件夾多選操作,先選中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è)置Type為Kinematic,其余項(xiàng)目設(shè)置如下:
運(yùn)行一下,小球會(huì)瘋狂地到處碰撞,包括與球拍的碰撞:
碰撞檢測(cè)
碰撞檢測(cè)用到的是SCNPhysicsContactDelegate協(xié)議.
打開GameViewController.swift,添加一個(gè)新屬性:
var lastContactNode: SCNNode!
它的作用有兩個(gè):
- 當(dāng)兩個(gè)節(jié)點(diǎn)發(fā)生互相滑動(dòng)時(shí),就相當(dāng)于和同一個(gè)節(jié)點(diǎn)不停發(fā)生碰撞,而我們只關(guān)心第一次碰撞.
- 在這個(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
}
}
代碼含義:
- 擴(kuò)展
GameViewController
類以實(shí)現(xiàn)SCNPhysicsContactDelegate
協(xié)議,方便組織代碼. - 實(shí)現(xiàn)
physicsWorld(_:didBegin:)
.默認(rèn)不觸發(fā),需要設(shè)置接觸掩碼. - 傳入一個(gè)
SCNPhysicsContact
參數(shù),可以判斷并找到哪個(gè)是小球. - 防止和同一個(gè)節(jié)點(diǎn)多次碰撞.
使用位掩碼來檢測(cè)接觸事件.
我們已經(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
代碼含義:
- 檢查
categoryBitMask
來判斷小球是不是和邊框節(jié)點(diǎn)碰撞了.再根據(jù)名字判斷,如果是和底部邊框碰撞,則需要扣掉一個(gè)生命值. - 檢查并判斷小球是不是和磚塊碰撞了.讓對(duì)應(yīng)磚塊消失120秒,再皇親出現(xiàn),這樣游戲就能一直玩下去.
- 判斷小球是不是和球拍碰撞了.如果遇到了中間部分,不改變物理效果,由引擎自動(dòng)控制反彈.如果是碰到了左邊或右邊,則給小球增加一個(gè)
20
度的水平偏轉(zhuǎn). - 將小球速度強(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
}
}
}
代碼含義:
- 當(dāng)觸摸位置移動(dòng)時(shí),根據(jù)相對(duì)初始觸摸位置的偏移
touchX
來更新球拍的位置. - 限制球拍的移動(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
.給場(chǎng)景中的兩個(gè)攝像機(jī)添加SCNLookAtConstraint
約束,能讓攝像機(jī)始終對(duì)準(zhǔn)目標(biāo)節(jié)點(diǎn),也就是游戲區(qū)域的中央.
可以運(yùn)行試玩一下了:
粒子效果
選中場(chǎng)景Game.scn.從對(duì)象庫(kù)中拖放一個(gè)Particle System粒子系統(tǒng)到場(chǎ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)目,放在代碼中對(duì)應(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í)候,播放對(duì)應(yīng)的音效:
- 使用
game.playSound(node: scnScene.rootNode, name: "SoundToPlay")
來播放已加載好的音效. - 給Block添加音效時(shí)使用隨機(jī)值,用
random() % 3
來產(chǎn)生0~2的隨機(jī)數(shù).
最終完成的項(xiàng)目,放在代碼中對(duì)應(yīng)章節(jié)的projects/challenge/Breaker文件夾里.