[SceneKit專題]22-3D平衡球遊戲Marble-Maze

說明

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

此書對應的代碼地址git

SceneKit系列文章目錄github

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

11-Materials材質

建立項目
  1. 打開Xcode,建立一個新的iOS版SceneKit遊戲項目,命名爲MarbleMaze.
  2. 刪除art.scnassets文件夾.
  3. resources文件夾中拖拽一個新的art.scnassets到項目中.
  4. 咱們只使用豎屏模式,因此取消Landscape LeftLandscape Right來禁用旋轉:
    WX20171113-211135.png

替換GameViewController.swift中的內容:app

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) {
  }
}
複製代碼

代碼含義:ide

  1. viewDidLoad()中調用這些空的方法;稍後會向其中添加代碼.
  2. self.view轉換爲SCNView並保存下來.並設置self爲渲染循環的代理.
  3. 實現SCNSceneRendererDelegate協議中的方法.
天空盒子,加載場景

art.scnassets中找到空的game.scn場景文件.打開並選中默認的camera node,而後選中右上方的Scene Inspector.從右下方的媒體庫中找到img_skybox.jpg拖拽到場景的背景屬性上.: 函數

WX20171113-213854@2x.png

GameViewController類中添加下面屬性:工具

var scnScene:SCNScene!
複製代碼

setupScene()中添加下面代碼:post

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

運行一下,看看神聖的天空景象: 學習

WX20171113-213918@2x.png

12-Reference Nodes引用節點

主角--小球

拖拽一個空的SceneKit場景文件到你的項目,放到art.scnassets中,命名爲obj_ball.scn:

WX20171113-220906@2x.png

選中art.scnassets/obj_ball.scn,展開場景樹,選中默認的攝像機節點.全部的新建場景都包含一個默認的攝像機節點,但做爲引用節點被使用時就很不爽,因此咱們刪除它:

WX20171113-220953@2x.png

下面開始建立木質小球.從對象庫中拖拽一個球體到場景中:

WX20171113-221006@2x.png

打開節點檢查器.將小球命名爲ball,放置位置爲**(x:0, y:0, z:0)**:

WX20171113-221018@2x.png

如今的小球太大了.打開屬性檢查器,更改半徑爲0.45,提高分段數爲36來讓它顯得更圓一些:

WX20171113-221030@2x.png

材質設置

漫反射設置

WX20171113-222847@2x.png

法線設置

WX20171113-222907@2x.png

高光設置

WX20171113-222929@2x.png

反射設置

WX20171113-222944@2x.png

發光設置

WX20171113-222959@2x.png

隨着各個貼圖的添加,效果漸變以下:

WX20171113-223012@2x.png

而後須要作的是將小球做爲引用節點添加到場景中去. 選中art.scnassets/game.scn,而後拖拽art.scnassets/obj_ball.scn到場景中.設置位置爲**(x:0, y:0, z:0)** 並命名爲ball:

WX20171113-223051@2x.png

這樣,小球就做爲一個引用節點被添加到場景中了.

運行一下:

WX20171113-223103@2x.png

挑戰--建立木箱,小石塊,大石塊,柱子的引用節點

這是一個小小的挑戰:

  1. 爲每一個對象建立一個空的場景.
  2. 刪除默認的攝像機. 試着建立下面的對象:
    WX20171114-205653@2x.png
  • obj_crate1x1:命名爲crate並設置尺寸爲 (x:1, y:1, z:1).使用img_crate_diffuse紋理做爲漫反射貼圖,img_crate_normal做爲法線貼圖.高光顏色設爲中灰色;若是設爲純白色,木箱會看起來像塑料的.
  • obj_stone1x1:命名爲stone並設置尺寸爲 (x:1, y:1, z:1).使用img_stone_diffuseimg_stone_normal紋理做爲貼圖,將法線intensity改成0.5. 設置高光色爲White.
  • obj_stone3x3:命名爲stone並設置尺寸爲 (x:3, y:3, z:3).紋理設置同上,高光仍爲White.可是須要使用紋理縮放設置,及WrapT和WrapS來使其生效.
  • obj_pillar1x3:命名爲pillar並設置尺寸爲 (x:1, y:3, z:1).使用img_pillar_ 紋理;還有高光紋理也要用上.還有應用縮放及wrap設置.

