本节主要讲解如何创建无限循环Endless的星空背景(如下图)、玩家飞船发射子弹,监测子弹击外星敌机的SpriteKit物理碰撞并消灭敌机,以及应用iOS的CoreMotion加速计移动飞船躲避外星敌机(加速计须用真机测试)。

 
Space Battle

此《宇宙大战 Space Battle》教程共分为三系列,

(一)宇宙大战 Space Battle -- 初始建立工程及场景Scene、导入各个SpriteNode精灵、Particle粒子节点及建立背景音乐

(二)宇宙大战 Space Battle -- 宇宙大战 Space Battle -- 无限循环背景Endless、SpriteKit物理碰撞、CoreMotion加速计(你正在此处进行学习)

(三)宇宙大战 Space Battle -- 各个场景SCENE之间的切换、利用UserDefaults统计分数

我们先了解一下何为iOS加速计和陀螺仪

iOS系统提供了加速计和陀螺仪支持,如果iOS设备提供了这些硬件支持,iOS即可通过CoreMotion框架提供的加速计来获取设备当前的加速度数据、陀螺仪数据、所处的磁场以及设备的方位等信息;

对于iOS应用开发者来说,开发传感器应用十分简单,CoreMotion框架的核心API是CMMotionManager,开发者只要创建一个CMMotionManager对象,接下来即可采用定时器周期性地从CMMotionManager对象获取加速度数据、陀螺仪数据等。

一、iOS支持的加速计和陀螺仪

加速计可以测出设备的加速度和重力,内置的陀螺仪还可以获取设备的转动,这些数据都通过CMMotionManager对象来获取。而且采用完全类似的方式来获取设备的加速度数据、陀螺仪数据、磁场数据等。

1、iOS加速计和陀螺仪的理论基础

iOS加速计是一个三轴加速计,这意味着它可以检测三维空间中的运动和重力,因此加速计不仅可以获取用户握持手机的方向(向上还是向下),而且可以感知手机正面向下还是向上。

加速计可以测量设备在特定方向的加速度(使用重力g作为单位),当加速度返回值为1.0时,表明设备在特定方向上感知到1g。

iOS设备的加速计所使用的三轴坐标系统如下:

 
iOS设备的加速计所使用的三轴坐标系统

从上图上可以看出:iOS设备的加速计的三轴坐标系统的X、Y、Z轴定义如下:

  • 沿着手机屏幕顶部向上是Y轴正方向,向下是Y轴负方向;

  • 当手机顶部朝上时,沿着手机屏幕向右是X轴正方向,向左是X轴负方向;

  • 正对手机时,垂直屏幕向外是Z轴正方向,垂直屏幕向里是Z轴负方向;

当手机静止不动时,地球引力将会给予手机1g的加速度。典型的,当用户垂直握持手机切顶部向上时,手机即可检测到大约-1g的加速度:如果用户以45度角握持手机,则1g的加速度将会平均分配到X、Y两个轴上。如果检测到加速度的值远大于1g,即可判断该设备突然发成了运动,比如设备被摇动、坠落等,此时加速度即可在一个或多个轴上检测到较大值。

除了加速度数据之外,iOS还可以获取陀螺仪数据,陀螺仪数据则可表示设备围绕各坐标轴的转动。例如,把手机平放在桌面上,手机在各方向的加速度基本不会改变,此时手机将会检测到Z轴方向有大约-1g的加速度。如果此时对手机进行旋转,手机的加速度依然不会有明显的改变,但手机陀螺仪将会返回绕Z轴发生转动。如果用户垂直握持手机,并绕垂直轴转动,此时手机检测到的加速度值依然不会发生改变,但手机陀螺仪将会检测到绕Y轴发生的转动。

简单来说,陀螺仪数据用于检测设备绕X、Y、Z轴转动时的速度,转动越快,陀螺仪返回的数据越大。iOS还可以获取周围磁场在X、Y、Z轴的强度,磁场强度一微特斯拉为单位。

