[SceneKit專題]3D平衡球游戲Marble Maze

說(shuō)明

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

SceneKit系列文章目錄

更多iOS相關(guān)知識(shí)查看github上WeekWeekUpProject

11-Materials材質(zhì)

創(chuàng)建項(xiàng)目
  1. 打開(kāi)Xcode,創(chuàng)建一個(gè)新的iOS版SceneKit游戲項(xiàng)目,命名為MarbleMaze.
  2. 刪除art.scnassets文件夾.
  3. resources文件夾中拖拽一個(gè)新的art.scnassets到項(xiàng)目中.
  4. 我們只使用豎屏模式,所以取消Landscape LeftLandscape Right來(lái)禁用旋轉(zhuǎn):
    WX20171113-211135.png

替換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
      scnView.allowsCameraControl = true
      scnView.showsStatistics = true
}
  func setupNodes() {
  }
  func setupSounds() {
  }
  override var shouldAutorotate : Bool { return false }
  override var prefersStatusBarHidden : Bool { return true }
}
// 3
extension GameViewController: SCNSceneRendererDelegate {
  func renderer(_ renderer: SCNSceneRenderer,
    updateAtTime time: TimeInterval) {
  }
}

代碼含義:

  1. viewDidLoad()中調(diào)用這些空的方法;稍后會(huì)向其中添加代碼.
  2. self.view轉(zhuǎn)換為SCNView并保存下來(lái).并設(shè)置self為渲染循環(huán)的代理.
  3. 實(shí)現(xiàn)SCNSceneRendererDelegate協(xié)議中的方法.
天空盒子,加載場(chǎng)景

art.scnassets中找到空的game.scn場(chǎng)景文件.打開(kāi)并選中默認(rèn)的camera node,然后選中右上方的Scene Inspector.從右下方的媒體庫(kù)中找到img_skybox.jpg拖拽到場(chǎng)景的背景屬性上.:

WX20171113-213854@2x.png

GameViewController類(lèi)中添加下面屬性:

var scnScene:SCNScene!

setupScene()中添加下面代碼:

// 1
scnScene = SCNScene(named: "art.scnassets/game.scn")
// 2
scnView.scene = scnScene

運(yùn)行一下,看看神圣的天空景象:


WX20171113-213918@2x.png

12-Reference Nodes引用節(jié)點(diǎn)

主角--小球

拖拽一個(gè)空的SceneKit場(chǎng)景文件到你的項(xiàng)目,放到art.scnassets中,命名為obj_ball.scn:

WX20171113-220906@2x.png

選中art.scnassets/obj_ball.scn,展開(kāi)場(chǎng)景樹(shù),選中默認(rèn)的攝像機(jī)節(jié)點(diǎn).所有的新建場(chǎng)景都包含一個(gè)默認(rèn)的攝像機(jī)節(jié)點(diǎn),但作為引用節(jié)點(diǎn)被使用時(shí)就很不爽,所以我們刪除它:

WX20171113-220953@2x.png

下面開(kāi)始創(chuàng)建木質(zhì)小球.從對(duì)象庫(kù)中拖拽一個(gè)球體到場(chǎng)景中:


WX20171113-221006@2x.png

打開(kāi)節(jié)點(diǎn)檢查器.將小球命名為ball,放置位置為(x:0, y:0, z:0):

WX20171113-221018@2x.png

現(xiàn)在的小球太大了.打開(kāi)屬性檢查器,更改半徑為0.45,提升分段數(shù)為36來(lái)讓它顯得更圓一些:

WX20171113-221030@2x.png

材質(zhì)設(shè)置

漫反射設(shè)置


WX20171113-222847@2x.png

法線設(shè)置


WX20171113-222907@2x.png

高光設(shè)置


WX20171113-222929@2x.png

反射設(shè)置


WX20171113-222944@2x.png

發(fā)光設(shè)置


WX20171113-222959@2x.png

隨著各個(gè)貼圖的添加,效果漸變?nèi)缦?


WX20171113-223012@2x.png

然后需要做的是將小球作為引用節(jié)點(diǎn)添加到場(chǎng)景中去.
選中art.scnassets/game.scn,然后拖拽art.scnassets/obj_ball.scn到場(chǎng)景中.設(shè)置位置為(x:0, y:0, z:0)并命名為ball:

