[SceneKit專題]3D打磚塊游戲Breaker

說明

本系列文章是對(duì)<3D Apple Games by Tutorials>一書的學(xué)習(xí)記錄和體會(huì)此書對(duì)應(yīng)的代碼地址

SceneKit系列文章目錄

更多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) {
  }
}

代碼含義:

  1. viewDidLoad()里調(diào)用一些空的占位方法.稍后,我們會(huì)向這些方法里添加代碼.
  2. 在創(chuàng)建場(chǎng)景方法里將self.view轉(zhuǎn)換為SCNView對(duì)象并儲(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)拖放到里面去.

WX20171106-215541@2x.png

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

WX20171106-221817@2x.png

完成后:


WX20171106-221905@2x.png

下面還需要添加音效.找到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
加載場(chǎng)景

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

WX20171106-222712@2x.png

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

WX20171106-222942@2x.png

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

WX20171106-225141@2x.png

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

var scnScene: SCNScene!

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

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

運(yùn)行一下:


WX20171106-225545@2x.png

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

WX20171106-225812@2x.png

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.

WX20171108-215639@2x.png

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

WX20171108-215828@2x.png

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

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

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

WX20171108-221039@2x.png

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

WX20171108-221410@2x.png

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

WX20171108-221819@2x.png

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


WX20171108-221912@2x.png

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è)都可以最大化利用顯示范圍.


WX20171108-223028@2x.png

為了追蹤設(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)行一下:


WX20171108-223615@2x.png

08-Lights燈光

添加小球

選中Game.scn.在對(duì)象庫(kù)中,拖放一個(gè)Sphere到場(chǎng)景中.

WX20171108-223931@2x.png

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

WX20171108-230307@2x.png

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

WX20171108-230522@2x.png

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

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

WX20171108-230913@2x.png

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

WX20171108-231032@2x.png

完成后,選中HorizontalCamera,場(chǎng)景看起來是這樣:

WX20171108-231153@2x.png

下面,打開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)景中的所有燈光.

WX20171109-212629@2x.png

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

WX20171109-213018@2x.png

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

WX20171109-213223@2x.png

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

WX20171109-213336@2x.png

再?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).

WX20171109-213612@2x.png

再?gòu)膶?duì)象庫(kù)中拖放一個(gè)Ambient light光源到場(chǎng)景中.還是移動(dòng)到Lights組節(jié)點(diǎn)下.

WX20171109-220913@2x.png

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

WX20171109-221045@2x.png

打開屬性檢查器:


WX20171109-221205@2x.png

完成后的場(chǎng)景效果:


WX20171109-221251@2x.png

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


WX20171109-221341@2x.png

09-Geometric Shapes幾何形狀

創(chuàng)建邊框

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

WX20171109-224809@2x.png

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

WX20171109-224937@2x.png

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

WX20171111-110146@2x.png

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

WX20171111-105642@2x.png

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

WX20171111-110434@2x.png

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

WX20171111-110514@2x.png

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

WX20171111-113425@2x.png

最終效果,如圖:


WX20171111-113510@2x.png

還有一個(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).

WX20171111-115817@2x.png

WX20171111-115849@2x.png

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

WX20171111-120053@2x.png

WX20171111-120123@2x.png

在節(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:

WX20171111-120335@2x.png

WX20171111-120655@2x.png

WX20171111-120713@2x.png

建立左邊框的下半部分
選中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):

WX20171111-125653@2x.png

WX20171111-125915@2x.png

WX20171111-125939@2x.png

最終效果如圖:


WX20171111-125954@2x.png

建立右側(cè)邊框

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

WX20171111-130404@2x.png

WX20171111-130443@2x.png

WX20171111-130454@2x.png

最終效果如圖:


WX20171111-130609@2x.png
創(chuàng)建球拍擋板

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

WX20171111-132831@2x.png

WX20171111-132841@2x.png

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

WX20171111-133129@2x.png

WX20171111-133141@2x.png

