文章选自掘金苹果API搬运工的文章[SceneKit专题]24-体素风格过马路游戏Mr. Pig
主要记录自己在学习ARKit的过程中看到的好的文章,避免到时候链接失效无法找到原文的情况,非常感谢原博主的辛勤付出,也在此分享出来跟大家一起学习。
效果如下:
项目中用到的模型是用MagicaVoxel创建的,可以到ephtracy.github.io上去免费下载.使用教程参见本系列其他文章.
创建项目
创建项目,选择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, with event: UIEvent?)
{
if game.state == .tapToPlay {
startGame()
}
}
运行后,点击屏幕开始游戏了.
添加主角
选中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).
完成后,效果如图:
动作编辑器
背景射线动起来 打开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)
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秒后重新出现;
- 收集金币,增加分数;
运行游戏,现在可以收集金币了
结束处理
更新摄像机位置
打开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")
运行一下, 完成了!!