WX20171113-223051@2x.png

這樣,小球就作為一個(gè)引用節(jié)點(diǎn)被添加到場(chǎng)景中了.

運(yùn)行一下:


WX20171113-223103@2x.png
挑戰(zhàn)--創(chuàng)建木箱,小石塊,大石塊,柱子的引用節(jié)點(diǎn)

這是一個(gè)小小的挑戰(zhàn):

  1. 為每個(gè)對(duì)象創(chuàng)建一個(gè)空的場(chǎng)景.
  2. 刪除默認(rèn)的攝像機(jī).
    試著創(chuàng)建下面的對(duì)象:


    WX20171114-205653@2x.png
  • obj_crate1x1:命名為crate并設(shè)置尺寸為(x:1, y:1, z:1).使用img_crate_diffuse紋理作為漫反射貼圖,img_crate_normal作為法線貼圖.高光顏色設(shè)為中灰色;如果設(shè)為純白色,木箱會(huì)看起來(lái)像塑料的.
  • obj_stone1x1:命名為stone并設(shè)置尺寸為(x:1, y:1, z:1).使用img_stone_diffuseimg_stone_normal紋理作為貼圖,將法線intensity改為0.5. 設(shè)置高光色為White.
  • obj_stone3x3:命名為stone并設(shè)置尺寸為(x:3, y:3, z:3).紋理設(shè)置同上,高光仍為White.但是需要使用紋理縮放設(shè)置,及WrapT和WrapS來(lái)使其生效.
  • obj_pillar1x3:命名為pillar并設(shè)置尺寸為(x:1, y:3, z:1).使用img_pillar_*紋理;還有高光紋理也要用上.還有應(yīng)用縮放及wrap設(shè)置.

當(dāng)設(shè)置3x3方塊時(shí),可參照下面步驟:


WX20171114-205708@2x.png

WX20171114-205724@2x.png

設(shè)置過(guò)程中,會(huì)看到如下的依次變化:


WX20171114-205738@2x.png

最終完成版在12-Reference Nodes中的projects/ challenge/MarbleMaze/文件夾.

13-Shadows陰影

組織場(chǎng)景

選中art.scnassets/game.scn.組織一下場(chǎng)景樹(shù)如下:

WX20171114-221728@2x.png

創(chuàng)建一個(gè)空節(jié)點(diǎn)命名為follow_camera:

WX20171114-221750@2x.png

camera節(jié)點(diǎn)放到follow_camera下,成為它的子節(jié)點(diǎn),并設(shè)置位置為(x:0, y:0, z:5),旋轉(zhuǎn)為(x:0, y:0, z:0):

WX20171114-221800@2x.png

創(chuàng)建另一個(gè)空節(jié)點(diǎn)命名為follow_light:

WX20171114-221816@2x.png

添加幾個(gè)空節(jié)點(diǎn)作為占位節(jié)點(diǎn),設(shè)置位置為零;

  • pearls:待收集的珍珠分組.
  • section1, section2, section3, section4:這些分組用來(lái)盛放本關(guān)卡的不同章節(jié).

創(chuàng)建最后一個(gè)空節(jié)點(diǎn),命名為static_light:

WX20171114-221830@2x.png

燈光

首先是固定燈光
拖拽一個(gè)泛光燈和一個(gè)環(huán)境光到場(chǎng)景中,并按順序放置在static_lights組節(jié)點(diǎn)中:

WX20171114-221948@2x.png

選中omni light,打開(kāi)節(jié)點(diǎn)檢查器,命名為omni,位置,角度設(shè)為零:

WX20171114-222002@2x.png

打開(kāi)屬性檢查器,設(shè)置顏色為深灰色:


WX20171114-222017@2x.png

選中ambient light,打開(kāi)節(jié)點(diǎn)檢查器:

WX20171114-225122@2x.png

打開(kāi)屬性檢查器,設(shè)置顏色為深灰:


WX20171114-225145@2x.png

查看一下場(chǎng)景中的小球:


WX20171114-225209@2x.png