當設置3x3方塊時,可參照下面步驟:

WX20171114-205708@2x.png
WX20171114-205724@2x.png

設置過程當中,會看到以下的依次變化:

WX20171114-205738@2x.png

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

13-Shadows陰影

組織場景

選中art.scnassets/game.scn.組織一下場景樹以下:

WX20171114-221728@2x.png

建立一個空節點命名爲follow_camera:

WX20171114-221750@2x.png

camera節點放到follow_camera下,成爲它的子節點,並設置位置爲 (x:0, y:0, z:5),旋轉爲 (x:0, y:0, z:0):

WX20171114-221800@2x.png

建立另外一個空節點命名爲follow_light:

WX20171114-221816@2x.png

添加幾個空節點做爲佔位節點,設置位置爲零;

  • pearls:待收集的珍珠分組.
  • section1, section2, section3, section4:這些分組用來盛放本關卡的不一樣章節.

建立最後一個空節點,命名爲static_light:

WX20171114-221830@2x.png

燈光

首先是固定燈光 拖拽一個泛光燈和一個環境光到場景中,並按順序放置在static_lights組節點中:

WX20171114-221948@2x.png

選中omni light,打開節點檢查器,命名爲omni,位置,角度設爲零:

WX20171114-222002@2x.png

打開屬性檢查器,設置顏色爲深灰色:

WX20171114-222017@2x.png

選中ambient light,打開節點檢查器:

WX20171114-225122@2x.png

打開屬性檢查器,設置顏色爲深灰:

WX20171114-225145@2x.png

查看一下場景中的小球:

WX20171114-225209@2x.png

接着添加跟隨燈光 拖拽一個聚光燈到場景中,放置在follow_light組節點下面:

WX20171114-225230@2x.png

選中聚光燈,打開它的節點檢查器,設置位置以下:

WX20171114-225241@2x.png

這個燈光是follow_light的子節點, follow_light的位置是 (x:0, y:0, z:0),旋轉角度 (x:-25, y:-45, z:0);

而後選中聚光燈,打開屬性檢查器,設置金黃色模擬環境中的陽光:

WX20171114-225308@2x.png

完成後的效果:

WX20171114-225335@2x.png

重用集合體

將遊戲中重複出現的結構作成重用集合體,方便在須要的時候直接調用. 此處咱們製做的是休息點,它由一塊3x3的石塊和上面的4根柱子組成.

拖拽一個空的SceneKit場景文件到項目的根目錄中,而後在彈出框中選擇art.scnassets,點擊Create按鈕.

WX20171115-142246@2x.png

拖拽一個obj_stone3x3.scn的引用節點到空場景的,放置在**(x: 0, y: 0, z:0)**.

WX20171115-142301@2x.png

拖拽一個obj_pillar1x3.scn引用節點到時大石塊的頂部.設置位置在**(x: -1, y: 3, z: 1)**,即右上角位置.

WX20171115-142314@2x.png

使用⌥⌘ (Option +Command) +點擊拖拽,複製三個柱子,位置以下:

  • 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

記得刪除場景中默認的攝像機.

選中game.scn,而後拖放新建立的set_restpoint.scn到場景下方.位置設爲 (x: 0, y: -2, z: 0)

WX20171115-142341@2x.png

運行一下,會看到漂亮的陰影:

WX20171115-142402@2x.png

建立其它部件

如今還須要建立幾個其餘的集合體,以便在主場景中直接引用. 好比straight_bridge,用了7個stone1x1組成:

WX20171115-142418@2x.png

zigzag_bridge,用了stone1x1crate1x1方塊.共9格寬7格長.

WX20171115-142432@2x.png

而後就能夠用這些來組成大場景:

WX20171115-142450@2x.png

從左下角開始,放置一個restpoint休息點在地平面下,(x:0, y:0, z:0) 處.而後將其餘引用集合體拖拽到場景中. 注意將這些都放在section1下面,這是個遊戲切換場景的小技巧:經過更改visible標記就能控制整個場景的顯示與隱藏.

運行一下,移動攝像機看看,還能夠旋轉視角,查看更漂亮的美景:

WX20171115-142516@2x.png
WX20171115-142531@2x.png

14-Intermediate Collision Detection中級碰撞檢測

