什么是SpriteKit
首先要知道什么是Sprite
。顾名思义Sprite就是精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像。
这个图像也许可以移动,用户可以与其交互,也有可能仅只是游戏的一个静止的背景图。总体来说精灵构成了游戏的绝大部分
主体视觉内容,一个2D引擎的主要工作,就是高效地组织,管理和渲染这些精灵。
Hello SpriteKit!
下面直接上SpriteKit的基本用法。
我们用Xocde5内置的SpriteKit模板来构建一个简单的Hello World工程。至于工程名字就采用很多大神都采用的SpriteKitSimpleGame吧。
由于我们需要的是一个横屏游戏,所以在建立工程后,在工程的General
标签中把Depoyment Info
中的Device Orientation
中的Portrait
勾去掉,这样是为了让应用只在横屏下运行。编译并运行,可以看见如下效果:
加入精灵
SpriteKit是基于场景(Scene)来组织的,每个SKView(专门用来呈现SpriteKit的View)中可以渲染和管理一个SKScene,每个Scene中可以装载多个精灵,并管理他们的行为。
现在让我们试着在Scene里加入一个精灵,首先从我们的奥特曼超人开始,下载此
工程资源文件导入到工程中。
我们这次用资源目录(Asset Catalog)来管理资源吧。点击工程中Images.xcassets
,打开Asset Catalog。
将下载解压后sprites.atlas
文件夹中的图片都拖入到打开的资源目录中,资源目录会自动根据文件的命名规则识别图片,
1x的图片将用于iPhone4和iPad3之前的非retina设备,2x的图片将用于retina设备。完成这一步,工程的资源目录会是这样:
现在要开始coding~~
默认的SpriteKit模板做的事情就是在ViewController的self.view中加入并显示了一个SKScene的子类实例MyScene。
犹如我们在App开发时主要代码量会集中在ViewController一样,在用SpriteKit进行游戏开发时,因为所有游戏逻辑和精灵管理都会在Scene中完成,
我们的代码量会集中在SKScene中。在MyScene.m
中用下面内容更换-initWithSize
里的内容:
在这里打印出了场景的size,至于什么原因很快你就会知道。
设置一个场景的背景色,只需要设置backgroundColor
属性,在这里将其设置为白色。
SKColor只是一个define定义而已,在iOS平台下被定义为UIColor,在Mac下被定义为NSColor。
在SpriteKit开发时,尽量使用SK开头的对应的UI类可以统一代码而减少跨iOS和Mac平台的成本。
在SpriteKit
中初始化一个精灵很简单,直接用SKSpriteNode
的spriteNodeWithImageNamed
指定图片名就行。
设定精灵的位置。SpriteKit中的坐标系和其他OpenGL游戏坐标系是一致的,屏幕左下角为(0,0)。
不过需要注意的是不论是横屏还是竖屏游戏,view的尺寸都是按照竖屏进行计算的,即对于iPhone来说在这里传入的sizewidth是320,
height是480或者568,而不会因为横屏而发生交换。因此在开发时,请千万不要使用绝对数值来进行位置设定及计算。
把player加入到当前scene中。
现在让我们编译并运行,看看效果...
OH~My God!!屏幕是白色的,我们的奥特曼超人并没有出现,细心的你也许会发现控制台输出的内容,会看到如下内容:
scene认为自己的宽度是320,高度则是568——实际上刚好相反!
我们来看看具体发生了什么:定位到ViewController.m的viewDidLoad方法:
上面的代码中利用view的边界size创建了场景。不过请注意,当viewDidLoad被调用的时候,view还没被添加到view层级结构中,
因此它还没有响应出布局的改变。所以view的边界可能还不正确,进而在viewDidLoad中并不是开启场景的最佳时机。
提醒
:要想了解更多相关内容,
请看由Rob Mayoff带来的最佳解释
将ViewController.m
中的-viewDidLoad:
方法全部替换成下面的-viewDidAppear:
。
现在我们来重新编译运行,yes~~~主角出现了:
添加怪兽兵团
牡丹虽好,也要绿叶扶持。没有怪兽出现怎么凸显我们的英雄厉害呢?添加怪兽的方式和我们添加英雄几乎一样,
生成精灵、设定位置,加到scene中。最大的区别在于怪兽是能移动和每隔一段时间出现一个。
在MyScene.h中,加入一个方法-addMonster
计算怪兽出生的位置。怪兽从右侧屏幕外随机高度进入屏幕,为了保证怪兽在屏幕范围内,需要指定最大和最小Y值,
然后从这个范围随机一个Y值作为出生点。
设定出生点在右侧屏幕外,然后添加怪兽精灵。
加入随机时间,让怪兽出现有快有慢而不是全部匀速,避免太单调。
建立SKAction。actionMove
负责将精灵在actualDuration
的时间间隔内移动到结束点;
actionMoveDone
负责将精灵移出场景,其实是run一段接收到的block代码。
runAction
方法可以让精灵执行某个操作,而在这里我们要做的是先将精灵移动到结束点,当移动结束后,移除精灵。
这里sequence:
可以让我们顺序执行多个action。
然后尝试在上面的-initWithSize:
里调用这个方法看看结果
Cooooool!! 能够看见能动的图像了。其实游戏的本质就是一大堆能动的图像。
不过目前只有一只怪兽,我之前说好的怪兽兵团差别太大了,我们的英雄也肯定会觉得寂寞难耐。
在-initWithSize:
的5后加上以下代码:
这里声明了一个SKAction序列,run一个Block,然后等待1秒。用-repeatActionForever:
生成一个无限循环的动作序列,然后让scene执行。
这样就每隔一秒调用一次-addMonster
让场景中不断添加敌人。当然你可以使用NSTimer来做同样的事情,不过如果你用NSTimer的,
那你必须自己维护它的状态。
现在让我们再次运行,看看效果:
每个成功奥特曼背后都有一只默默挨打的怪兽
我们打算让用户点击屏幕的某个位置时,就在英雄所在位置发出一枚固定速度的飞镖,当飞镖命中怪兽时,就把怪兽从屏幕中移除。
让我们先来实现发射飞镖。检测用户点击,让后让精灵朝点击点的防线以某个速度移动。在这里我们采用moveTo:duration:
来实现。
在发射之前我们需要做一些基本数学运算,希望大家对相似三角形还有印象。如图,很简单的几何学,具体计算就不作讲解了,若你却是
没有印象,请翻开你遗弃的书本。其实也可以用矢量函数的,可参考矢量方法解释
又要开始coding~~~,在MyScene.m找到-touchesBegan:withEvent:
,使用旗下代码覆盖原来的。
设定精灵位置
将点击的位置转换为node的坐标系的坐标,并计算点击位置和飞镖位置的偏移量。若点击在飞镖初始位置后方,则返回。
根据相似三角形计算屏幕右侧外的结束位置。
移动飞镖,并在移动结束后将飞镖从场景中移除。
碰撞检测
程序运行,你会发现飞镖直接穿过怪兽,毫无杀伤力。这是因为我们没有做碰撞检测。我们要做的时让飞镖和怪兽在接触的时候都移除屏幕,
这样看起来就像飞镖打中怪兽,然后怪兽消失。
基本思路是在每隔一个小的时间间隔,就扫描一遍场景中现存的飞镖和怪物。这里就需要提到SpriteKit中最基本的每一帧的周期概念。
在iOS传统的view的系统中,view的内容被渲染一次后就将一直等待,直到需要渲染的内容发生改变(比如用户发生交互,view的迁移等)的时候,
才进行下一次渲染。这主要是因为传统的view大多工作在静态环境下,并没有需要频繁改变的需求。而对于SpriteKit来说,其本身就是用来制作大
多数时候是动态的游戏的,为了保证动画的流畅和场景的持续更新,在SpriteKit中view将会循环不断地重绘。
动画和渲染的进程是和SKScene对象绑定的,只有当场景被呈现时,这些渲染以及其中的action才会被执行。SKScene实例中,一个循环按执行顺序包括
每一帧开始时,SKScene的-update:方法将被调用,参数是从开始时到调用时所经过的时间。在该方法中,我们应该实现一些游戏逻辑,包括AI,精灵行为等等,
另外也可以在该方法中更新node的属性或者让node执行action
在update执行完毕后,SKScene将会开始执行所有的action。因为action是可以由开发者设定的(还记得runBlock:么),因此在这一个阶段我
们也是可以执行自己的代码的
在当前帧的action结束之后,SKScene的-didEvaluateActions
将被调用,我们可以在这个方法里对结点做最后的调整或者限制,
之后将进入物理引擎的计算阶段。
然后SKScene将会开始物理计算,如果在结点上添加了SKPhysicsBody的话,那么这个结点将会具有物理特性,并参与到这个阶段的计算。
根据物理计算的结果,SpriteKit将会决定结点新的状态。
然后-didSimulatePhysics
会被调用,这类似之前的-didEvaluateActions
。这里是我们开发者能参与的最后的地方,
是我们改变结点的最后机会。
一帧的最后是渲染流程,根据之前设定和计算的结果对整个呈现的场景进行绘制。完成之后,SpriteKit将开始新的一帧。
理论讲解完了,现在让我们回到码农。检测场景上每个怪物和飞镖的状态,如果它们相撞就移除,这是对精灵的计算的和操作,
我们可以将其放到-update:
方法中来处理。此之前,我们需要保存一下添加到场景中的怪物和飞镖,在MyScene.h中加入如下声明:
然后在-initWithSize:
中配置场景之前,初始化这两个数组:
在将怪物或者飞镖加入场景中的同时,分别将它们加入到数组中,
c
-(void) addMonster {
//...
[self.monsters addObject:monster];
}
同时,在将它们移除场景时,将它们移出所在数组,分别在[monster removeFromParent]
和
[projectile removeFromParent]
后加入[self.monsters removeObject:monster]
和
[self.projectiles removeObject:projectile]
。
接下来终于可以在-update:
中检测并移除了:
其实还可以采用Sprite Kit的物理引擎来检测炮弹与怪物的碰撞,留给读者自己改进(其实是我比较懒)。
英雄之歌
音效绝对是游戏的一个重要环节,你最开始下载的资源文件里面有个Sounds文件夹,将里面
整个文件夹拖到工程导航里面,然后勾上“Copy item”。
我么需要在发射飞镖时播放音效,对于音效的播放是十分简单的,SpriteKit为我们提供了一个action,用来播放单个音效。
因为每次的音效是相同的,所以只需要在一开始加载一次action,之后就一直使用这个action,以提高效率。
现在MyScene.h的@interface中加入:
然后在-initWithSize:
一开始的地方加入
最后,修改发射飞镖的action,使播放音效的action和移动精灵的action同时执行。
将-touchesBegan:withEvent:
最后runAction的部分改为:
-sequence:
连接不同的action,使它们顺序串行执行。在这里我们用了另一个方便的方法,-group:
可以范围一个新的action,
这个action将并行同时开始执行传入的所有action。
游戏中音效一般来说至少会有效果音(SE)和背景音(BGM)两种,SE可以用SpriteKit的action来解决,而BGM就要惨一些,
SpriteKit还没有一个BGM的专门的对应方案。所以现在我们使用传统的播放较长背景音乐的方法来实现背景音,那就是用AVAudioPlayer。
在MyScene.h里加入bgmPlayer声明。
然后在-initWithSize:
中加载背景音并一直播放。
AVAudioPlayer用来播放背景音乐相当的合适,唯一的问题是有可能你想在暂停的时候停止这个背景音乐的播放。因为使用的是SpriteKit以外的框架,
而并非action,因此BGM的播放不会随着设置Scene为暂停或者移除这个Scene而停止。想要停止播放,必须手动显式地调用[self.bgmPlayer stop]
。
杀敌数统计和场景切换
目前改游戏已经相当完美,除了一点,即使你用尽最后一格电源来打怪,游戏也不会结束。所以我们需要设定游戏结束规则,打死30只怪兽就胜利,
若怪兽到达屏幕最左边则战斗失败。
为了检测打怪数MyScene.h里添加一个monstersDestroyed
,然后在打中怪物时使这个值+1,并在随后判断如果消灭怪物数量大于等于30,则切换场景。
接下来就是制作新的表示结果的场景了,新建一个SKScene的子类很简单,和平时我们新建Cocoa或者CocoaTouch的类没有什么区别,
选择Objective-C class,然后将新建的文件取名为ResultScene,父类填写为SKScene。在ResultScene.m加入如下代码:
添加了一个SKLabelNode来显示游戏的结果。
在结果标签的下方加入了一个重开一盘的标签。
为这个node进行了命名,通过对node命名,我们可以在之后方便地拿到这个node的参照,而不必新建一个变量来持有它。
最后不要忘了这个方法名写到.h文件中去,不然在游戏场景中就不能调用啦。
回到游戏场景,在Myscens.m中加入ResultScene.h的引用,然后在实现中加入一个切换场景的方法。
之后,将刚才留下来的两个TODO的地方,分别替换为以相应参数对这个方法的调用。
最后,我们想要在ResultScene中点击Retry标签时,重开一盘游戏。在ResultScene.m中加入代码并加入MyScene.h的引用。
到这里本教程已经结束,有兴趣的童鞋可以
下载源代码参考
使用Sprite Kit的物理引擎来检测炮弹与怪物的碰撞
之前曾经提过可以使用Sprite Kit的物理引擎来检测的,不过当时将问题留给各位童鞋。现在突然心血来潮,补上这部分教程。
整体可能看起来可能会比较凌乱,希望各位见谅。
物理世界的配置
。物理世界是一个模拟的空间,用来进行物理计算。默认情况下,在场景(scene)中已经创建好了一个,
我们可以对其做一些属性配置,例如重力感应。
为精灵(sprite)创建对应的物体(physics bodies)
。在Sprite Kit中,为了碰撞检测,我们可以为每个精灵创建
一个相应的形状,并设置一些属性,这就称为物体(physics body)
。注意啦,这个图文的形状一般跟精灵不会一模一样的,
只是形状上給精灵大概相似,这已经足够大多数游戏使用了。
将精灵分类
。在物体(physics body)上可以设置的一个属性是category,该属性是一个位掩码(bitmask)。
通过该属性可以将精灵分类。在本文的游戏中,有两个类别——一类是炮弹,另一类则是怪物。设置之后,当两种物体相互碰撞时,
就可以很容易的通过类别对精灵做出相应的处理。
设置一个contact(触点) delegate
。我们可以在物理世界上设置一个contact delegate
,通过该delegate,
当两个物体碰撞时,可以收到通知。收到通知后,我们可以通过代码检查物体的类别从而做出相应的动作。
现在开始实现。
碰撞检测和物理特性: 实现
在MyScene.m
文件顶部添加两个常量:
设置了两个类别,记住需要用位(bit)的方式表达————一个用于炮弹,另一个则是怪物。
注意:
在Sprite Kit中category是一个32位整数,当做一个位掩码(bitmask)。这种表达方法比较奇特:
在一个32位整数中的每一位表示一种类别(因此最多也就只能有32类)。在这里,第一位表示炮弹,下一位表示怪兽。
接着为进行物理世界配置,在-initWithSize:
中,将下面代码添加在player到添加到场景后:
上面的代码将物理世界的重力感应设置为0,并将场景设置位物理世界的代理(当有两个物体碰撞时,会受到通知)。
为怪兽创建对应的物体,在addMonster
方法中添加:
接着在touchesBegan:withEvent:
方法中设置炮弹位置的代码后面添加如下代码:
在上面的代码中跟之前的类似,只不过有些不同:
1. 炮弹的形状是圆形的。
2. usesPreciseCollisionDetection
属性设置为YES。这对于快速移动的物体非常重要(例如炮弹),如果不这样设置的话,
有可能快速移动的两个物体会直接相互穿过去,而不会检测到碰撞的发生。
下面我们添加炮弹与怪兽接触是的处理方法,这个方法是不会自行调用的,下面会介绍怎样调用它。
由于采用Sprite Kit的物理引擎来检测,所以记分代码也需要做修改。首先原本在update:currentTime
里面的代码全部注释。
下面该实现contact delegate方法了。在这之前请在MyScene.h添加
代码下载