接著添加跟隨燈光
拖拽一個(gè)聚光燈到場(chǎng)景中,放置在follow_light組節(jié)點(diǎn)下面:

WX20171114-225230@2x.png

選中聚光燈,打開(kāi)它的節(jié)點(diǎn)檢查器,設(shè)置位置如下:


WX20171114-225241@2x.png

這個(gè)燈光是follow_light的子節(jié)點(diǎn), follow_light的位置是(x:0, y:0, z:0),旋轉(zhuǎn)角度(x:-25, y:-45, z:0);

然后選中聚光燈,打開(kāi)屬性檢查器,設(shè)置金黃色模擬環(huán)境中的陽(yáng)光:


WX20171114-225308@2x.png

完成后的效果:


WX20171114-225335@2x.png
重用集合體

將游戲中重復(fù)出現(xiàn)的結(jié)構(gòu)做成重用集合體,方便在需要的時(shí)候直接調(diào)用.
此處我們制作的是休息點(diǎn),它由一塊3x3的石塊和上面的4根柱子組成.

拖拽一個(gè)空的SceneKit場(chǎng)景文件到項(xiàng)目的根目錄中,然后在彈出框中選擇art.scnassets,點(diǎn)擊Create按鈕.

WX20171115-142246@2x.png

拖拽一個(gè)obj_stone3x3.scn的引用節(jié)點(diǎn)到空?qǐng)鼍暗?放置在(x: 0, y: 0, z:0).

WX20171115-142301@2x.png

拖拽一個(gè)obj_pillar1x3.scn引用節(jié)點(diǎn)到時(shí)大石塊的頂部.設(shè)置位置在(x: -1, y: 3, z: 1),即右上角位置.

WX20171115-142314@2x.png

使用?? (Option +Command) +點(diǎn)擊拖拽,復(fù)制三個(gè)柱子,位置如下:

  • Top-Left. Positioned at (x: -1, y: 3, z: -1).
  • Top-Right. Positioned at (x: 1, y: 3, z: -1).
  • Bottom-Right. Positioned at (x: 1, y: 3, z: 1).


    WX20171115-142328@2x.png

記得刪除場(chǎng)景中默認(rèn)的攝像機(jī).

選中game.scn,然后拖放新創(chuàng)建的set_restpoint.scn到場(chǎng)景下方.位置設(shè)為(x: 0, y: -2, z: 0)

WX20171115-142341@2x.png

運(yùn)行一下,會(huì)看到漂亮的陰影:


WX20171115-142402@2x.png
創(chuàng)建其它部件

現(xiàn)在還需要?jiǎng)?chuàng)建幾個(gè)其他的集合體,以便在主場(chǎng)景中直接引用.
比如straight_bridge,用了7個(gè)stone1x1組成:

WX20171115-142418@2x.png

zigzag_bridge,用了stone1x1crate1x1方塊.共9格寬7格長(zhǎng).

WX20171115-142432@2x.png

然后就可以用這些來(lái)組成大場(chǎng)景:


WX20171115-142450@2x.png

從左下角開(kāi)始,放置一個(gè)restpoint休息點(diǎn)在地平面下,(x:0, y:0, z:0)處.然后將其他引用集合體拖拽到場(chǎng)景中.
注意將這些都放在section1下面,這是個(gè)游戲切換場(chǎng)景的小技巧:通過(guò)更改visible標(biāo)記就能控制整個(gè)場(chǎng)景的顯示與隱藏.

運(yùn)行一下,移動(dòng)攝像機(jī)看看,還可以旋轉(zhuǎn)視角,查看更漂亮的美景:


WX20171115-142516@2x.png

WX20171115-142531@2x.png

14-Intermediate Collision Detection中級(jí)碰撞檢測(cè)

拖拽一個(gè)空的SceneKit文件到項(xiàng)目中,命名為obj_pearl.scn,保存到art.scnassets文件夾:

WX20171115-154050@2x.png

接著從對(duì)象庫(kù)中拖放一個(gè)球體節(jié)點(diǎn)到新場(chǎng)景中:


WX20171115-154104@2x.png

節(jié)點(diǎn)檢查器中命名改為pearl,位置,角度為零.
屬性檢查器中,設(shè)置半徑為0.2,分段數(shù)為16:

WX20171115-154119@2x.png

WX20171115-161805@2x.png

接下來(lái)打開(kāi)材料檢查器,設(shè)置漫反射顏色為黑色,高光為白色.反射貼圖使用img_skybox.jpg,但將強(qiáng)度降為0.75:

WX20171115-161827@2x.png

完成后的效果圖:


WX20171115-161839@2x.png

還需要添加游戲工具類(lèi)

resources/ GameUtils/中拖拽GameUtils文件夾到項(xiàng)目中,如下圖,點(diǎn)擊Finish:

WX20171115-161854@2x.png

位掩碼(分類(lèi)掩碼,碰撞掩碼,接觸掩碼)

我們將采用如下的分類(lèi)位掩碼設(shè)置:


WX20171115-161946@2x.png

打開(kāi)GameViewController.swift,在開(kāi)頭添加分類(lèi)碼:

let CollisionCategoryBall = 1
let CollisionCategoryStone = 2
let CollisionCategoryPillar = 4
let CollisionCategoryCrate = 8
let CollisionCategoryPearl = 16

游戲中,我們想讓小球與除了能量珍珠外的所有物體碰撞,所以需要定義碰撞掩碼,來(lái)決定和哪些物體碰撞:


WX20171115-162001@2x.png

Stone石頭, Pillar柱子, Crate木箱和Pearl能量珍珠和碰撞掩碼都是1,就是說(shuō)它們能和分類(lèi)掩碼為1的物體碰撞,也就是都能和小球碰撞.而小球的碰撞掩碼是14:
CollisionMask = Stone + Pillar + Crate = 2 + 4 + 8 = 14

接觸掩碼決定了哪些物體碰撞時(shí),代理方法會(huì)被調(diào)用.


WX20171115-162017@2x.png

我們只關(guān)心小球和能量珍珠,柱子及木箱的碰撞,所以:
ContactMask = Pearl + Pillar + Crate = 16 + 8 + 4 = 28

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

var ballNode:SCNNode!

添加下列代碼到setupNodes()中:

ballNode = scnScene.rootNode.childNode(withName: "ball", recursively:
true)!
ballNode.physicsBody?.contactTestBitMask = CollisionCategoryPillar |
CollisionCategoryCrate | CollisionCategoryPearl
啟用物理效果

選中obj_ball.scn,然后選中ball節(jié)點(diǎn),打開(kāi)物理效果檢查器來(lái)將Physics Body類(lèi)型設(shè)置為Dynamic:

WX20171115-162043@2x.png

確保重力影響是打開(kāi)的,不然小球可能會(huì)漂在空中:


WX20171115-162056@2x.png

設(shè)置Category mask1,Collision mask14:

WX20171115-162112@2x.png

ShapeDefault shape,TypeConvex:

WX20171115-162906@2x.png

除了小球,其它物體都是不動(dòng)的,是靜態(tài)物理形體.設(shè)置如下:


WX20171115-162924@2x.png
  • obj_stone1x1.scnCategory mask2, Collision mask1;
    WX20171115-162938@2x.png
  • obj_stone3x3.scn: Category mask2, Collision mask1**.
  • obj_pillar1x3.scn: Category mask4,Collision mask1.
  • obj_crate1x1.scn: Category mask8, Collision mask1.
  • obj_pearl.scn: Category mask16, Collision mask-1.

對(duì)能量珍珠Physics shape設(shè)為Default shape, TypeConvex:

WX20171115-162959@2x.png

其余的Physics shape設(shè)為Default shape, TypeBounding Box:

WX20171115-163025@2x.png

添加碰撞檢測(cè)處理

現(xiàn)在終于設(shè)置好了各個(gè)物體,要處理相互的碰撞了.在GameViewController.swift底部:

extension GameViewController : SCNPhysicsContactDelegate {
  func physicsWorld(_ world: SCNPhysicsWorld,
    didBegin contact: SCNPhysicsContact) {
    // 1
    var contactNode:SCNNode!
    if contact.nodeA.name == "ball" {
      contactNode = contact.nodeB
    } else {
      contactNode = contact.nodeA
    }
// 2
    if contactNode.physicsBody?.categoryBitMask ==
      CollisionCategoryPearl {
    contactNode.isHidden = true
      contactNode.runAction(
        SCNAction.waitForDurationThenRunBlock(
          duration: 30) { (node:SCNNode!) -> Void in
        node.isHidden = false
      })
}
// 3
    if contactNode.physicsBody?.categoryBitMask ==
      CollisionCategoryPillar ||
        contactNode.physicsBody?.categoryBitMask ==
          CollisionCategoryCrate {
} }
}

代碼含義:

  1. 和前面一樣,用來(lái)判斷碰撞雙方哪一個(gè)是小球.
  2. 如果碰撞到的是參量珍珠,則消失30秒,然后重新出現(xiàn).
  3. 判斷小球是碰撞到了柱子還是木箱,可以添加音效.

并在setupScene()底部添加成為代理:

scnScene.physicsWorld.contactDelegate = self

還需要再添加一些小效果讓游戲更生動(dòng).
打開(kāi)obj_ball.scn,選中ball,設(shè)置y軸位置為10讓小球出現(xiàn)時(shí)有個(gè)掉落效果:

WX20171115-163047@2x.png

運(yùn)行一下,可以看到掉落下來(lái):


WX20171115-163106@2x.png

選中游戲場(chǎng)景,然后拖拽obj_pearl.scn到場(chǎng)景中.放置在(x: 0, y: 0, z: 0)處.放到pearls組下面:

WX20171115-163127@2x.png

運(yùn)行一下,小球掉落并吸收了能量珍珠:


WX20171115-163158@2x.png

還可以給場(chǎng)景中添加更多的能量珍珠,如下:


WX20171115-163217@2x.png

15-Motion Control運(yùn)動(dòng)控制

輔助類(lèi)和音效

在前面我們已經(jīng)添加了GameUtils類(lèi),現(xiàn)在還需要再添加一些東西以便使用它.

GameViewController中添加下面的屬性:

var game = GameHelper.sharedInstance
var motion = CoreMotionHelper()
var motionForce = SCNVector3(x:0 , y:0, z:0)

再?gòu)?strong>resources拖放Sounds文件夾到項(xiàng)目中:

WX20171118-214115@2x.png

setupSounds()中添加下面代碼:

game.loadSound(name: "GameOver", fileNamed: "GameOver.wav")
game.loadSound(name: "Powerup", fileNamed: "Powerup.wav")
game.loadSound(name: "Reset", fileNamed: "Reset.wav")
game.loadSound(name: "Bump", fileNamed: "Bump.wav")
節(jié)點(diǎn)綁定和狀態(tài)管理

GameViewController類(lèi)中添加下面的屬性:

var cameraNode:SCNNode!

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

// 1
cameraNode = scnScene.rootNode.childNode(withName: "camera",
  recursively: true)!
// 2
let constraint = SCNLookAtConstraint(target: ballNode)
cameraNode.constraints = [constraint]

代碼含義:

  1. 將游戲場(chǎng)景中的camera綁定到cameraNode.
  2. 給攝像機(jī)添加一個(gè)SCNLookAtConstraint約束,使其朝向ballNode.

當(dāng)攝像機(jī)有SCNLookAtConstraint約束時(shí),小球到處滾動(dòng),可能會(huì)導(dǎo)致攝像機(jī)向左或向右傾斜,所以我們需要在setupNodes()末尾打開(kāi)萬(wàn)向節(jié)鎖:

constraint.isGimbalLockEnabled = true

其它節(jié)點(diǎn)也需要同樣處理.在GameViewController類(lèi)中添加下列屬性:

 var cameraFollowNode:SCNNode!
var lightFollowNode:SCNNode!

setupNodes()末尾添加下列代碼:

// 1
cameraFollowNode = scnScene.rootNode.childNode(
  withName: "follow_camera", recursively: true)!
// 2
cameraNode.addChildNode(game.hudNode)
// 3
lightFollowNode = scnScene.rootNode.childNode(
  withName: "follow_light", recursively: true)!