拖拽一個空的SceneKit文件到項目中,命名爲obj_pearl.scn,保存到art.scnassets文件夾:

WX20171115-154050@2x.png

接着從對象庫中拖放一個球體節點到新場景中:

WX20171115-154104@2x.png

節點檢查器中命名改成pearl,位置,角度爲零. 屬性檢查器中,設置半徑爲0.2,分段數爲16:

WX20171115-154119@2x.png
WX20171115-161805@2x.png

接下來打開材料檢查器,設置漫反射顏色爲黑色,高光爲白色.反射貼圖使用img_skybox.jpg,但將強度降爲0.75:

WX20171115-161827@2x.png

完成後的效果圖:

WX20171115-161839@2x.png

還須要添加遊戲工具類

resources/ GameUtils/ 中拖拽GameUtils文件夾到項目中,以下圖,點擊Finish:

WX20171115-161854@2x.png

位掩碼(包括分類掩碼,碰撞掩碼,接觸掩碼)

咱們將採用以下的分類位掩碼設置:

WX20171115-161946@2x.png

打開GameViewController.swift,在開頭添加分類碼:

let CollisionCategoryBall = 1
let CollisionCategoryStone = 2
let CollisionCategoryPillar = 4
let CollisionCategoryCrate = 8
let CollisionCategoryPearl = 16
複製代碼

遊戲中,咱們想讓小球與除了能量珍珠外的全部物體碰撞,因此須要定義碰撞掩碼,來決定和哪些物體碰撞:

WX20171115-162001@2x.png

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

接觸掩碼決定了哪些物體碰撞時,代理方法會被調用.

WX20171115-162017@2x.png

咱們只關心小球和能量珍珠,柱子及木箱的碰撞,因此: ContactMask = Pearl + Pillar + Crate = 16 + 8 + 4 = 28

GameViewController.swift中,添加一個屬性:

var ballNode:SCNNode!
複製代碼

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

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

選中obj_ball.scn,而後選中ball節點,打開物理效果檢查器來將Physics Body類型設置爲Dynamic:

WX20171115-162043@2x.png

確保重力影響是打開的,否則小球可能會漂在空中:

WX20171115-162056@2x.png

設置Category mask1,Collision mask14:

WX20171115-162112@2x.png

ShapeDefault shape,TypeConvex:

WX20171115-162906@2x.png

除了小球,其它物體都是不動的,是靜態物理形體.設置以下:

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**.

對能量珍珠Physics shape設爲Default shape, TypeConvex:

WX20171115-162959@2x.png

其他的Physics shape設爲Default shape, TypeBounding Box:

WX20171115-163025@2x.png

添加碰撞檢測處理

如今終於設置好了各個物體,要處理相互的碰撞了.在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. 和前面同樣,用來判斷碰撞雙方哪個是小球.
  2. 若是碰撞到的是參量珍珠,則消失30秒,而後從新出現.
  3. 判斷小球是碰撞到了柱子仍是木箱,能夠添加音效.

並在setupScene()底部添加成爲代理:

scnScene.physicsWorld.contactDelegate = self
複製代碼

還須要再添加一些小效果讓遊戲更生動. 打開obj_ball.scn,選中ball,設置y軸位置爲10讓小球出現時有個掉落效果:

WX20171115-163047@2x.png

運行一下,能夠看到掉落下來:

WX20171115-163106@2x.png

選中游戲場景,而後拖拽obj_pearl.scn到場景中.放置在**(x: 0, y: 0, z: 0)處.放到pearls**組下面:

WX20171115-163127@2x.png

運行一下,小球掉落並吸取了能量珍珠:

WX20171115-163158@2x.png

還能夠給場景中添加更多的能量珍珠,以下:

WX20171115-163217@2x.png

15-Motion Control運動控制

輔助類和音效

在前面咱們已經添加了GameUtils類,如今還須要再添加一些東西以便使用它.

GameViewController中添加下面的屬性:

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

再從resources拖放Sounds文件夾到項目中:

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")
複製代碼
節點綁定和狀態管理

GameViewController類中添加下面的屬性:

var cameraNode:SCNNode!
複製代碼

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

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

代碼含義:

  1. 將遊戲場景中的camera綁定到cameraNode.
  2. 給攝像機添加一個SCNLookAtConstraint約束,使其朝向ballNode.