总结出来,iOS的CMMotionManager大致可获取3种数据:

  • 加速度数据:该数据通过CMAccelerometerData对象来表示。该对象只有一个CMAcceleration结构体类型的acceleration属性,该结构体属性值包含x、y、z三个字段,分别代表设备在X、Y、Z轴方向检测到的加速度值;

  • 陀螺仪数据:该数据通过CMGyroData对象来表示。该对象只有一个CMRotationRate结构体类型的rotationRate属性,该结构体属性值包含x、y、z三个字段,分别代表设备围绕X、Y、Z轴转动的速度;

  • 磁场数据:该数据通过CMMagnetometerData对象来表示。该对象只有一个CMMagneticField结构体类型的magneticField属性,该结构体属性值包含x、y、z三个字段,分别代表设备在X、Y、Z轴方向检测到的磁场强度,以微特斯拉为单位。

除此之外,CMAccelerometerData、CMGyroData、CMMagnetometerData有一个公共的弗雷:CMLogItem,该弗雷定义了timestamp属性,这意味着不管是加速度数据、陀螺仪数据、磁场数据,都可通过timestamp属性来访问程序得到的该数据的时间。

2、iOS应用程序获取加速度数据(本游戏只用到加速计)

 
玩家飞船倾斜设备来调用加速计躲闪外星敌机

为了移动玩家飞船,在这儿你将会用到iPhone的加速计。很遗憾,在similator模拟器上不能用加速计,所以你得在真机上做测试。

你通过倾斜设备来调用加速计。这就是我们在第一节课时,限制设备让它只能是Portait状态的原因(去掉勾选Upside Down)。如果你在倾斜的时候屏幕自动旋转了那还玩毛。

由于有Core Motion的存在,使用加速器变得非常简单,在update()方法,游戏帧数每次刷新的时候都被调用。

首先,添加下面的代码到GameScene.swift里:

import CoreMotion

接着,添加下面的属性:

let motionManager = CMMotionManager() // 加速度计管理器
var xAcceleration:CGFloat = 0 // 存放x左右移动的加速度变量
var yAcceleration:CGFloat = 0

你需要这些属性来追踪加速计的数据。你仅仅只需要追踪x和y轴的信息,z轴在这个游戏里用不到。

接着,添加下面的方法:

//MARK: -- 开启加速度计
func startMonitoringAcceleration(){
if motionManager.isAccelerometerAvailable {
updateAccleration() /// 获取加速度计 } }

//MARK: -- 停止Acceleration
func stopMonitoringAcceleration(){
if motionManager.isAccelerometerAvailable && motionManager.isAccelerometerActive {
motionManager.stopAccelerometerUpdates()
}
}

上述方法,让加速计在可以用的情况下开启和停止。

接着我们在didMove(to view: SKView)添加下面添加代码

startMonitoringAcceleration() /// 开启手机加速计感应

对于停止加速计,合适的地方是一个类型的deinit方法:

stopMonitoringAcceleration()

获取加速计:

func updateAccleration(){

        motionManager.accelerometerUpdateInterval = 0.2 /// 感应时间
motionManager.startAccelerometerUpdates(to: OperationQueue.current!) { (data, error) in
///1. 取得data数据;
guard let accelerometerData = data else {
return
}
///2. 取得加速度
let acceleration = accelerometerData.acceleration
///3. 更新XAcceleration的值
let filterFactor:CGFloat = 0.75 //fiter的加入是很有必要的,这样处理一下得到的数据更加平滑
self.xAcceleration = CGFloat(acceleration.x) * filterFactor + self.xAcceleration * (1 - filterFactor)
self.yAcceleration = CGFloat(acceleration.y) * filterFactor + self.yAcceleration * (1 - filterFactor) }
}
 
SpriteKit框架渲染每一帧的周期Loop流程原理图

接着,我们在SpriteKit框架渲染每一帧的周期Loop中的didSimulatePhysics调用物理特性让飞船改变位置,代码如下:

//MARK: -  手机加速度计感应,在SpriteKit框架渲染每一帧的周期Loop中的didSimulatePhysics调用物理特性让飞船改变位置
override func didSimulatePhysics() {
/// 取得xAcceleration的加速度
/// 速度乘以时间得到应该移动的距离,更新现在飞船应该在的位置
self.playerNode.position.x += self.xAcceleration * 50 /// * 50表示时间
self.playerNode.position.y += self.yAcceleration * 50
// 让player => SpaceShip在屏幕之间滑动 x
// X-Axis X轴水平方向 最小值
// 如果player的x-axis最小值 < player飞船的size.with 1/2 设飞船的最小值为 size.with/2
if self.playerNode.position.x < -self.frame.size.width / 2 + self.playerNode.size.width {
self.playerNode.position.x = -self.frame.size.width / 2 + self.playerNode.size.width
}
// 最大值
if self.playerNode.position.x > self.frame.size.width / 2 - self.playerNode.size.width {
self.playerNode.position.x = self.frame.size.width / 2 - self.playerNode.size.width
}
// Y-Axis Y轴方向
if self.playerNode.position.y > -self.playerNode.size.height {
self.playerNode.position.y = -self.playerNode.size.height
} if self.playerNode.position.y < -self.frame.size.height / 2 + self.playerNode.size.height {
self.playerNode.position.y = -self.frame.size.height / 2 + self.playerNode.size.height
}
}

最终,didSimulatePhysics()将会被调用来更新飞船的位置。

用真机跑一下你的程序吧。你现在已经可以通过倾斜设备来调用加速计来让飞船运动啦!

二、如何创建无限循环Endless的星空背景

 
ENDLESS无限循环背景

红色框中的节点bgNode1,SpriteNode的名称Name BG1 位置为Position(0,0)

bgNode1 = childNode(withName: "BG1") as! SKSpriteNode

黄色框为的节点bgNode2, SpriteNode的名称Name BG2 位置为Position(0,2048)

bgNode2 = childNode(withName: "BG2") as! SKSpriteNode

二个SpriteNode同时向下移动

func  updateBackground(deltaTime:TimeInterval){
// 下移
bgNode1.position.y -= CGFloat(deltaTime * 300)
bgNode2.position.y -= CGFloat(deltaTime * 300)
} override func update(_ currentTime: TimeInterval) {
// 每Frame的时间差
if lastUpdateTimeInterval == 0 {
lastUpdateTimeInterval = currentTime
}
deltaTime = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime // endless 无限循环星空背景
updateBackground(deltaTime: deltaTime)
}
 
二个SpriteNode同时向下移动

当红色框BG1的位置bgNode1.position.y < bgNode1.size.height 的高度(即屏幕的height),把bgNode1移到之间黄色框的位置

/// 第一个背景node
if bgNode1.position.y < -bgNode1.size.height {
bgNode1.position.y = bgNode2.position.y + bgNode2.size.height
}

 
红色框bgNode2.position.y = 2048,黄色框bgNode2.position.y = 0

此时黄色框bgNode2.position.y = 0 位于屏幕的正中央
红色框bgNode1.position.y = 2048 取代之间花黄色框的位置,同理,黄色框再次向下移动时,当黄色框BG2的位置bgNode2.position.y < bgNode2.size.height 的高度(即屏幕的height),把bgNode2
移到之间当前红色框(bgNode1)的位置,代码如下

/// 第二个背景node
if bgNode2.position.y < -bgNode2.size.height {
bgNode2.position.y = bgNode1.position.y + bgNode1.size.height
}

完整的代码如下:

override func update(_ currentTime: TimeInterval) {
// 每Frame的时间差
if lastUpdateTimeInterval == 0 {
lastUpdateTimeInterval = currentTime
}
deltaTime = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime updateBackground(deltaTime: deltaTime) // endless 无限循环星空背景 }
/// command + option + <- (箭头) 折叠 || command + option + -> (箭头) 打开
func updateBackground(deltaTime:TimeInterval){
// 下移
bgNode1.position.y -= CGFloat(deltaTime * 300)
bgNode2.position.y -= CGFloat(deltaTime * 300)
// 第一个背景node
if bgNode1.position.y < -bgNode1.size.height {
bgNode1.position.y = bgNode2.position.y + bgNode2.size.height
}
// 第二个背景node
if bgNode2.position.y < -bgNode2.size.height {
bgNode2.position.y = bgNode1.position.y + bgNode1.size.height
} }

三、SpriteKit物理碰撞

物理碰撞发生在:玩家飞船发射子弹击中外星敌机、发星敌机撞到玩家飞船

SpriteKit SKPhysicsBody类物理体的属性图表:
http://www.ifiero.com/index.php/archives/166

1.Spritekit物理节点categoryBitMask属性

/// 玩家飞船
playerNode.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "Player"), size: SKTexture(imageNamed: "Player").size())
playerNode.physicsBody?.affectedByGravity = false // 不受物理世界的重力影响
playerNode.physicsBody?.isDynamic = true
playerNode.physicsBody?.categoryBitMask = PhysicsCategory.SpaceShip
playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.Alien ///碰撞时发出通知
playerNode.physicsBody?.collisionBitMask = PhysicsCategory.None
/// 子弹;
bulletNode.physicsBody = SKPhysicsBody(circleOfRadius: bulletNode.size.width / 2)
bulletNode.physicsBody?.affectedByGravity = false // 子弹不受重力影响;
bulletNode.physicsBody?.categoryBitMask = PhysicsCategory.BulletBlue
bulletNode.physicsBody?.contactTestBitMask = PhysicsCategory.Alien
bulletNode.physicsBody?.collisionBitMask = PhysicsCategory.None
/// 外星飞船
// 1.设置物理身体
alien.physicsBody = SKPhysicsBody(circleOfRadius: alien.size.width / 2)
// 不受重力影响,自定义飞船移动速度;
alien.physicsBody?.affectedByGravity = false
// 2.设置唯一属性
alien.physicsBody?.categoryBitMask = PhysicsCategory.Alien
// 3.和哪些节点Node发生碰撞后发出通知
alien.physicsBody?.contactTestBitMask = PhysicsCategory.BulletBlue | PhysicsCategory.SpaceShip
alien.physicsBody?.collisionBitMask = PhysicsCategory.None

2.用didBegin来监测碰撞:

 
物理体发生碰撞

didBegin接收playerNode.physicsBody.contactTestMask的碰撞通知:

playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.Alien

//MARK:- 发生碰撞时接收到通知
func didBegin(_ contact: SKPhysicsContact) { let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch contactMask {
/// 子弹vs外星人
case PhysicsCategory.Alien | PhysicsCategory.BulletBlue:
bulletHitAlien(nodeA: contact.bodyA.node as! SKSpriteNode,nodeB: contact.bodyB.node as! SKSpriteNode)
/// 外星人Alien撞击到飞船
case PhysicsCategory.Alien | PhysicsCategory.SpaceShip:
alienHitSpaceShip(nodeA: contact.bodyA.node as! SKSpriteNode, nodeB: contact.bodyB.node as! SKSpriteNode)
default:
break
}
}

我们在函数bulletHitAlien()和alienHitSpaceShip()不用判断标识的大小,即判断 PhyscisCategory.Alien < PhysicsCategory.BulletBlue或者PhyscisCategory.Alien > PhysicsCategory.BulletBlue,但还是要了解一下哪个是nodeA及哪个是nodeB为好,因为接下来的游戏都要运用到。

我们之前定义的struct如下:

struct  PhysicsCategory {
// static let BulletRed :UInt32 = 0x1 << 1 // Alien的子弹
static let BulletBlue:UInt32 = 0x1 << 2
static let Alien :UInt32 = 0x1 << 3
static let SpaceShip :UInt32 = 0x1 << 4
static let None :UInt32 = 0
}

根据上面的struct,物理标识 PhysicsCategory.BulletBlue < PhysicsCategory.Alien,即在didBegin:

func didBegin(_ contact: SKPhysicsContact) {

        let bodyA:SKPhysicsBody
let bodyB:SKPhysicsBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
bodyA = contact.bodyA
bodyB = contact.bodyB
}else{
bodyA = contact.bodyB
bodyB = contact.bodyA
}
/// bodyA.categoryBitMask == PhysicsCategory.BulletBlue ///返回true
/// bodyB.categoryBitMask == PhysicsCategory.Alien ///返回true
}
 if bodyA.categoryBitMask == PhysicsCategory.BulletBlue && bodyB.categoryBitMask == PhysicsCategory.Alien {
/// print("执行代码")
}