游戲節(jié)點(diǎn)綁定完成,還需要處理游戲的狀態(tài).游戲需要三種基本狀態(tài):

  • waitForTap:游戲開(kāi)始前的狀態(tài)
  • playing:點(diǎn)擊屏幕開(kāi)始游戲的狀態(tài)
  • gameOver:能量用光或者掉落下平臺(tái)的狀態(tài).

GameViewController類(lèi)中添加下列代碼:

// 1
func playGame() {
  game.state = GameStateType.playing
  cameraFollowNode.eulerAngles.y = 0
  cameraFollowNode.position = SCNVector3Zero
}
// 2
func resetGame() {
  game.state = GameStateType.tapToPlay
  game.playSound(node: ballNode, name: "Reset")
  ballNode.physicsBody!.velocity = SCNVector3Zero
  ballNode.position = SCNVector3(x:0, y:10, z:0)
  cameraFollowNode.position = ballNode.position
  lightFollowNode.position = ballNode.position
  scnView.isPlaying = true
  game.reset()
}
// 3
func testForGameOver() {
  if ballNode.presentation.position.y < -5 {
    game.state = GameStateType.gameOver
    game.playSound(node: ballNode, name: "GameOver")
    ballNode.run(SCNAction.waitForDurationThenRunBlock(
      duration: 5) { (node:SCNNode!) -> Void in
        self.resetGame()
      })
} }

代碼含義:

  1. 切換到.playing狀態(tài),開(kāi)始游戲.以及基本的清理和重置.
  2. 切換到.waitForTap狀態(tài),播放音效,以及各種清理和重置工作.
  3. 檢查小球的位置,y值小于-5,則切換到.gameOver狀態(tài),播放音效.5秒后自動(dòng)調(diào)用resetGame(),并切換到.waitForTap狀態(tài).

還要在viewDidLoad()末尾添加調(diào)用:

resetGame()

游戲開(kāi)始時(shí),玩家需要點(diǎn)擊屏幕.因此在GameViewController類(lèi)中,添加下面的觸摸代碼:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
  if game.state == GameStateType.tapToPlay {
playGame() }
}

GameViewController類(lèi)中,添加下面的代碼:

func updateMotionControl() {
  // 1
  if game.state == GameStateType.playing {
    motion.getAccelerometerData(interval: 0.1) { (x,y,z) in
     self.motionForce = SCNVector3(x: Float(x) * 0.05, y:0,
       z: Float(y+0.8) * -0.05)
    }
// 2
    ballNode.physicsBody!.velocity += motionForce
  }
}

代碼含義:

  1. 根據(jù)當(dāng)前的運(yùn)動(dòng)數(shù)據(jù)更新motionForce向量.
  2. motionForce向量賦值給小球的velocity.

還需要在renderer(_, updateAtTime)方法中調(diào)用updateMotionControl()方法:

updateMotionControl()

運(yùn)行游戲,看到小球從空中落下,點(diǎn)擊屏幕開(kāi)始游戲:


WX20171118-214158@2x.png

WX20171118-214223@2x.png

小球身上的發(fā)光效果實(shí)際就是生命值,小球的發(fā)光強(qiáng)度將隨著時(shí)間不斷減弱直到降為0.0.如果收集到一個(gè)能量珍珠,則生命值恢復(fù)到1.0.我們需要一個(gè)方法來(lái)補(bǔ)充生命值.在GameViewController類(lèi)中,添加下面的代碼:

func replenishLife() {
  // 1
  let material = ballNode.geometry!.firstMaterial!
  // 2
  SCNTransaction.begin()
  SCNTransaction.animationDuration = 1.0
// 3
  material.emission.intensity = 1.0
// 4
  SCNTransaction.commit()
  // 5
  game.score += 1
  game.playSound(node: ballNode, name: "Powerup")
}
  1. 要獲取發(fā)光貼圖,就需要先獲取ballNode的firstMaterial.
  2. 通過(guò)SCNTransaction.begin()來(lái)開(kāi)始動(dòng)畫(huà).此處我們?cè)O(shè)置時(shí)長(zhǎng)為1秒animationDuration = 1.0.
  3. 設(shè)置發(fā)光強(qiáng)度為1.0.
  4. 提交動(dòng)畫(huà)事務(wù).提交后SceneKit將開(kāi)始執(zhí)行動(dòng)畫(huà),將發(fā)光強(qiáng)度從當(dāng)前值改為1.0
  5. 增加分?jǐn)?shù),播放音效.