當攝像機有SCNLookAtConstraint約束時,小球處處滾動,可能會致使攝像機向左或向右傾斜,因此咱們須要在setupNodes()末尾打開萬向節鎖:

constraint.isGimbalLockEnabled = true
複製代碼

其它節點也須要一樣處理.在GameViewController類中添加下列屬性:

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)!
複製代碼

遊戲節點綁定完成,還須要處理遊戲的狀態.遊戲須要三種基本狀態:

  • waitForTap:遊戲開始前的狀態
  • playing:點擊屏幕開始遊戲的狀態
  • gameOver:能量用光或者掉落下平臺的狀態.

GameViewController類中添加下列代碼:

// 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狀態,開始遊戲.以及基本的清理和重置.
  2. 切換到.waitForTap狀態,播放音效,以及各類清理和重置工做.
  3. 檢查小球的位置,y值小於-5,則切換到.gameOver狀態,播放音效.5秒後自動調用resetGame(),並切換到.waitForTap狀態.

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

resetGame()
複製代碼

遊戲開始時,玩家須要點擊屏幕.所以在GameViewController類中,添加下面的觸摸代碼:

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

GameViewController類中,添加下面的代碼:

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. 根據當前的運動數據更新motionForce向量.
  2. motionForce向量賦值給小球的velocity.

還須要在renderer(_, updateAtTime)方法中調用updateMotionControl()方法:

updateMotionControl()
複製代碼

運行遊戲,看到小球從空中落下,點擊屏幕開始遊戲:

WX20171118-214158@2x.png
WX20171118-214223@2x.png

小球身上的發光效果實際就是生命值,小球的發光強度將隨着時間不斷減弱直到降爲0.0.若是收集到一個能量珍珠,則生命值恢復到1.0.咱們須要一個方法來補充生命值.在GameViewController類中,添加下面的代碼:

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. 要獲取發光貼圖,就須要先獲取ballNode的firstMaterial.
  2. 經過SCNTransaction.begin() 來開始動畫.此處咱們設置時長爲1秒animationDuration = 1.0.
  3. 設置發光強度爲1.0.
  4. 提交動畫事務.提交後SceneKit將開始執行動畫,將發光強度從當前值改成1.0
  5. 增長分數,播放音效.

該方法須要在剛變成.playing狀態時調用.在playGame()方法的末尾調用:

replenishLife()
複製代碼

有了恢復生命值的方法,還須要逐漸減小的方法.在GameViewController類中,添加下面的代碼:

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

咱們須要在每次檢查.gameOver狀態時調用這個方法.

攝像機和燈光

GameViewController類中,添加下面的代碼:

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. 用線性插值法計算要移動的位置.創造出一種特殊的減速移動效果.
  2. lightFollowNode節點跟隨攝像機節點.
  3. 當進入.tapToPlay狀態時,將攝像機擡起一些.

這個函數須要在renderer(_, updateAtTime)的末尾調用,這樣才能在每幀都能實時更新攝像機和燈光:

updateCameraAndLights()
複製代碼

運行一下,以下:

WX20171118-214308@2x.png

點擊屏幕,開始遊戲:

WX20171118-214324@2x.png

遊戲已經基本完成,還須要處理一下HUD的顯示問題,以及生命值耗盡的問題.

GameViewController類中,添加下面的代碼:

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)方法的末尾,添加調用:

updateHUD()
複製代碼

WX20171118-214346@2x.png

如今生命值耗盡,遊戲也不會結束,只有掉落下去纔會死.咱們須要處理耗盡問題.在renderer(_, updateAtTime)方法的末尾,添加代碼:

if game.state == GameStateType.playing {
  testForGameOver()
  diminishLife()
}
複製代碼

還須要處理小球與能量珍珠碰撞時,珍珠消失但小球的生命值沒有增長的問題.只須要在physicsWorld(_, didBeginContact)裏處理與珍珠的碰撞代碼塊中,調用replenishLife()就好了:

replenishLife()
複製代碼

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

game.playSound(node: ballNode, name: "Bump")
複製代碼

最後一步,移除setupScene()中的調試代碼:

//scnView.allowsCameraControl = true
 //scnView.showsStatistics = true
複製代碼

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

相關文章
相關標籤/搜索