說明
本系列文章是對<3D Apple Games by Tutorials>一書的學習記錄和體會
更多iOS相關知識檢視github上WeekWeekUpProject
效果如下:
專案中用到的模型是用MagicaVoxel建立的,可以到ephtracy.github.io上去免費下載.使用教程參見本系列其他文章.
19-Transitions轉場
建立專案
建立專案,選擇iOS > Application > Single View Application模板.
更改設定,只保留豎直方向:
新增資原始檔: 拖拽resources/GameUtils/資料夾到專案中,選擇Group:
拖拽resources/MrPig.scnassets資料夾到專案中,選擇Create folder references:
完成後的效果:
新增應用圖示和啟動螢幕 在resources資料夾下找到LaunchScreen和AppIcon資料夾,拖拽到對應地方去:
給啟動螢幕新增圖片約束:
開啟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 }
}
複製程式碼
內容簡單不做過多說明,匯入SpriteKit是為了使用轉場功能.
繼續建立SceneKit View: 在ViewController的最上方新增:
var scnView: SCNView!
複製程式碼
在setupScenes()中新增:
scnView = SCNView(frame: self.view.frame)
self.view.addSubview(scnView)
複製程式碼
建立多場景
拖拽一個SceneKit Scene File到專案根目錄中,將其命名為GameScene.scn,放在MrPig.scnassets資料夾下:
選中MrPig.scnassets/GameScene.scn的同時,拖拽一個Floor Node到場景中,並選擇Node Inspector節點檢查器,將其命名為Grass,位置和旋轉角度設為0:
再設定屬性檢查器,將反射Floor Reflectivity設為0,因為草地並不需要反光:
移動到材質檢查器,設定Material Diffuse貼圖,縮放設為12.5:
建立啟動閃屏
再拖拽一個SceneKit Scene File到專案根目錄中,將其命名為SplashScene.scn,放在MrPig.scnassets資料夾下:
選中MrPig.scnassets/SplashScene.scn的同時,從右下角的材質庫中拖拽一個MrPig引用節點到場景中,開啟其節點檢查器,將位置和旋轉設定為零:
接著開啟場景檢查器,新增漸變背景Gradient_Diffuse.png到Scene Background屬性:
為了讓背景更好看,設定出太陽般的放射光暈效果,需要拖拽一個Plane節點到場景中,命名為Rays,設定位置 (x:0, y:0.25, z:-1),可見度Visibility Opacity為0.25:
開啟屬性檢查器,設定Size為 (x:5, y:5),並設定圓角半徑Corner Radius為2.5:
開啟材質檢查器Materials Inspector,將Lighting model設定為Constant,以避免光照對射線產生影響.
滾動到Settings區域並將Blend Mode混合模式設定為Subtract減弱:
設定攝像機和燈光
點選場景樹下方的+
號,新增一個空節點.命名為Camera,並將原始的攝像機節點移動過來作為子節點.選中Camera節點,開啟節點檢查器,設定位置為(x:0, y:0.3, z:0)旋轉尤拉角為(x:-10, y:0, z:0).
選中內層的camera節點,設定位置為(x:0, y:0, z:3)尤拉角為(x:0, y:0, z:0).
再新增一個空節點到根目錄中,命名為Lights,拖拽一個Ambient和一個Omni燈光到場景中:
修改omni燈光的位置,進入節點檢查器,位置改為(x:5, y:5, z:5).
新增logo和點選開始節點
- The Logo node:使用Plane型別節點,MrPigLogo_Diffuse.png貼圖,設定尺寸為width:1, height:0.5,位置設定為 (x:0, y:1, z:0.5),注意不要受到燈光的影響,做法參考Rays節點.
- The TapToPlay node:使用Plane型別節點,TapToPlay_Diffuse.png貼圖,設定尺寸為width:1, height:0.25,位置設定為 (x:0, y:-0.3, z:0.5),注意不要受到燈光的影響,做法參考Rays節點.
載入並展示閃屏介面
在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
複製程式碼
執行一下,效果如下:
轉場
不同效果的轉場動畫前面已經介紹過了. 在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
})
}
複製程式碼
繼續新增停止遊戲和開啟閃屏的方法:
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()
}
}
複製程式碼
執行後,點選螢幕開始遊戲了.
20-Advanced Scene Creation高階場景建立
新增主角
選中MrPig.scnassets/ GameScene.scn,拖拽一個MrPig引用節點到場景中,位置和旋轉設定為0:
在setupNodes()
中新增程式碼:
pigNode = gameScene.rootNode.childNode(withName: "MrPig", recursively:true)!
複製程式碼
建立攝像機和燈光
回到MrPig.scnassets/GameScene.scn中,設定攝像機.
在場景樹中點選+
號新增一個空節點,在節點檢查器中,將其命名為FollowCamera,位置為0,旋轉 (x:-45, y:20, z:0).
將已經存在的camera節點拖放到FollowCamera節點下.位置 (x:0, y:0, z:14),旋轉0.
選中ViewController.swift,新增一些屬性來控制攝像機:
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)!
複製程式碼
程式碼含義:
- 將cameraNode繫結到camera上,然後將hudNode新增上去作為子節點,這樣HUD就能一直顯示在鏡頭前了.
- 將cameraFollowNode繫結到FollowCamera上,這樣只需要更新其位置,攝像機就能一直跟隨著小豬了.
在場景樹中點選+
號新增一個空節點,在節點檢查器中,將其命名為FollowLight,位置為0,旋轉為0.拖拽一個Ambient light和一個Directional light到空節點中.
選中ambient燈光,位置和旋轉設定為0:
選擇屬性檢查器,設定顏色為Aluminum:
然後選中directional燈光,進入節點檢查器,設定位置和旋轉如下:
進入屬性檢查器,配置燈光和陰影屬性:
配置完成後預覽一下:
在ViewController中新增屬性:
var lightFollowNode: SCNNode!
複製程式碼
在setupNodes() 中新增下面程式碼:
lightFollowNode = gameScene.rootNode.childNode(withName: "FollowLight",recursively: true)!
複製程式碼
新增高速公路和車輛
在場景樹中點選+
號新增一個空節點,在節點檢查器中,將其命名為Highway,拖拽兩個Road引用作為子節點.選中第一個,設定位置 (x:0, y:0, z:-4.5),旋轉為0:
選中第二個,位置為 (x:0, y:0, z:-11.5),旋轉為0:
點選+
號新增一個空節點,在節點檢查器中,將其命名為Traffic,拖拽一個Bus引用節點到其中,位置 (x:0, y:0, z:-4),旋轉 (x:0, y:-90, z:0):
拖拽一個Mini引用節點到其中,位置 (x:3, y:0, z:-5),旋轉 (x:0, y:-90, z:0):
拖拽一個SUV引用節點到其中,位置 (x:-3, y:0, z:-5),旋轉 (x:0, y:-90, z:0):
選中ViewController.swift,新增屬性:
var trafficNode: SCNNode!
複製程式碼
在 *setupNodes()*中新增程式碼:
trafficNode = gameScene.rootNode.childNode(withName: "Traffic", recursively: true)!
複製程式碼
接著,需要複製一下達到下面的效果:
- 左側車道是公交車道,右側車道是較小較快的車道;
- 兩條公路,一條朝左開,一條朝右開;
- 按住Option鍵來快速複製;
- 按住Command鍵來與周圍元素對齊;
- 兩車之間就留下足夠距離讓小豬能行;
- 完成一條公路後,選中所有車輛,並按住Option拖拽到另一條公路上,就複製完成了,然後再掉轉180度;
- 旋轉車輛時,按住Command鍵可以更方便地對齊;
完成後,執行一下:
新增樹林
拖拽一個空的SceneKit Scene File,命名為TreeLine並放置在MrPig.scnassets目錄下:
在TreeLine.scn中,建立一個空節點,命名為TreeLine,它將作為父容器節點:
按下面的圖來擺放各種樹木:
- X:代表在x軸上的座標;
- Z:代表在z軸上的座標;
- S:代表小的樹SmallTree;
- M:代表中等樹MediumTree;
- L:代表大的樹LargeTree;
例如左上角第一個,放置一個SmallTree在位置 (x:-5, y:0, z:-1) 處.使用Option和Command鍵可以提高複製貼上速度.
拖拽一個空的SceneKit Scene File,命名為TreePatch並放置在MrPig.scnassets目錄下:
在TreePatch.scn中,建立一個空節點,命名為TreePatch,它將作為父容器節點:
按下面的圖來擺放各種樹木:
完成後:
回到MrPig.scnassets/GameScene.scn中,新增一個空節點,命名為Trees,作為樹林的容器節點:
按下圖來擺放樹林TreeLine:
前面部分,靠近Mr.Pig處樹林座標為:
- 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).
公路中間處的座標為:
- 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).
後面部分的座標為:
- 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:
放置座標如下:
- 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).
新增金幣
先建立一個空節點,命名為Coins,作為金幣的容器節點:
然後拖拽Coin引用節點到場景中,座標如下:
- 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).
完成後,效果如圖:
21-Actions動作
動作編輯器
背景射線動起來 開啟MrPig.scnassets/SplashScene.scn,選中Rays.拖拽一個Rotate Action,在屬性檢查器中設定時長30,z軸360;
右鍵點選,建立迴圈,點選∞
金幣動畫
選中MrPig.scnassets/Coin.scn,按順序拖拽兩個Move Action,再並排放置一個Rotate Action
選中第一個Move Action,設定Start Time為0,Duration為0.5.設定Timing Function為Ease In, Ease Out, 設定Offset為 (x:0, y:0.5, z:0).
選中第二個Move Action,設定Start Time為0.5,Duration為0.5.設定Timing Function為Ease In, Ease Out, 設定Offset為 (x:0, y:-0.5, z:0).
選中Move Action,設定Start Time為0,Duration為1.設定Timing Function為Linear, 設定Euler Angle為 (x:0, y:360, z:0).
按Shift選中全部,然後右鍵單擊,建立迴圈
讓閃屏頁面的小豬動起來 進入MrPig.scnassets/SplashScreen.scn,選中MrPig,建立一系列旋轉動作:
- 連線7個Rotate Actions序列,設定時長為0.25s;
- 設定第一個旋轉動作,讓它沿x軸旋轉30度;
- 設定下一個,沿x軸旋轉-30度;
- 重複設定接下來的幾個動作,直到最後一個;
- 最後一個動作是沿y軸旋轉180度,讓小豬秀它的尾巴;
- 移動遊標,預覽動作,會看到小豬搖頭晃腦,然後轉身給你看尾巴;
- 設定迴圈播放
執行一下,檢視動作:
程式碼建立動作
交通動作 開啟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)
}
}
複製程式碼
執行一下,車輛動起來了,但是開過去後就沒有了,這個問題我們稍後再處理:
新增小豬的動作
新增屬性:
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])
複製程式碼
程式碼含義:
- 定義時長;
- 向上,向下的彈簧效果;
- 修改時間模式,一個漸入,一個漸出;
- 建立
bounceAction
將向上和向下彈簧效果組成序列; - 用
SCNAction.moveBy(x:y:z:duration:)
建立四個方向的移動動作; - 用
SCNAction.rotateTo(x:y:z:duration:usesShortestUnitArc:)
建立四個方向的旋轉動作; - 按順序組合出四個跳躍動作;
新增移動手勢
給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
} }
複製程式碼
程式碼含義:
- 定義一個手勢方法;
- 判斷遊戲狀態;
- 判斷手勢方向,左右限制不能超出範圍;
在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)
複製程式碼
執行一下,測試手勢控制:
設定遊戲結束時的動作序列
給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])
複製程式碼
程式碼含義:
- 建立一些基本動作:一個旋轉720度,一個向上移動,一個逐漸透明;共同組成了一個動作組,叫
goodByePig
; SCNAction.runAction(_:)
類方法允許我們插入一些邏輯程式碼,在block中重設了小豬的位置,透明度,並觸發了startSplash()
方法;- 建立最終的
triggerGameOver
動作序列,先執行goodByePig
,再執行gameOver
;
在stopGame()
方法後面呼叫一下:
pigNode.runAction(triggerGameOver)
複製程式碼
22-Advanced Collision Detection高階碰撞檢測
本章節解決以下問題:
- 小豬遇到障礙時不能停止,如撞上樹木;
- 小豬撞到汽車不能結束遊戲;
- 小豬無法真正收集金幣;
隱藏的碰撞檢測幾何體
這裡我們用點小技巧來處理小豬與樹林的碰撞問題,使用四個隱藏的節點,當左側節點與樹林碰撞時,就不能再向左移動了:
建立隱藏的碰撞節點
建立SceneKit Scene File到根目錄下,命名為Collision.scn,儲存在MrPig.scnassets下:
選中Collision.scn,新增一個空節點,命名為Collision.
拖拽一個Box到場景中,放置在Collision節點下,命名為Front,位置 (x:0, y:0.25, z:-1).
進入屬性檢查器,設定尺寸為 (x:0.25, y:0.25, z:0.25).
按住Option和Command,拖拽出另外三個副本,選中一個命名為Back,位置設為 (x:0, y:0.25, z:1).
選中另一個,命名為Left,位置 (x:-1, y:0.25, z:0)
選中最後一個,命名為Right,位置 (x:1, y:0.25, z:0)
完成後的效果
接下來,啟用物理屬性 按住Shift選中四個節點,進入物理檢查器,將Type改為Kinematic.
再進入節點檢查器,滾動到Visibility區,設定Opacity為0.5(供除錯),同時取消勾選Casts Shadow.
設定各個節點的位掩碼 Front節點,物理檢查器中,Category mask設為8.
Back節點,物理檢查器中,Category mask設為16.
Left節點,物理檢查器中,Category mask設為32.
Right節點,物理檢查器中,Category mask設為64.
最後,還要刪除預設的camera.
使用碰撞節點
選中MrPig.scnassets/GameScene.scn,然後拖拽一個Collsion.scn引用節點到場景中.
在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)!
複製程式碼
建立渲染迴圈
在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()
}
}
複製程式碼
程式碼含義:
- 實現了
SCNSceneRenderDelegate
協議; - 在渲染迴圈中剛剛完成動畫和動作後,插入遊戲邏輯;
- 判斷遊戲狀態;
- 更新HUD;
- 呼叫
updatePositions()
,使collisionNode
位置和pigNode
保持一致;
記得在setupScenes()
中新增代理:
scnView.delegate = self
複製程式碼
執行一下,檢視效果:
新增物理效果
在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節點,進入物理檢查器,將Type改為Kinematic.
接著在Bit masks區,將Category mask設為1,在Physics shape區, 將Type改為Bounding Box並設定Scale為0.6.
按同樣步驟,選中MrPig.scnassets/Bus.scn,再選中Bus節點,進入物理檢查器,將Type改為Kinematic.
接著在Bit masks區,將Category mask設為2,在Physics shape區, 將Type改為Bounding Box並設定Scale為0.8.
選中MrPig.scnassets/Mini.scn,再選中Mini節點,進入物理檢查器,將Type改為Kinematic.
接著在Bit masks區,將Category mask設為2,在Physics shape區, 將Type改為Bounding Box並設定Scale為0.8.
選中MrPig.scnassets/SUV.scn,再選中SUV節點,進入物理檢查器,將Type改為Kinematic.
接著在Bit masks區,將Category mask設為2,在Physics shape區, 將Type改為Bounding Box並設定Scale為0.8.
選中MrPig.scnassets/TreeLine.scn,再選中TreeLine節點,進入物理檢查器,將Type改為Static.
接著在Bit masks區,將Category mask設為4,在Physics shape區, 將Type改為Bounding Box並設定Scale為1.
選中MrPig.scnassets/TreePatch.scn,再選中TreePatch節點,進入物理檢查器,將Type改為Static.
接著在Bit masks區,將Category mask設為4,在Physics shape區, 將Type改為Bounding Box並設定Scale為1.
選中MrPig.scnassets/Coin.scn,再選中Coin節點,進入物理檢查器,將Type改為Kinematic.
接著在Bit masks區,將Category mask設為128,在Physics shape區, 將Type改為Bounding Box並設定Scale為0.8.
設定接觸掩碼
開啟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
}
}
複製程式碼
程式碼含義:
- 新增類擴充套件,遵守
SCNPhysicsContactDelegate
協議; - 實現
physicsWorld(_:didBegin:)
方法; - 只需要關注
.playing
狀態時的情況,其他不處理; - 判斷哪個是障礙物,哪個是隱藏的碰撞檢測盒子;
- 用位運算OR,將碰撞檢測盒子的類掩碼新增到
activeCollisionsBitMask
中去; - 實現
physicsWorld(_:didEnd:)
方法,碰撞結束時呼叫; - 判斷
.playing
狀態; - 判斷哪個是障礙物,哪個是隱藏的碰撞檢測盒子;
- 用位運算子NOT和位運算子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
}
複製程式碼
程式碼含義:
- 用位運算子AND來判斷四個方向的隱藏節點是否已經發生了碰撞;
- 用
guard
來確保沒有碰撞,可以向該方向移動;
最後,在setupScenes()
中新增代理:
gameScene.physicsWorld.contactDelegate = self
複製程式碼
現在執行一下,小豬就不會再跳進樹林中了:
處理和車輛的碰撞
在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()
}
複製程式碼
程式碼含義:
- 和前面類似,用來判斷哪個是小豬;
- 如果是和車輛碰撞,就結束遊戲;
處理和金幣的碰撞
在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()
}
複製程式碼
程式碼含義:
- 如果是和金幣碰撞;
- 隱藏金幣,並在60秒後重新出現;
- 收集金幣,增加分數;
執行遊戲,現在可以收集金幣了
23-Audio音訊
結束處理
更新攝像機位置
開啟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
複製程式碼
這段程式碼讓攝像機朝小豬方向慢慢移動.
更新燈光位置
在updatePositions()
在最底部,新增下面的程式碼:
lightFollowNode.position = cameraFollowNode.position
複製程式碼
更新交通狀況 我們需要用幾輛車來模擬不斷的交通情況,所以當小車遇到邊界時,需要重新設定它們的位置. 在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:)
底部新增呼叫:
updateTraffic()
複製程式碼
設定房屋
- 建立一個新SceneKit場景,命名為Home.scn,並刪除預設的攝像機;
- 新增一個House.scn到場景中,放在正中間;
- 建立一個空的節點,命名為Obstacles,用來作為容器節點;
- 新增一些樹;
- 新增一個Mini.scn;
參考下圖:
- 周圍的障礙物Obstacles需要設定物理形體;分類掩碼category bit mask設定為4.而House的掩碼設定為256;
- 完成後,引入到遊戲場景中;
- 最後,在
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)
}
複製程式碼
此外,還要再新增一些環境音,在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")
}
複製程式碼
程式碼含義:
- 檢查是否是
.Playing
狀態; - 設定MrPig.scnassets/Audio/Traffic.mp3作為流音訊的源;
- 新增到時根節點時開始播放音訊源;
- 預載入其他用到的音效;
最後再新增一些音效
跳躍音效:在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")
複製程式碼
執行一下, 完成了!!