24-体素风格过马路游戏Mr. Pig

文章选自掘金苹果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文件夹下找到LaunchScreenAppIcon文件夹,拖拽到对应地方去:

给启动屏幕添加图片约束:


打开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.pngScene Background属性:

为了让背景更好看,设置出太阳般的放射光晕效果,需要拖拽一个Plane节点到场景中,命名为Rays,设置位置 (x:0, y:0.25, z:-1),可见度Visibility Opacity0.25:

打开属性检查器,设置Size(x:5, y:5),并设置圆角半径Corner Radius2.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()
  }
}

运行后,点击屏幕开始游戏了.


添加主角

选中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)!

代码含义:

  1. 将cameraNode绑定到camera上,然后将hudNode添加上去作为子节点,这样HUD就能一直显示在镜头前了.
  2. 将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 Time0,Duration0.5.设置Timing FunctionEase In, Ease Out, 设置Offset(x:0, y:0.5, z:0).

选中第二个Move Action,设置Start Time0.5,Duration0.5.设置Timing FunctionEase In, Ease Out, 设置Offset(x:0, y:-0.5, z:0).

选中Move Action,设置Start Time0,Duration1.设置Timing FunctionLinear, 设置Euler Angle(x:0, y:360, z:0).

Shift选中全部,然后右键单击,创建循环

让闪屏页面的小猪动起来 进入MrPig.scnassets/SplashScreen.scn,选中MrPig,创建一系列旋转动作:

  1. 连接7个Rotate Actions序列,设置时长为0.25s;

  2. 设置第一个旋转动作,让它沿x轴旋转30度;


  3. 设置下一个,沿x轴旋转-30度;


  4. 重复设置接下来的几个动作,直到最后一个;

  5. 最后一个动作是沿y轴旋转180度,让小猪秀它的尾巴;


  6. 移动游标,预览动作,会看到小猪摇头晃脑,然后转身给你看尾巴;

  7. 设置循环播放



运行一下,查看动作:

代码创建动作

交通动作 打开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])

代码含义:

  1. 定义时长;
  2. 向上,向下的弹簧效果;
  3. 修改时间模式,一个渐入,一个渐出;
  4. 创建bounceAction将向上和向下弹簧效果组成序列;
  5. SCNAction.moveBy(x:y:z:duration:)创建四个方向的移动动作;
  6. SCNAction.rotateTo(x:y:z:duration:usesShortestUnitArc:)创建四个方向的旋转动作;
  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. 判断游戏状态;
  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)

运行一下,测试手势控制:


设置游戏结束时的动作序列

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. 创建一些基本动作:一个旋转720度,一个向上移动,一个逐渐透明;共同组成了一个动作组,叫goodByePig;
  2. SCNAction.runAction(_:)类方法允许我们插入一些逻辑代码,在block中重设了小猪的位置,透明度,并触发了startSplash()方法;
  3. 创建最终的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).

按住OptionCommand,拖拽出另外三个副本,选中一个命名为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()
  }
}

代码含义:

  1. 实现了SCNSceneRenderDelegate协议;
  2. 在渲染循环中刚刚完成动画和动作后,插入游戏逻辑;
  3. 判断游戏状态;
  4. 更新HUD;
  5. 调用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并设置Scale0.6.

按同样步骤,选中MrPig.scnassets/Bus.scn,再选中Bus节点,进入物理检查器,将Type改为Kinematic.

接着在Bit masks区,将Category mask设为2,在Physics shape区, 将Type改为Bounding Box并设置Scale0.8.

选中MrPig.scnassets/Mini.scn,再选中Mini节点,进入物理检查器,将Type改为Kinematic.

接着在Bit masks区,将Category mask设为2,在Physics shape区, 将Type改为Bounding Box并设置Scale0.8.

选中MrPig.scnassets/SUV.scn,再选中SUV节点,进入物理检查器,将Type改为Kinematic.

接着在Bit masks区,将Category mask设为2,在Physics shape区, 将Type改为Bounding Box并设置Scale0.8.

选中MrPig.scnassets/TreeLine.scn,再选中TreeLine节点,进入物理检查器,将Type改为Static.

接着在Bit masks区,将Category mask设为4,在Physics shape区, 将Type改为Bounding Box并设置Scale1.

选中MrPig.scnassets/TreePatch.scn,再选中TreePatch节点,进入物理检查器,将Type改为Static.

接着在Bit masks区,将Category mask设为4,在Physics shape区, 将Type改为Bounding Box并设置Scale1.

选中MrPig.scnassets/Coin.scn,再选中Coin节点,进入物理检查器,将Type改为Kinematic.

接着在Bit masks区,将Category mask设为128,在Physics shape区, 将Type改为Bounding Box并设置Scale0.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
  }

}

代码含义:

  1. 添加类扩展,遵守SCNPhysicsContactDelegate协议;
  2. 实现physicsWorld(_:didBegin:)方法;
  3. 只需要关注.playing状态时的情况,其他不处理;
  4. 判断哪个是障碍物,哪个是隐藏的碰撞检测盒子;
  5. 用位运算OR,将碰撞检测盒子的类掩码添加到activeCollisionsBitMask中去;
  6. 实现physicsWorld(_:didEnd:)方法,碰撞结束时调用;
  7. 判断.playing状态;
  8. 判断哪个是障碍物,哪个是隐藏的碰撞检测盒子;
  9. 用位运算符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
}

代码含义:

  1. 用位运算符AND来判断四个方向的隐藏节点是否已经发生了碰撞;
  2. 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()
}

代码含义:

  1. 和前面类似,用来判断哪个是小猪;
  2. 如果是和车辆碰撞,就结束游戏;

处理和金币的碰撞

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秒后重新出现;
  3. 收集金币,增加分数;

运行游戏,现在可以收集金币了


结束处理

更新摄像机位置

打开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()

设置房屋

  1. 创建一个新SceneKit场景,命名为Home.scn,并删除默认的摄像机;
  2. 添加一个House.scn到场景中,放在正中间;
  3. 创建一个空的节点,命名为Obstacles,用来作为容器节点;
  4. 添加一些树;
  5. 添加一个Mini.scn;

参考下图:


103.png
  1. 周围的障碍物Obstacles需要设置物理形体;分类掩码category bit mask设置为4.而House的掩码设置为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)
}

此外,还要再添加一些环境音,在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状态;
  2. 设置MrPig.scnassets/Audio/Traffic.mp3作为流音频的源;
  3. 添加到时根节点时开始播放音频源;
  4. 预加载其他用到的音效;

最后再添加一些音效 跳跃音效:在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")

运行一下, 完成了!!


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270