24-體素風(fēng)格過馬路游戲Mr. Pig

文章選自掘金蘋果API搬運(yùn)工的文章[SceneKit專題]24-體素風(fēng)格過馬路游戲Mr. Pig
主要記錄自己在學(xué)習(xí)ARKit的過程中看到的好的文章晤锥,避免到時候鏈接失效無法找到原文的情況讼撒,非常感謝原博主的辛勤付出轧坎,也在此分享出來跟大家一起學(xué)習(xí)。

效果如下:


項目中用到的模型是用MagicaVoxel創(chuàng)建的,可以到ephtracy.github.io上去免費(fèi)下載.使用教程參見本系列其他文章.

創(chuàng)建項目

創(chuàng)建項目,選擇iOS > Application > Single View Application模板.

更改設(shè)置,只保留豎直方向:


添加資源文件: 拖拽resources/GameUtils/文件夾到項目中,選擇Group:

拖拽resources/MrPig.scnassets文件夾到項目中,選擇Create folder references:

完成后的效果:


添加應(yīng)用圖標(biāo)和啟動屏幕 在resources文件夾下找到LaunchScreenAppIcon文件夾,拖拽到對應(yīng)地方去:

給啟動屏幕添加圖片約束:


打開ViewController.swift,按下面作些修改:

// 1
import UIKit
import SceneKit
import SpriteKit
// 2
class ViewController: UIViewController {
// 3
  let game = GameHelper.sharedInstance
  override func viewDidLoad() {
    super.viewDidLoad()
    // 4
    setupScenes()
    setupNodes()
    setupActions()
    setupTraffic()
    setupGestures()
    setupSounds()
    // 5
    game.state = .tapToPlay
  }
  func setupScenes() {
  }
  func setupNodes() {
  }
  func setupActions() {
  }
  func setupTraffic() {
  }
  func setupGestures() {
  }
  func setupSounds() {
  }
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
  override var prefersStatusBarHidden : Bool { return true }
  override var shouldAutorotate : Bool { return false }
}

內(nèi)容簡單不做過多說明,導(dǎo)入SpriteKit是為了使用轉(zhuǎn)場功能.

繼續(xù)創(chuàng)建SceneKit View: 在ViewController的最上方添加:

var scnView: SCNView!

在setupScenes()中添加:

 scnView = SCNView(frame: self.view.frame)
 self.view.addSubview(scnView)

創(chuàng)建多場景

拖拽一個SceneKit Scene File到項目根目錄中,將其命名為GameScene.scn,放在MrPig.scnassets文件夾下:

選中MrPig.scnassets/GameScene.scn的同時,拖拽一個Floor Node到場景中,并選擇Node Inspector節(jié)點檢查器,將其命名為Grass,位置和旋轉(zhuǎn)角度設(shè)為0:

再設(shè)置屬性檢查器,將反射Floor Reflectivity設(shè)為0,因為草地并不需要反光:

移動到材質(zhì)檢查器,設(shè)置Material Diffuse貼圖,縮放設(shè)為12.5:

創(chuàng)建啟動閃屏

再拖拽一個SceneKit Scene File到項目根目錄中,將其命名為SplashScene.scn,放在MrPig.scnassets文件夾下:

選中MrPig.scnassets/SplashScene.scn的同時,從右下角的材質(zhì)庫中拖拽一個MrPig引用節(jié)點到場景中,打開其節(jié)點檢查器,將位置和旋轉(zhuǎn)設(shè)置為零:

接著打開場景檢查器,添加漸變背景Gradient_Diffuse.pngScene Background屬性:

為了讓背景更好看,設(shè)置出太陽般的放射光暈效果,需要拖拽一個Plane節(jié)點到場景中,命名為Rays,設(shè)置位置 (x:0, y:0.25, z:-1),可見度Visibility Opacity0.25:

打開屬性檢查器,設(shè)置Size(x:5, y:5),并設(shè)置圓角半徑Corner Radius2.5:

打開材質(zhì)檢查器Materials Inspector,將Lighting model設(shè)置為Constant,以避免光照對射線產(chǎn)生影響.

滾動到Settings區(qū)域并將Blend Mode混合模式設(shè)置為Subtract減弱:

設(shè)置攝像機(jī)和燈光

點擊場景樹下方的+號,添加一個空節(jié)點.命名為Camera,并將原始的攝像機(jī)節(jié)點移動過來作為子節(jié)點.選中Camera節(jié)點,打開節(jié)點檢查器,設(shè)置位置為(x:0, y:0.3, z:0)旋轉(zhuǎn)歐拉角為(x:-10, y:0, z:0).

選中內(nèi)層的camera節(jié)點,設(shè)置位置為(x:0, y:0, z:3)歐拉角為(x:0, y:0, z:0).

再添加一個空節(jié)點到根目錄中,命名為Lights,拖拽一個Ambient和一個Omni燈光到場景中:

修改omni燈光的位置,進(jìn)入節(jié)點檢查器,位置改為(x:5, y:5, z:5).