于是,我们就可以根据categoryBitMask物理标识来获得碰撞中的物理体了。
我们继续函数bulletHitAlien()和alienHitSpaceShip()的代码:


// MARK: 子弹vs外星人
func bulletHitAlien(nodeA:SKSpriteNode,nodeB:SKSpriteNode){ /// 判断哪个是子弹节点bulletNode,碰撞didBegin没有比较大小时,则会相互切换,也就是A和B互相切换;
if nodeA.physicsBody?.categoryBitMask == PhysicsCategory.BulletBlue {
nodeA.removeAllChildren() /// 移除所有子效果 粒子效果emitter(非常重要)
nodeA.isHidden = true // 子弹隐藏
nodeA.physicsBody?.categoryBitMask = 0 // 设置子弹不会再发生碰撞
nodeB.removeFromParent() // 移除外星人
}else if nodeB.physicsBody?.categoryBitMask == PhysicsCategory.BulletBlue {
nodeA.removeFromParent() // 移除外星人
nodeB.removeAllChildren()
nodeB.isHidden = true
nodeB.physicsBody?.categoryBitMask = 0
}
}
// MARK: 外星人Alien撞击到飞船
func alienHitSpaceShip(nodeA:SKSpriteNode,nodeB:SKSpriteNode){ if (nodeA.physicsBody?.categoryBitMask == PhysicsCategory.Alien || nodeB.physicsBody?.categoryBitMask == PhysicsCategory.Alien) && (nodeA.physicsBody?.categoryBitMask == PhysicsCategory.SpaceShip || nodeB.physicsBody?.categoryBitMask == PhysicsCategory.SpaceShip) {
nodeA.removeFromParent()
nodeB.removeFromParent()
}
}