打開節(jié)點(diǎn)檢查器,設(shè)置Position0,設(shè)置Euler(x:0, y:0, z:90).
打開屬性檢查器,設(shè)置Radius0.25, Height1.5.
打開材料檢查器,設(shè)置DiffuseHex Color #333333, SpecularWhite.

WX20171111-133213@2x.png

WX20171111-133225@2x.png

WX20171111-133239@2x.png

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

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

WX20171111-133904@2x.png

設(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.

WX20171111-134208@2x.png

WX20171111-134218@2x.png

WX20171111-134235@2x.png

復(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).還是要注意取消幾何體共享.

WX20171111-141015@2x.png

WX20171111-141028@2x.png

綁定球拍擋板,以便操作

打開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):


    WX20171111-142007@2x.png
  • 為了方便定位,白色磚塊可以放置在(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ū)域.

最終效果如圖:


WX20171111-142642@2x.png

運(yùn)行程序


WX20171111-142655@2x.png

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

10-Basic Collision Detection碰撞檢測(cè)基礎(chǔ)

物理效果

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

WX20171111-143239@2x.png

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

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

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

WX20171111-143930@2x.png

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


WX20171111-144621@2x.png

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


WX20171111-144805@2x.png

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

WX20171111-144821@2x.png

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

WX20171111-150415@2x.png

WX20171111-150430@2x.png

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


WX20171111-151240@2x.png
碰撞檢測(cè)

碰撞檢測(cè)用到的是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)多次碰撞.

使用位掩碼來檢測(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

代碼含義:

  1. 檢查categoryBitMask來判斷小球是不是和邊框節(jié)點(diǎn)碰撞了.再根據(jù)名字判斷,如果是和底部邊框碰撞,則需要扣掉一個(gè)生命值.
  2. 檢查并判斷小球是不是和磚塊碰撞了.讓對(duì)應(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)行一下,可以打掉磚塊了!


WX20171111-160202@2x.png
觸摸控制球拍

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ù)相對(duì)初始觸摸位置的偏移touchX來更新球拍的位置.
  2. 限制球拍的移動(dòng),確保在邊框之間.

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


WX20171111-163506@2x.png
攝像機(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)行試玩一下了:


WX20171111-164815@2x.png
粒子效果

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

WX20171111-165921@2x.png

:
WX20171111-165743@2x.png

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

WX20171111-170628@2x.png

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


WX20171111-171334@2x.png

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


WX20171111-171439@2x.png

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


WX20171111-171501@2x.png

該部分最終完成的項(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)的音效:

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末届搁,一起剝皮案震驚了整個(gè)濱河市搬味,隨后出現(xiàn)的幾起案子伪阶,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異痹栖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瞭空,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門揪阿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咆畏,你說我怎么就攤上這事南捂。” “怎么了旧找?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵溺健,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我钮蛛,道長(zhǎng)鞭缭,這世上最難降的妖魔是什么剖膳? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮岭辣,結(jié)果婚禮上潮秘,老公的妹妹穿的比我還像新娘。我一直安慰自己易结,他們只是感情好枕荞,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搞动,像睡著了一般躏精。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹦肿,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天矗烛,我揣著相機(jī)與錄音,去河邊找鬼箩溃。 笑死瞭吃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涣旨。 我是一名探鬼主播歪架,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼霹陡!你這毒婦竟也來了和蚪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤烹棉,失蹤者是張志新(化名)和其女友劉穎攒霹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浆洗,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡催束,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伏社。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抠刺。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖洛口,靈堂內(nèi)的尸體忽然破棺而出矫付,到底是詐尸還是另有隱情凯沪,我是刑警寧澤第焰,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站妨马,受9級(jí)特大地震影響挺举,放射性物質(zhì)發(fā)生泄漏杀赢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一湘纵、第九天 我趴在偏房一處隱蔽的房頂上張望脂崔。 院中可真熱鬧,春花似錦梧喷、人聲如沸砌左。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)汇歹。三九已至,卻和暖如春偿凭,著一層夾襖步出監(jiān)牢的瞬間产弹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工弯囊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痰哨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓匾嘱,卻偏偏與公主長(zhǎng)得像斤斧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子霎烙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345