添加logo和點擊開始節(jié)點


  • The Logo node:使用Plane類型節(jié)點,MrPigLogo_Diffuse.png貼圖,設(shè)置尺寸為width:1, height:0.5,位置設(shè)置為 (x:0, y:1, z:0.5),注意不要受到燈光的影響,做法參考Rays節(jié)點.
  • The TapToPlay node:使用Plane類型節(jié)點,TapToPlay_Diffuse.png貼圖,設(shè)置尺寸為width:1, height:0.25,位置設(shè)置為 (x:0, y:-0.3, z:0.5),注意不要受到燈光的影響,做法參考Rays節(jié)點.

加載并展示閃屏界面

ViewController中添加屬性:

 var gameScene: SCNScene!
 var splashScene: SCNScene!

setupScenes() 中添加下列代碼:

// 1
gameScene = SCNScene(named: "/MrPig.scnassets/GameScene.scn")
splashScene = SCNScene(named: "/MrPig.scnassets/SplashScene.scn")
// 2
scnView.scene = splashScene

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


轉(zhuǎn)場

不同效果的轉(zhuǎn)場動畫前面已經(jīng)介紹過了. 在ViewController類中添加下面的代碼:

func startGame() {
  // 1
  splashScene.isPaused = true
  // 2
  let transition = SKTransition.doorsOpenVertical(withDuration: 1.0)
  // 3
  scnView.present(gameScene, with: transition, incomingPointOfView: nil, completionHandler: {
// 4
    self.game.state = .playing
    self.setupSounds()
    self.gameScene.isPaused = false

 })
}

繼續(xù)添加停止游戲和開啟閃屏的方法:

func stopGame() {
  game.state = .gameOver
  game.reset()
}
func startSplash() {
  // 1
  gameScene.isPaused = true
  // 2
  let transition = SKTransition.doorsOpenVertical(withDuration: 1.0)
  scnView.present(splashScene, with: transition, incomingPointOfView:
nil, completionHandler: {
    self.game.state = .tapToPlay
    self.setupSounds()
    self.splashScene.isPaused = false
  })
}

最后需要添加的方法是點擊開始:

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

運(yùn)行后,點擊屏幕開始游戲了.


添加主角

選中MrPig.scnassets/ GameScene.scn,拖拽一個MrPig引用節(jié)點到場景中,位置和旋轉(zhuǎn)設(shè)置為0:

setupNodes()中添加代碼:

 pigNode = gameScene.rootNode.childNode(withName: "MrPig", recursively:true)!

建立攝像機(jī)和燈光

回到MrPig.scnassets/GameScene.scn中,設(shè)置攝像機(jī). 在場景樹中點擊+號添加一個空節(jié)點,在節(jié)點檢查器中,將其命名為FollowCamera,位置為0,旋轉(zhuǎn) (x:-45, y:20, z:0).

將已經(jīng)存在的camera節(jié)點拖放到FollowCamera節(jié)點下.位置 (x:0, y:0, z:14),旋轉(zhuǎn)0.

選中ViewController.swift,添加一些屬性來控制攝像機(jī):

 var cameraNode: SCNNode!
 var cameraFollowNode: SCNNode!

setupNodes()中添加下面的代碼:

// 1
cameraNode = gameScene.rootNode.childNode(withName: "camera", recursively: true)!
cameraNode.addChildNode(game.hudNode)
// 2
cameraFollowNode = gameScene.rootNode.childNode(withName: "FollowCamera", recursively: true)!

代碼含義:

  1. 將cameraNode綁定到camera上,然后將hudNode添加上去作為子節(jié)點,這樣HUD就能一直顯示在鏡頭前了.
  2. 將cameraFollowNode綁定到FollowCamera上,這樣只需要更新其位置,攝像機(jī)就能一直跟隨著小豬了.

在場景樹中點擊+號添加一個空節(jié)點,在節(jié)點檢查器中,將其命名為FollowLight,位置為0,旋轉(zhuǎn)為0.拖拽一個Ambient light和一個Directional light到空節(jié)點中.

選中ambient燈光,位置和旋轉(zhuǎn)設(shè)置為0:

選擇屬性檢查器,設(shè)置顏色為Aluminum:

然后選中directional燈光,進(jìn)入節(jié)點檢查器,設(shè)置位置和旋轉(zhuǎn)如下:

進(jìn)入屬性檢查器,配置燈光和陰影屬性:


配置完成后預(yù)覽一下:


ViewController中添加屬性:

  var lightFollowNode: SCNNode!

setupNodes() 中添加下面代碼:

 lightFollowNode = gameScene.rootNode.childNode(withName: "FollowLight",recursively: true)!

添加高速公路和車輛

在場景樹中點擊+號添加一個空節(jié)點,在節(jié)點檢查器中,將其命名為Highway,拖拽兩個Road引用作為子節(jié)點.選中第一個,設(shè)置位置 (x:0, y:0, z:-4.5),旋轉(zhuǎn)為0:

選中第二個,位置為 (x:0, y:0, z:-11.5),旋轉(zhuǎn)為0:

點擊+號添加一個空節(jié)點,在節(jié)點檢查器中,將其命名為Traffic,拖拽一個Bus引用節(jié)點到其中,位置 (x:0, y:0, z:-4),旋轉(zhuǎn) (x:0, y:-90, z:0):

拖拽一個Mini引用節(jié)點到其中,位置 (x:3, y:0, z:-5),旋轉(zhuǎn) (x:0, y:-90, z:0):