很棒,我们完成了物理体碰撞,现在运行一下COMMAND+R(请用真机噢,你才可以躲避外星敌机),你就可以看到当二个物理体发生碰撞后,它们都从场景Scene中移除了。

在接下来的下一节,我们就学习当玩家飞船被敌机击中后,游戏结束时如何进行场景切换,记录击中外星敌机的架次了(游戏的分数),还用使用UserDefaults记录游戏最高分 ,当然,还有使用Particle粒子效果给游戏增加酷酷的效果 _。

更多游戏教程:http://www.iFIERO.com
Github游戏代码传送门:https://github.com/apiapia/SpaceBattleSpriteKitGame

iFIERO - (二)宇宙大战 Space Battle -- SpriteKit 无限循环背景Endless、SpriteKit物理碰撞、CoreMotion加速计的更多相关文章

  1. iFIERO - (一) 宇宙大战 SPACE BATTLE — 场景SCENE、SpriteKit精灵、PARTICLE粒子及背景音乐

    开始游戏教程前,首先介绍一下SpriteKit是什么?SpriteKit提供了一个图形渲染和动画的基础结构,你可以使用它让任意类型的纹理图片或者精灵动起来.SpriteKit使用渲染循环,利用图形硬件 ...

  2. (三)宇宙大战 Space Battle -- 场景SCENE切换、UserDefaults统计分数、Particle粒子效果

    此<宇宙大战 Space Battle>SpirteKit手机游戏教程共分为三系列: (一)宇宙大战 Space Battle -- 新建场景Scene.精灵节点.Particle粒子及背 ...

  3. 如何应用SPRITEKIT的CAMERA实现游戏中的ENDLESS无限循环背景

    A Tutorial For How To Use SpriteKit Camera Making Endless Background Player运用Camera节点向前移动的效果 向前舞动 命为 ...

  4. iOS宇宙大战游戏、调试工具、各种动画、AR相册、相机图片编辑等源码

    iOS精选源码 日期时间选择器,swift Space Battle 宇宙大战 SpriteKit游戏源码 LLDebugTool - 便捷的IOS调试工具(新增截屏功能) 相机扫描or长按识别二维码 ...

  5. 少儿编程Scratch第三讲:宇宙大战.枪战游戏

    小朋友这周的表现还算不错.周末多数时间都由我陪(bi)着(zhe)做课本上的数学题,后来还学了英语.任重道远啊,语数外都还得加强,还远不到自己就能取得好成绩的阶段. 上周说好这周要做一个发射炮弹的游戏 ...

  6. 一行代码引入 ViewPager 无限循环 + 页码显示

    (出处:http://www.cnblogs.com/linguanh) 前序: 网上的这类 ViewPager 很多,但是很多都不够好,体现在 bug多.对少页面不支持,例如1~2张图片.功能整合不 ...

  7. iOS无限循环滚动scrollview

    经常有园友会问"博主,有没有图片无限滚动的Demo呀?", 正儿八经的图片滚动的Demo我这儿还真没有,今天呢就封装一个可以在项目中直接使用的图片轮播.没看过其他iOS图片无限轮播 ...

  8. UIScrollView实现图片轮播器及其无限循环效果

    图片轮播器: 一.实现效果 实现图片的自动轮播            二.实现代码 storyboard中布局 代码: 1 #import "YYViewController.h" ...

  9. 安卓开发笔记——自定义广告轮播Banner(实现无限循环)

    关于广告轮播,大家肯定不会陌生,它在现手机市场各大APP出现的频率极高,它的优点在于"不占屏",可以仅用小小的固定空位来展示几个甚至几十个广告条,而且动态效果很好,具有很好的用户& ...

随机推荐

  1. 【洛谷P2470】[SCOI2007]压缩

    压缩 #include<iostream> #include<cstring> #include<cstdio> using namespace std; #def ...

  2. Vue nodejs商城项目-商品列表页面组件

    data(){        return {            goodsList:[], // 商品列表            priceFilter:[ // 价格区间数组          ...

  3. ## `nrm`的安装使用

    作用:提供了一些最常用的NPM包镜像地址,能够让我们快速的切换安装包时候的服务器地址:什么是镜像:原来包刚一开始是只存在于国外的NPM服务器,但是由于网络原因,经常访问不到,这时候,我们可以在国内,创 ...

  4. 菜鸟笔记 -- Chapter 6 面向对象

    在Java语言中经常被提到的两个词汇是类与对象,实质上可以将类看作是对象的载体,它定义了对象所具有的功能.学习Java语言必须要掌握类与对象,这样可以从深层次去理解Java这种面向对象语言的开发理念, ...

  5. 9.异常Exception

    9.1 异常概述 package exception; /* * 异常:程序运行的不正常情况 * * Throwable: 异常的超类 * |-Error * 严重问题,这种问题我们通过异常处理是不能 ...

  6. 微信小程序学习笔记(一)

    1.目录及文件构成 1.1 根目录下 ** app.js 是小程序的脚本代码,用来监听并处理小程序的生命周期函数.声明全局变量. ** app.json 是对整个小程序的全局配置,配置小程序是由哪些页 ...

  7. css3 笔记

    1.元字符使用: []: 全部可选项 ||:并列 |:多选一 ?: 0个或者一个 *:0个或者多个 {}: 范围 2.CSS3属性选择器: E[attr]:存在attr属性即可: E[attr=val ...

  8. kubeadm常见报错和解决方法

    k8s随着社区不断壮大国内使用率现在也是比较高的,常用的部署方式主要还是以二进制和kubeadm为主,当然1.13之前大部分人还是以二进制,但是随着版本更新kubeadm已经逐步适用于生成环境,由于k ...

  9. 一个关于 json ,加密,测试,集多功能为一体的在线工具

    很多情况下我们会序列化json或者解析json,那么要解析json也许使用json实体类会好很多,或者你有时候需要压缩转义json的时候, 有一个网站真的是非常好用,里面什么都有......是真的啥都 ...

  10. ethereum(以太坊)(基础)--容易忽略的坑(三)

    pragma solidity ^0.4.10; contract Byte{ bytes [] public T=new bytes[](3); function setLeng(uint len) ...