該方法需要在剛變成.playing狀態(tài)時(shí)調(diào)用.在playGame()方法的末尾調(diào)用:

  replenishLife()

有了恢復(fù)生命值的方法,還需要逐漸減少的方法.在GameViewController類(lèi)中,添加下面的代碼:

func diminishLife() {
  // 1
  let material = ballNode.geometry!.firstMaterial!
  // 2
  if material.emission.intensity > 0 {
    material.emission.intensity -= 0.001
  } else {
    resetGame()
  }
}

我們需要在每次檢查.gameOver狀態(tài)時(shí)調(diào)用這個(gè)方法.

攝像機(jī)和燈光

GameViewController類(lèi)中,添加下面的代碼:

func updateCameraAndLights() {
  // 1
  let lerpX = (ballNode.presentation.position.x -
    cameraFollowNode.position.x) * 0.01
  let lerpY = (ballNode.presentation.position.y -
    cameraFollowNode.position.y) * 0.01
  let lerpZ = (ballNode.presentation.position.z -
    cameraFollowNode.position.z) * 0.01
  cameraFollowNode.position.x += lerpX
  cameraFollowNode.position.y += lerpY
  cameraFollowNode.position.z += lerpZ
  // 2
  lightFollowNode.position = cameraFollowNode.position
// 3
  if game.state == GameStateType.tapToPlay {
      cameraFollowNode.eulerAngles.y += 0.005
  }
}

代碼含義:

  1. 用線性插值法計(jì)算要移動(dòng)的位置.創(chuàng)造出一種特殊的減速移動(dòng)效果.
  2. lightFollowNode節(jié)點(diǎn)跟隨攝像機(jī)節(jié)點(diǎn).
  3. 當(dāng)進(jìn)入.tapToPlay狀態(tài)時(shí),將攝像機(jī)抬起一些.

這個(gè)函數(shù)需要在renderer(_, updateAtTime)的末尾調(diào)用,這樣才能在每幀都能實(shí)時(shí)更新攝像機(jī)和燈光:

updateCameraAndLights()

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


WX20171118-214308@2x.png

點(diǎn)擊屏幕,開(kāi)始游戲:


WX20171118-214324@2x.png

游戲已經(jīng)基本完成,還需要處理一下HUD的顯示問(wèn)題,以及生命值耗盡的問(wèn)題.

GameViewController類(lèi)中,添加下面的代碼:

func updateHUD() {
  switch game.state {
  case .playing:
    game.updateHUD()
  case .gameOver:
    game.updateHUD(s: "-GAME OVER-")
  case .tapToPlay:
    game.updateHUD(s: "-TAP TO PLAY-")
  }
}

renderer(_, updateAtTime)方法的末尾,添加調(diào)用:

updateHUD()
WX20171118-214346@2x.png

現(xiàn)在生命值耗盡,游戲也不會(huì)結(jié)束,只有掉落下去才會(huì)死.我們需要處理耗盡問(wèn)題.在renderer(_, updateAtTime)方法的末尾,添加代碼:

if game.state == GameStateType.playing {
  testForGameOver()
  diminishLife()
}

還需要處理小球與能量珍珠碰撞時(shí),珍珠消失但小球的生命值沒(méi)有增加的問(wèn)題.只需要在physicsWorld(_, didBeginContact)里處理與珍珠的碰撞代碼塊中,調(diào)用replenishLife()就行了:

replenishLife()

添加碰撞音效,在physicsWorld(_, didBeginContact)里處理與柱子/木箱的碰撞代碼塊中,調(diào)用播放音效就行了:

game.playSound(node: ballNode, name: "Bump")

最后一步,移除setupScene()中的調(diào)試代碼:

 //scnView.allowsCameraControl = true
 //scnView.showsStatistics = true

最終的完成版代碼,在15-Motion Control中的projects/final/ MarbleMaze/文件夾下.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,746評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,991評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,706評(píng)論 6 413
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 56,036評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,203評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,451評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,677評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,857評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,266評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,606評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,407評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,643評(píng)論 2 380