拖拽一個SUV引用節(jié)點到其中,位置 (x:-3, y:0, z:-5),旋轉(zhuǎn) (x:0, y:-90, z:0):

選中ViewController.swift,添加屬性:

var trafficNode: SCNNode!

setupNodes()中添加代碼:

 trafficNode = gameScene.rootNode.childNode(withName: "Traffic", recursively: true)!

接著,需要復(fù)制一下達(dá)到下面的效果:


  • 左側(cè)車道是公交車道,右側(cè)車道是較小較快的車道;
  • 兩條公路,一條朝左開,一條朝右開;
  • 按住Option鍵來快速復(fù)制;
  • 按住Command鍵來與周圍元素對齊;
  • 兩車之間就留下足夠距離讓小豬能行;
  • 完成一條公路后,選中所有車輛,并按住Option拖拽到另一條公路上,就復(fù)制完成了,然后再掉轉(zhuǎn)180度;
  • 旋轉(zhuǎn)車輛時,按住Command鍵可以更方便地對齊;

完成后,運(yùn)行一下:


添加樹林

拖拽一個空的SceneKit Scene File,命名為TreeLine并放置在MrPig.scnassets目錄下:

TreeLine.scn中,創(chuàng)建一個空節(jié)點,命名為TreeLine,它將作為父容器節(jié)點:

按下面的圖來擺放各種樹木:


  • X:代表在x軸上的坐標(biāo);
  • Z:代表在z軸上的坐標(biāo);
  • S:代表小的樹SmallTree;
  • M:代表中等樹MediumTree;
  • L:代表大的樹LargeTree;

例如左上角第一個,放置一個SmallTree在位置 (x:-5, y:0, z:-1) 處.使用Option和Command鍵可以提高復(fù)制粘貼速度.

拖拽一個空的SceneKit Scene File,命名為TreePatch并放置在MrPig.scnassets目錄下:

TreePatch.scn中,創(chuàng)建一個空節(jié)點,命名為TreePatch,它將作為父容器節(jié)點:

按下面的圖來擺放各種樹木:


完成后:


回到MrPig.scnassets/GameScene.scn中,添加一個空節(jié)點,命名為Trees,作為樹林的容器節(jié)點:

按下圖來擺放樹林TreeLine:


前面部分,靠近Mr.Pig處樹林坐標(biāo)為:

  • Position:(x:0,y:0,z:7),Euler:(x:0,y:0,z:0).
  • Position:(x:-7, y:0, z:3), Euler: (x:0, y:90, z:0).
  • Position:(x:7,y:0,z:3),Euler:(x:0,y:90,z:0).
  • Position:(x:-14,y:0,z:-1),Euler:(x:0,y:0,z:0).
  • Position:(x:14,y:0,z:-1),Euler:(x:0,y:0,z:0).

公路中間處的坐標(biāo)為:

  • Position:(x:-14,y:0,z:-8),Euler:(x:0,y:0,z:0).
  • Position:(x:14,y:0,z:-8),Euler:(x:0,y:0,z:0).

后面部分的坐標(biāo)為:

  • Position:(x:18,y:0,z:-19),Euler:(x:0,y:90,z:0).
  • Position:(x:-18,y:0,z:-19),Euler:(x:0,y:90,z:0).
  • Position:(x::-11,y:0,z:-23),Euler:(x:0,y:0,z:0).
  • Position: (x:0, y:0, z:-23), Euler:(x:0, y:0, z:0).
  • Position:(x:11,y:0,z:-23),Euler:(x:0,y:0,z:0).

按下圖來擺放樹林TreePatch:


放置坐標(biāo)如下:

  • Position:(x:10,y:0,z:-17),Euler:(x:0,y:0,z:0).
  • Position:(x:-10,y:0,z:-17),Euler:(x:0,y:0,z:0).
  • Position:(x:0,y:0,z:-17),Euler:(x:0,y:90,z:0).

添加金幣

先創(chuàng)建一個空節(jié)點,命名為Coins,作為金幣的容器節(jié)點:

然后拖拽Coin引用節(jié)點到場景中,坐標(biāo)如下:

  • Position:(x:0,y:0.5,z:-8).
  • Position:(x:0,y:0.5,z:-21).
  • Position:(x:-14,y:0.5,z:-20).
  • Position:(x:14,y:0.5,z:-20).

完成后,效果如圖:

動作編輯器

背景射線動起來 打開MrPig.scnassets/SplashScene.scn,選中Rays.拖拽一個Rotate Action,在屬性檢查器中設(shè)置時長30,z軸360;

右鍵點擊,創(chuàng)建循環(huán),點擊

金幣動畫

選中MrPig.scnassets/Coin.scn,按順序拖拽兩個Move Action,再并排放置一個Rotate Action

選中第一個Move Action,設(shè)置Start Time0,Duration0.5.設(shè)置Timing FunctionEase In, Ease Out, 設(shè)置Offset(x:0, y:0.5, z:0).

選中第二個Move Action,設(shè)置Start Time0.5,Duration0.5.設(shè)置Timing FunctionEase In, Ease Out, 設(shè)置Offset(x:0, y:-0.5, z:0).

選中Move Action,設(shè)置Start Time0,Duration1.設(shè)置Timing FunctionLinear, 設(shè)置Euler Angle(x:0, y:360, z:0).

Shift選中全部,然后右鍵單擊,創(chuàng)建循環(huán)

讓閃屏頁面的小豬動起來 進(jìn)入MrPig.scnassets/SplashScreen.scn,選中MrPig,創(chuàng)建一系列旋轉(zhuǎn)動作:

  1. 連接7個Rotate Actions序列,設(shè)置時長為0.25s;

  2. 設(shè)置第一個旋轉(zhuǎn)動作,讓它沿x軸旋轉(zhuǎn)30度;


  3. 設(shè)置下一個,沿x軸旋轉(zhuǎn)-30度;


  4. 重復(fù)設(shè)置接下來的幾個動作,直到最后一個;

  5. 最后一個動作是沿y軸旋轉(zhuǎn)180度,讓小豬秀它的尾巴;


  6. 移動游標(biāo),預(yù)覽動作,會看到小豬搖頭晃腦,然后轉(zhuǎn)身給你看尾巴;

  7. 設(shè)置循環(huán)播放



運(yùn)行一下,查看動作:

代碼創(chuàng)建動作

交通動作 打開ViewController.swift,添加屬性:

 var driveLeftAction: SCNAction!
 var driveRightAction: SCNAction!

setupActions()中添加下面代碼:

driveLeftAction = SCNAction.repeatForever(SCNAction.move(by:
SCNVector3Make(-2.0, 0, 0), duration: 1.0))
driveRightAction = SCNAction.repeatForever(SCNAction.move(by:
SCNVector3Make(2.0, 0, 0), duration: 1.0))

setupTraffic()中添加下面代碼:

 // 1
for node in trafficNode.childNodes {
// 2 Buses are slow, the rest are speed demons
  if node.name?.contains("Bus") == true {
    driveLeftAction.speed = 1.0
    driveRightAction.speed = 1.0
  } else {
    driveLeftAction.speed = 2.0
    driveRightAction.speed = 2.0
  }
  // 3 Let vehicle drive towards its facing direction
  if node.eulerAngles.y > 0 {
    node.runAction(driveLeftAction)
} else {
    node.runAction(driveRightAction)
  }
}

運(yùn)行一下,車輛動起來了,但是開過去后就沒有了,這個問題我們稍后再處理:


添加小豬的動作

添加屬性:

var jumpLeftAction: SCNAction!
var jumpRightAction: SCNAction!
var jumpForwardAction: SCNAction!
var jumpBackwardAction: SCNAction!

setupActions()最下方中添加下面代碼:

// 1
let duration = 0.2
// 2
let bounceUpAction = SCNAction.moveBy(x: 0, y: 1.0, z: 0, duration: duration * 0.5)
let bounceDownAction = SCNAction.moveBy(x: 0, y: -1.0, z: 0, duration: duration * 0.5)
// 3
bounceUpAction.timingMode = .easeOut
bounceDownAction.timingMode = .easeIn
// 4
let bounceAction = SCNAction.sequence([bounceUpAction, bounceDownAction])
// 5
let moveLeftAction = SCNAction.moveBy(x: -1.0, y: 0, z: 0, duration: duration)
let moveRightAction = SCNAction.moveBy(x: 1.0, y: 0, z: 0, duration: duration)
let moveForwardAction = SCNAction.moveBy(x: 0, y: 0, z: -1.0, duration: duration)
let moveBackwardAction = SCNAction.moveBy(x: 0, y: 0, z: 1.0, duration: duration)
// 6
let turnLeftAction = SCNAction.rotateTo(x: 0, y: convertToRadians(angle: -90), z: 0, duration: duration, usesShortestUnitArc: true)
let turnRightAction = SCNAction.rotateTo(x: 0, y: convertToRadians(angle: 90), z: 0, duration: duration, usesShortestUnitArc: true)
let turnForwardAction = SCNAction.rotateTo(x: 0, y:
convertToRadians(angle: 180),  z: 0, duration: duration, usesShortestUnitArc: true)
let turnBackwardAction = SCNAction.rotateTo(x: 0, y:
convertToRadians(angle: 0),  z: 0, duration: duration, usesShortestUnitArc: true)
// 7
jumpLeftAction = SCNAction.group([turnLeftAction, bounceAction, moveLeftAction])
jumpRightAction = SCNAction.group([turnRightAction, bounceAction, moveRightAction])
jumpForwardAction = SCNAction.group([turnForwardAction, bounceAction, moveForwardAction])
jumpBackwardAction = SCNAction.group([turnBackwardAction, bounceAction, moveBackwardAction])

代碼含義:

  1. 定義時長;
  2. 向上,向下的彈簧效果;
  3. 修改時間模式,一個漸入,一個漸出;
  4. 創(chuàng)建bounceAction將向上和向下彈簧效果組成序列;
  5. SCNAction.moveBy(x:y:z:duration:)創(chuàng)建四個方向的移動動作;
  6. SCNAction.rotateTo(x:y:z:duration:usesShortestUnitArc:)創(chuàng)建四個方向的旋轉(zhuǎn)動作;
  7. 按順序組合出四個跳躍動作;

添加移動手勢

ViewController添加handleGesture(_:)方法:

// 1
func handleGesture(_ sender: UISwipeGestureRecognizer) {
  // 2
  guard game.state == .playing else {
return
}
// 3
  switch sender.direction {
    case UISwipeGestureRecognizerDirection.up:
      pigNode.runAction(jumpForwardAction)
    case UISwipeGestureRecognizerDirection.down:
      pigNode.runAction(jumpBackwardAction)
    case UISwipeGestureRecognizerDirection.left:
      if pigNode.position.x >  -15 {
        pigNode.runAction(jumpLeftAction)
      }
    case UISwipeGestureRecognizerDirection.right:
      if pigNode.position.x < 15 {
        pigNode.runAction(jumpRightAction)
      }
    default:
      break
} }

代碼含義:

  1. 定義一個手勢方法;
  2. 判斷游戲狀態(tài);
  3. 判斷手勢方向,左右限制不能超出范圍;

setupGestures()中添加下面代碼:

let swipeRight = UISwipeGestureRecognizer(target: self,
  action: #selector(ViewController.handleGesture(_:)))
swipeRight.direction = .right
scnView.addGestureRecognizer(swipeRight)
let swipeLeft = UISwipeGestureRecognizer(target: self,
  action: #selector(ViewController.handleGesture(_:)))
swipeLeft.direction = .left
scnView.addGestureRecognizer(swipeLeft)
let swipeForward = UISwipeGestureRecognizer(target: self,
  action: #selector(ViewController.handleGesture(_:)))
swipeForward.direction = .up
scnView.addGestureRecognizer(swipeForward)
let swipeBackward = UISwipeGestureRecognizer(target: self,
  action: #selector(ViewController.handleGesture(_:)))
swipeBackward.direction = .down
scnView.addGestureRecognizer(swipeBackward)

運(yùn)行一下,測試手勢控制:


設(shè)置游戲結(jié)束時的動作序列

ViewController添加一個屬性:

var triggerGameOver: SCNAction!

setupActions()的最底部添加代碼:

// 1
let spinAround = SCNAction.rotateBy(x: 0, y: convertToRadians(angle:
720), z: 0, duration: 2.0)
let riseUp = SCNAction.moveBy(x: 0, y: 10, z: 0, duration: 2.0)
let fadeOut = SCNAction.fadeOpacity(to: 0, duration: 2.0)
let goodByePig = SCNAction.group([spinAround, riseUp, fadeOut])
// 2
let gameOver = SCNAction.run { (node:SCNNode) -> Void in
  self.pigNode.position = SCNVector3(x:0, y:0, z:0)
  self.pigNode.opacity = 1.0
  self.startSplash()
}
// 3
triggerGameOver = SCNAction.sequence([goodByePig, gameOver])

代碼含義:

  1. 創(chuàng)建一些基本動作:一個旋轉(zhuǎn)720度,一個向上移動,一個逐漸透明;共同組成了一個動作組,叫goodByePig;
  2. SCNAction.runAction(_:)類方法允許我們插入一些邏輯代碼,在block中重設(shè)了小豬的位置,透明度,并觸發(fā)了startSplash()方法;
  3. 創(chuàng)建最終的triggerGameOver動作序列,先執(zhí)行goodByePig,再執(zhí)行gameOver;

stopGame()方法后面調(diào)用一下:

pigNode.runAction(triggerGameOver)

Advanced Collision Detection高級碰撞檢測

本章節(jié)解決以下問題:

  • 小豬遇到障礙時不能停止,如撞上樹木;
  • 小豬撞到汽車不能結(jié)束游戲;
  • 小豬無法真正收集金幣;

隱藏的碰撞檢測幾何體

這里我們用點小技巧來處理小豬與樹林的碰撞問題,使用四個隱藏的節(jié)點,當(dāng)左側(cè)節(jié)點與樹林碰撞時,就不能再向左移動了:


創(chuàng)建隱藏的碰撞節(jié)點

創(chuàng)建SceneKit Scene File到根目錄下,命名為Collision.scn,保存在MrPig.scnassets下:

選中Collision.scn,添加一個空節(jié)點,命名為Collision.

拖拽一個Box到場景中,放置在Collision節(jié)點下,命名為Front,位置 (x:0, y:0.25, z:-1).

進(jìn)入屬性檢查器,設(shè)置尺寸為 (x:0.25, y:0.25, z:0.25).

按住OptionCommand,拖拽出另外三個副本,選中一個命名為Back,位置設(shè)為 (x:0, y:0.25, z:1).

選中另一個,命名為Left,位置 (x:-1, y:0.25, z:0)

選中最后一個,命名為Right,位置 (x:1, y:0.25, z:0)

完成后的效果


接下來,啟用物理屬性 按住Shift選中四個節(jié)點,進(jìn)入物理檢查器,將Type改為Kinematic.

再進(jìn)入節(jié)點檢查器,滾動到Visibility區(qū),設(shè)置Opacity為0.5(供調(diào)試),同時取消勾選Casts Shadow.

設(shè)置各個節(jié)點的位掩碼 Front節(jié)點,物理檢查器中,Category mask設(shè)為8.

Back節(jié)點,物理檢查器中,Category mask設(shè)為16.

Left節(jié)點,物理檢查器中,Category mask設(shè)為32.

Right節(jié)點,物理檢查器中,Category mask設(shè)為64.

最后,還要刪除默認(rèn)的camera.

使用碰撞節(jié)點

選中MrPig.scnassets/GameScene.scn,然后拖拽一個Collsion.scn引用節(jié)點到場景中.

ViewController.swift中添加屬性:

var collisionNode: SCNNode!
var frontCollisionNode: SCNNode!
var backCollisionNode: SCNNode!
var leftCollisionNode: SCNNode!
var rightCollisionNode: SCNNode!

setupNodes()最后添加下面代碼:

collisionNode = gameScene.rootNode.childNode(withName: "Collision", recursively: true)!
frontCollisionNode = gameScene.rootNode.childNode(withName: "Front", recursively: true)!
backCollisionNode = gameScene.rootNode.childNode(withName: "Back", recursively: true)!
leftCollisionNode = gameScene.rootNode.childNode(withName: "Left", recursively: true)!
rightCollisionNode = gameScene.rootNode.childNode(withName: "Right", recursively: true)!

創(chuàng)建渲染循環(huán)

ViewController中添加方法:

func updatePositions() {
  collisionNode.position = pigNode.position
}

ViewController.swift最底部添加方法:

// 1
extension ViewController : SCNSceneRendererDelegate {
  // 2
  func renderer(_ renderer: SCNSceneRenderer, didApplyAnimationsAtTime
time:
    TimeInterval) {
    // 3
    guard game.state == .playing else {
return
}
// 4
    game.updateHUD()
// 5
    updatePositions()
  }
}

代碼含義:

  1. 實現(xiàn)了SCNSceneRenderDelegate協(xié)議;
  2. 在渲染循環(huán)中剛剛完成動畫和動作后,插入游戲邏輯;
  3. 判斷游戲狀態(tài);
  4. 更新HUD;
  5. 調(diào)用updatePositions(),使collisionNode位置和pigNode保持一致;

記得在setupScenes()中添加代理:

scnView.delegate = self

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


添加物理效果

ViewController中,定義以下常量:

let BitMaskPig = 1
let BitMaskVehicle = 2
let BitMaskObstacle = 4
let BitMaskFront = 8
let BitMaskBack = 16
let BitMaskLeft = 32
let BitMaskRight = 64
let BitMaskCoin = 128
let BitMaskHouse = 256

接下來,啟動物理效果

選中MrPig.scnassets/MrPig.scn,選中MrPig節(jié)點,進(jìn)入物理檢查器,將Type改為Kinematic.

接著在Bit masks區(qū),將Category mask設(shè)為1,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale0.6.

按同樣步驟,選中MrPig.scnassets/Bus.scn,再選中Bus節(jié)點,進(jìn)入物理檢查器,將Type改為Kinematic.

接著在Bit masks區(qū),將Category mask設(shè)為2,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale0.8.

選中MrPig.scnassets/Mini.scn,再選中Mini節(jié)點,進(jìn)入物理檢查器,將Type改為Kinematic.

接著在Bit masks區(qū),將Category mask設(shè)為2,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale0.8.

選中MrPig.scnassets/SUV.scn,再選中SUV節(jié)點,進(jìn)入物理檢查器,將Type改為Kinematic.

接著在Bit masks區(qū),將Category mask設(shè)為2,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale0.8.

選中MrPig.scnassets/TreeLine.scn,再選中TreeLine節(jié)點,進(jìn)入物理檢查器,將Type改為Static.

接著在Bit masks區(qū),將Category mask設(shè)為4,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale1.

選中MrPig.scnassets/TreePatch.scn,再選中TreePatch節(jié)點,進(jìn)入物理檢查器,將Type改為Static.

接著在Bit masks區(qū),將Category mask設(shè)為4,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale1.

選中MrPig.scnassets/Coin.scn,再選中Coin節(jié)點,進(jìn)入物理檢查器,將Type改為Kinematic.

接著在Bit masks區(qū),將Category mask設(shè)為128,在Physics shape區(qū), 將Type改為Bounding Box并設(shè)置Scale0.8.

設(shè)置接觸掩碼

打開ViewController.swift,在setupNodes()的底部添加代碼:

// 1
pigNode.physicsBody?.contactTestBitMask = BitMaskVehicle | BitMaskCoin | BitMaskHouse
// 2
frontCollisionNode.physicsBody?.contactTestBitMask = BitMaskObstacle
backCollisionNode.physicsBody?.contactTestBitMask = BitMaskObstacle
leftCollisionNode.physicsBody?.contactTestBitMask = BitMaskObstacle
rightCollisionNode.physicsBody?.contactTestBitMask = BitMaskObstacle

處理碰撞

ViewController添加屬性:

var activeCollisionsBitMask: Int = 0

ViewContoller.swift最下方添加代碼:

// 1
extension ViewController : SCNPhysicsContactDelegate {
  // 2
  func physicsWorld(_ world: SCNPhysicsWorld,
    didBegin contact: SCNPhysicsContact) {
    // 3
    guard game.state == .playing else {
        return
    }
    // 4
    var collisionBoxNode: SCNNode!
    if contact.nodeA.physicsBody?.categoryBitMask == BitMaskObstacle {
      collisionBoxNode = contact.nodeB
    } else {
      collisionBoxNode = contact.nodeA
    }
    // 5
   activeCollisionsBitMask |=    collisionBoxNode.physicsBody!.categoryBitMask
 }

    // 6
  func physicsWorld(_ world: SCNPhysicsWorld,
    didEnd contact: SCNPhysicsContact) {
    // 7
    guard game.state == .playing else {
        return
    }
    // 8
    var collisionBoxNode: SCNNode!
    if contact.nodeA.physicsBody?.categoryBitMask == BitMaskObstacle {
      collisionBoxNode = contact.nodeB
    } else {
      collisionBoxNode = contact.nodeA
    }
    // 9
    activeCollisionsBitMask &=
      ~collisionBoxNode.physicsBody!.categoryBitMask
  }

}

代碼含義:

  1. 添加類擴(kuò)展,遵守SCNPhysicsContactDelegate協(xié)議;
  2. 實現(xiàn)physicsWorld(_:didBegin:)方法;
  3. 只需要關(guān)注.playing狀態(tài)時的情況,其他不處理;
  4. 判斷哪個是障礙物,哪個是隱藏的碰撞檢測盒子;
  5. 用位運(yùn)算OR,將碰撞檢測盒子的類掩碼添加到activeCollisionsBitMask中去;
  6. 實現(xiàn)physicsWorld(_:didEnd:)方法,碰撞結(jié)束時調(diào)用;
  7. 判斷.playing狀態(tài);
  8. 判斷哪個是障礙物,哪個是隱藏的碰撞檢測盒子;
  9. 用位運(yùn)算符NOT和位運(yùn)算符AND,從activeCollisionsBitMask中移除碰撞檢測盒子的類掩碼;

handleGestures(_:)中的guard語句后,添加下面代碼:

// 1
let activeFrontCollision = activeCollisionsBitMask & BitMaskFront ==
BitMaskFront
let activeBackCollision = activeCollisionsBitMask & BitMaskBack ==
BitMaskBack
let activeLeftCollision = activeCollisionsBitMask & BitMaskLeft ==
BitMaskLeft
let activeRightCollision = activeCollisionsBitMask & BitMaskRight ==
BitMaskRight
// 2
guard (sender.direction == .up && !activeFrontCollision) ||
  (sender.direction == .down && !activeBackCollision) ||
  (sender.direction == .left && !activeLeftCollision) ||
  (sender.direction == .right && !activeRightCollision) else {
return
}

代碼含義:

  1. 用位運(yùn)算符AND來判斷四個方向的隱藏節(jié)點是否已經(jīng)發(fā)生了碰撞;
  2. guard來確保沒有碰撞,可以向該方向移動;

最后,在setupScenes()中添加代理:

gameScene.physicsWorld.contactDelegate = self

現(xiàn)在運(yùn)行一下,小豬就不會再跳進(jìn)樹林中了:


處理和車輛的碰撞

physicsWorld(_:didBegin:)中最后添加下面代碼:

// 1
var contactNode: SCNNode!
if contact.nodeA.physicsBody?.categoryBitMask == BitMaskPig {
  contactNode = contact.nodeB
} else {
  contactNode = contact.nodeA
}
// 2
if contactNode.physicsBody?.categoryBitMask == BitMaskVehicle {
stopGame()
}

代碼含義:

  1. 和前面類似,用來判斷哪個是小豬;
  2. 如果是和車輛碰撞,就結(jié)束游戲;

處理和金幣的碰撞

physicsWorld(_:didBegin:)中的后面添加下面代碼:

// 1
if contactNode.physicsBody?.categoryBitMask == BitMaskCoin {
  // 2
  contactNode.isHidden = true
  contactNode.runAction(SCNAction.waitForDurationThenRunBlock(duration:
60) { (node: SCNNode!) -> Void in
    node.isHidden = false
  })
// 3
  game.collectCoin()
}

代碼含義:

  1. 如果是和金幣碰撞;
  2. 隱藏金幣,并在60秒后重新出現(xiàn);
  3. 收集金幣,增加分?jǐn)?shù);

運(yùn)行游戲,現(xiàn)在可以收集金幣了


結(jié)束處理

更新攝像機(jī)位置

打開ViewController.swift,在updatePositions()在最底部,添加下面的代碼:

let lerpX = (pigNode.position.x - cameraFollowNode.position.x) * 0.05
let lerpZ = (pigNode.position.z - cameraFollowNode.position.z) * 0.05
cameraFollowNode.position.x += lerpX
cameraFollowNode.position.z += lerpZ

這段代碼讓攝像機(jī)朝小豬方向慢慢移動.

更新燈光位置 在updatePositions()在最底部,添加下面的代碼:

lightFollowNode.position = cameraFollowNode.position

更新交通狀況 我們需要用幾輛車來模擬不斷的交通情況,所以當(dāng)小車遇到邊界時,需要重新設(shè)定它們的位置. 在ViewController中,添加下面的方法:

func updateTraffic() {
  // 1
  for node in trafficNode.childNodes {
    // 2
    if node.position.x > 25 {
      node.position.x = -25
    } else if node.position.x < -25 {
      node.position.x = 25
    }
} }

然后還要在renderer(_:didApplyAnimationsAtTime:)底部添加調(diào)用:

updateTraffic()

設(shè)置房屋

  1. 創(chuàng)建一個新SceneKit場景,命名為Home.scn,并刪除默認(rèn)的攝像機(jī);
  2. 添加一個House.scn到場景中,放在正中間;
  3. 創(chuàng)建一個空的節(jié)點,命名為Obstacles,用來作為容器節(jié)點;
  4. 添加一些樹;
  5. 添加一個Mini.scn;

參考下圖:


103.png
  1. 周圍的障礙物Obstacles需要設(shè)置物理形體;分類掩碼category bit mask設(shè)置為4.而House的掩碼設(shè)置為256;

  2. 完成后,引入到游戲場景中;


  3. 最后,在physicsWorld(_:didBegin:)中添加代碼,讓Mr.Pig把金幣放到家中;

if contactNode.physicsBody?.categoryBitMask == BitMaskHouse {
  if game.bankCoins() == true {
  }
}

添加音頻

ViewController.swift中,給setupSounds()中添加下面代碼:

// 1
if game.state == .tapToPlay {
  // 2
  let music = SCNAudioSource(fileNamed: "MrPig.scnassets/Audio/
Music.mp3")!
// 3
  music.volume = 0.3;
  music.loops = true
  music.shouldStream = true
  music.isPositional = false
  // 4
  let musicPlayer = SCNAudioPlayer(source: music)
  // 5
  splashScene.rootNode.addAudioPlayer(musicPlayer)
}

此外,還要再添加一些環(huán)境音,在setupSounds()底部再添加:

// 1
else if game.state == .playing {
  // 2
  let traffic = SCNAudioSource(fileNamed: "MrPig.scnassets/Audio/
Traffic.mp3")!
  traffic.volume = 0.3
  traffic.loops = true
  traffic.shouldStream = true
  traffic.isPositional = true
  // 3
  let trafficPlayer = SCNAudioPlayer(source: traffic)
  gameScene.rootNode.addAudioPlayer(trafficPlayer)
  // 4
  game.loadSound(name: "Jump", fileNamed: "MrPig.scnassets/Audio/
Jump.wav")
  game.loadSound(name: "Blocked", fileNamed: "MrPig.scnassets/Audio/
Blocked.wav")
  game.loadSound(name: "Crash", fileNamed: "MrPig.scnassets/Audio/
Crash.wav")
  game.loadSound(name: "CollectCoin", fileNamed: "MrPig.scnassets/Audio/
CollectCoin.wav")
  game.loadSound(name: "BankCoin", fileNamed: "MrPig.scnassets/Audio/
BankCoin.wav")
}

代碼含義:

  1. 檢查是否是.Playing狀態(tài);
  2. 設(shè)置MrPig.scnassets/Audio/Traffic.mp3作為流音頻的源;
  3. 添加到時根節(jié)點時開始播放音頻源;
  4. 預(yù)加載其他用到的音效;

最后再添加一些音效 跳躍音效:在handleGesture(_:)方法后面添加:

game.playSound(node: pigNode, name: "Jump")

遇到障礙物音效:在第二個guard語句中:

game.playSound(node: pigNode, name: "Blocked")

收集金幣音效:在physicsWorld(_:didBegin:)中的game.collectCoin()語句后,添加:

game.playSound(node: pigNode, name: "CollectCoin")

存放金幣音效:在physicsWorld(_:didBegin:)if game.bankCoins() == true語句后面添加:

game.playSound(node: pigNode, name: "BankCoin")

被車撞音效:在physicsWorld(_:didBegin:)stopGame()之前添加:

game.playSound(node: pigNode, name: "Crash")

運(yùn)行一下, 完成了!!


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颜曾,一起剝皮案震驚了整個濱河市纠拔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泛豪,老刑警劉巖稠诲,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诡曙,居然都是意外死亡臀叙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門价卤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劝萤,“玉大人,你說我怎么就攤上這事慎璧∥绕洌” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵炸卑,是天一觀的道長。 經(jīng)常有香客問我煤傍,道長盖文,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任蚯姆,我火速辦了婚禮五续,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘龄恋。我一直安慰自己疙驾,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布郭毕。 她就那樣靜靜地躺著它碎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪显押。 梳的紋絲不亂的頭發(fā)上扳肛,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音乘碑,去河邊找鬼挖息。 笑死,一個胖子當(dāng)著我的面吹牛兽肤,可吹牛的內(nèi)容都是我干的套腹。 我是一名探鬼主播绪抛,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼电禀!你這毒婦竟也來了幢码?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤鞭呕,失蹤者是張志新(化名)和其女友劉穎蛤育,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葫松,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡瓦糕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了腋么。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咕娄。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖珊擂,靈堂內(nèi)的尸體忽然破棺而出圣勒,到底是詐尸還是另有隱情,我是刑警寧澤摧扇,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布圣贸,位于F島的核電站,受9級特大地震影響扛稽,放射性物質(zhì)發(fā)生泄漏吁峻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一在张、第九天 我趴在偏房一處隱蔽的房頂上張望用含。 院中可真熱鬧,春花似錦帮匾、人聲如沸啄骇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缸夹。三九已至,卻和暖如春哼转,著一層夾襖步出監(jiān)牢的瞬間明未,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工壹蔓, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留趟妥,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓佣蓉,卻偏偏與公主長得像披摄,于是被迫代替她去往敵國和親亲雪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345