用Swift和SpriteKit开发iOS游戏
2014-10-28 00:00
736 查看
之前用SpriteKit做过一个叫做ColorAtom的小游戏,用了访问者模式处理碰撞检测,还用了SpriteKit中的粒子系统、连接体、力场和动画等,可以说是一个学习SpriteKit比较不错的Demo,随着Swift的火热,我也用Swift和SpriteKit写了一个更为简单的小游戏Spiral
附上Spiral的动图:
游戏规则是:玩家是五角星小球,小球自动沿着陀螺线向外运动,当玩家点击屏幕时五角星小球会跳跃到内层螺旋,当五角星小球碰到红色旋风或滚动到螺旋线终点时游戏结束。玩家吃掉绿色旋风来得2分,吃到紫色三角得一分并获得保护罩,保护罩用来抵挡一次红色旋风。随着分数的增加游戏会升级,速度加快。游戏结束后可以截屏分享到社交网络,也可以选择重玩。
以下是本文内容:
准备工作
绘制基本界面
Swift中用访问者模式处理碰撞
界面数据显示
按钮的绘制和截图分享
准备工作
SpriteKit是苹果iOS7新推出的2D游戏引擎,这里不再过多介绍。我们新建工程的时候选取iOS中的Game,然后选择SpriteKit作为游戏引擎,语言选择Swift,Xcode6会为我们自动创建一个游戏场景GameScene,它包含GameScene.swift和GameScene.sks两个文件,sks文件可以让我们可视化拖拽游戏控件到场景上,然后再代码中加载sks文件来完成场景的初始化:
但我比较喜欢纯写代码的方式来搭接面,因为sks文件作为游戏场景布局还不成熟,它是iOS8新加入的功能,以前在iOS7的时候sks文件只是作为粒子系统的可视化编辑文件。
所以我们修改GameViewController.swift文件的viewDidLoad()函数,像以前那样直接用代码加载游戏场景:
GameScene虽然是Xcode自动生成的,但是只是个空架子,我们需要把它生成的没用的代码删掉,比如初始化函数里内容为“HelloWorld”的SKLabelNode,还有touchesBegan(touches: NSSet, withEvent event: UIEvent)方法中绘制飞船的代码。把这些删光后,我们还需要有图片素材来绘制这四类精灵节点:Player(五角星),Killer(红色旋风),Score(绿色旋风)和Shield(紫色三角)。我是用Sketch来绘制这些矢量图形的,文件名为spiral.sketch,随同工程文件一同放到GitHub上了。当然你不需要手动导出图片到工程,直接下载工程文件就好了:
https://github.com/yulingtianxia/Spiral
绘制基本界面
这部分的工作主要是绘制出螺旋线作为地图,并让四种精灵节点动起来。
螺旋线的绘制
SKNode有一个子类SKShapeNode,专门用于绘制线条的,我们新建一个Map类,继承SKShapeNode。下面我们需要生成一个CGPath来赋值给Map的path属性:
算法很简单,就是顺时针计算点坐标然后画线,这里把每一步的坐标都存入了points数组里,是为了以后计算其他数据时方便。因为这部分算法不难而且不是我们的重点,这里不过多介绍了。
四种精灵的绘制
因为四种精灵都是沿着Map类的路径来顺时针运动,它们的动画绘制是相似的,所以我建立了一个Shape类作为基类来绘制动画,它继承于SKSpriteKit类,并拥有半径(radius)、移动速度(moveSpeed)和线段计数(lineNum)这三个属性。其中lineNum是用于标记精灵在螺旋线第几条线段上的,这样比较方便计算动画的参数。
[code=cpp; gutter: true; first-line: 1">class Shape: SKSpriteNode {
let radius:CGFloat = 10
var moveSpeed:CGFloat = 50
var lineNum = 0
init(name:String,imageName:String){
super.init(texture: SKTexture(imageNamed: imageName),color:SKColor.clearColor(), size: CGSizeMake(radius*2, radius*2))
self.physicsBody = SKPhysicsBody(circleOfRadius: radius)
self.physicsBody.usesPreciseCollisionDetection = true
self.physicsBody.collisionBitMask = 0
self.physicsBody.contactTestBitMask = playerCategory|killerCategory|scoreCategory
moveSpeed += CGFloat(Data.speedScale) * self.moveSpeed
self.name = name
self.physicsBody.angularDamping = 0
}
}
构造函数中设定了Shape类的一些物理参数,比如物理体的形状大小,碰撞检测掩码等。这里设定usesPreciseCollisionDetection为true是为了增加碰撞检测的精度,常用于体积小速度快的物体。collisionBitMask属性标记了需要模拟物理碰撞的类别,contactTestBitMask属性标记了需要检测到碰撞的类别。这里说的“类别”指的是物体的类别:
let playerCategory:UInt32 = 0x1 << 0;
let killerCategory:UInt32 = 0x1 << 1;
let scoreCategory:UInt32 = 0x1 << 2;
let shieldCategory:UInt32 = 0x1 << 3;
这种用位运算来判断和存储物体类别的方式很常用,上面这段代码写在了NodeCategories.swift文件中。
为了描述Shape的速度随着游戏等级上升而增加,这里速度的计算公式含有Data.speedScale作为参数,关于Data“类”在后面会讲到。
为了让精灵动起来,需要知道动画的移动目的地是什么。虽然SKAction有followPath(path: CGPath?, speed: CGFloat)方法,但是在这里并不实用,因为Player会经常改变路线,所以我写了一个runInMap(map:Map)方法让精灵每次只移动到路径上的下一个节点(之前Map类存储的points属性用到了吧!嘿嘿)
func calDistanceInMap(map:Map)->CGFloat{
if self.lineNum==map.points.count {
return 0
}
switch lineNum%4{
case 0:
return position.y-map.points[lineNum+1].y
case 1:
return position.x-map.points[lineNum+1].x
case 2:
return map.points[lineNum+1].y-position.y
case 3:
return map.points[lineNum+1].x-position.x
default:
return 0
}
}
到此为止Shape类完成了,Killer、Score和Shield类比较简单,继承Shape类并设置自身纹理和类别即可:
class Player: Shape {
var jump = false
var shield:Bool = false {
willSet{
if newValue{
self.texture = SKTexture(imageNamed: "player0")
}
else{
self.texture = SKTexture(imageNamed: "player")
}
}
}
convenience init() {
self.init(name:"Player",imageName:"player")
self.physicsBody.categoryBitMask = playerCategory
self.moveSpeed = 70
self.lineNum = 3
}
func restart(map:Map) {
self.alpha = 1
self.removeAllActions()
self.lineNum = 3
self.moveSpeed = 70
self.jump = false
self.shield = false
self.position = map.points[self.lineNum]
self.runInMap(map)
}
}
Player类的初始位置是螺旋线第四个节点,而且移动速度要略快于其他三种精灵,所以在这里设置为70(Shape默认速度50)。jump和shield是用来标记Player当前状态的属性,其中shield属性还定义了属性监察器,这是Swift中存储属性具有的响应机制,类似于KVO。在shield状态改变时也同时改变Player的纹理。需要注意的是构造器中对属性的改变并不会调用属性检查器,在willSet和didSet中改变自身属性也不会调用属性检查器,因为那样会造成死循环。
restart(map:Map)方法用于在游戏重新开始时重置Player的相关数据。
Swift中用访问者模式处理碰撞
访问者模式是双分派(Double Dispatch)模式的一种实现,关于双分派模式的详细解释,参考我的另一篇文章:Double Dispatch模式及其在iOS开发中实践,里面包含了C++,Java和Obje-C的实现,这次我们用Swift实现访问者模式。
因为SpriteKit中物理碰撞检测到的都是SKPhysicsBody,所以我们的被访问者需要包含一个SKPhysicsBody对象:
class VisitablePhysicsBody{
let body:SKPhysicsBody
init(body:SKPhysicsBody){
self.body = body
}
func acceptVisitor(visitor:ContactVisitor){
visitor.visitBody(body)
}
}
acceptVisitor方法传入的是一个ContactVisitor类,它是访问者的基类(也相当于接口),访问者的visitBody(body:SKPhysicsBody)方法会根据传入的body实例来推断出被访问者的真实类别,然后调用对应的方法来处理碰撞:
func visitBody(body:SKPhysicsBody){
//第二次dispatch,通过构造方法名来执行对应方法
// 生成方法名,比如"visitPlayer"
var contactSelectorString = "visit" + body.node.name + ":"
let selector = NSSelectorFromString(contactSelectorString)
if self.respondsToSelector(selector){
dispatch_after(0, dispatch_get_main_queue(), {
NSThread.detachNewThreadSelector(selector, toTarget:self, withObject: body)
})
}
}
Swift废弃了performSelector方法,所以这里耍了个小聪明来将消息传给具体的访问者。有关Swift中替代performSelector的方案,参见这里
下面让GameScene实现SKPhysicsContactDelegate协议:
protocol DisplayData{
func updateData()
func levelUp()
func gameOver()
func restart()
}
下面是Data结构体代码,大量使用了存储属性的监察器来响应数据变化:
附上Spiral的动图:
游戏规则是:玩家是五角星小球,小球自动沿着陀螺线向外运动,当玩家点击屏幕时五角星小球会跳跃到内层螺旋,当五角星小球碰到红色旋风或滚动到螺旋线终点时游戏结束。玩家吃掉绿色旋风来得2分,吃到紫色三角得一分并获得保护罩,保护罩用来抵挡一次红色旋风。随着分数的增加游戏会升级,速度加快。游戏结束后可以截屏分享到社交网络,也可以选择重玩。
以下是本文内容:
准备工作
绘制基本界面
Swift中用访问者模式处理碰撞
界面数据显示
按钮的绘制和截图分享
准备工作
SpriteKit是苹果iOS7新推出的2D游戏引擎,这里不再过多介绍。我们新建工程的时候选取iOS中的Game,然后选择SpriteKit作为游戏引擎,语言选择Swift,Xcode6会为我们自动创建一个游戏场景GameScene,它包含GameScene.swift和GameScene.sks两个文件,sks文件可以让我们可视化拖拽游戏控件到场景上,然后再代码中加载sks文件来完成场景的初始化:
extension SKNode { class func unarchiveFromFile(file : NSString) -> SKNode? { let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") var sceneData = NSData.dataWithContentsOfFile(path, options: .DataReadingMappedIfSafe, error: nil) var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData) archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene") let scene = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as GameScene archiver.finishDecoding() return scene } }
但我比较喜欢纯写代码的方式来搭接面,因为sks文件作为游戏场景布局还不成熟,它是iOS8新加入的功能,以前在iOS7的时候sks文件只是作为粒子系统的可视化编辑文件。
所以我们修改GameViewController.swift文件的viewDidLoad()函数,像以前那样直接用代码加载游戏场景:
override func viewDidLoad() { super.viewDidLoad() // Configure the view. let skView = self.view as SKView /* Sprite Kit applies additional optimizations to improve rendering performance */ skView.ignoresSiblingOrder = true let scene = GameScene(size: skView.bounds.size) /* Set the scale mode to scale to fit the window */ scene.scaleMode = .AspectFill skView.presentScene(scene) }
GameScene虽然是Xcode自动生成的,但是只是个空架子,我们需要把它生成的没用的代码删掉,比如初始化函数里内容为“HelloWorld”的SKLabelNode,还有touchesBegan(touches: NSSet, withEvent event: UIEvent)方法中绘制飞船的代码。把这些删光后,我们还需要有图片素材来绘制这四类精灵节点:Player(五角星),Killer(红色旋风),Score(绿色旋风)和Shield(紫色三角)。我是用Sketch来绘制这些矢量图形的,文件名为spiral.sketch,随同工程文件一同放到GitHub上了。当然你不需要手动导出图片到工程,直接下载工程文件就好了:
https://github.com/yulingtianxia/Spiral
绘制基本界面
这部分的工作主要是绘制出螺旋线作为地图,并让四种精灵节点动起来。
螺旋线的绘制
SKNode有一个子类SKShapeNode,专门用于绘制线条的,我们新建一个Map类,继承SKShapeNode。下面我们需要生成一个CGPath来赋值给Map的path属性:
import UIKit import SpriteKit class Map: SKShapeNode { let spacing:CGFloat = 35 var points:[CGPoint] = [] convenience init(origin:CGPoint,layer:CGFloat){ var x:CGFloat = origin.x var y:CGFloat = origin.y var path = CGPathCreateMutable() self.init() CGPathMoveToPoint(path, nil, x, y) points.append(CGPointMake(x, y)) for index in 1..<layer{ y-=spacing*(2*index-1) CGPathAddLineToPoint(path, nil , x, y) points.append(CGPointMake(x, y)) x-=spacing*(2*index-1) CGPathAddLineToPoint(path, nil , x, y) points.append(CGPointMake(x, y)) y+=spacing*2*index CGPathAddLineToPoint(path, nil , x, y) points.append(CGPointMake(x, y)) x+=spacing*2*index CGPathAddLineToPoint(path, nil , x, y) points.append(CGPointMake(x, y)) } self.path = path self.glowWidth = 1 self.antialiased = true CGPathGetCurrentPoint(path) } }
算法很简单,就是顺时针计算点坐标然后画线,这里把每一步的坐标都存入了points数组里,是为了以后计算其他数据时方便。因为这部分算法不难而且不是我们的重点,这里不过多介绍了。
四种精灵的绘制
因为四种精灵都是沿着Map类的路径来顺时针运动,它们的动画绘制是相似的,所以我建立了一个Shape类作为基类来绘制动画,它继承于SKSpriteKit类,并拥有半径(radius)、移动速度(moveSpeed)和线段计数(lineNum)这三个属性。其中lineNum是用于标记精灵在螺旋线第几条线段上的,这样比较方便计算动画的参数。
[code=cpp; gutter: true; first-line: 1">class Shape: SKSpriteNode {
let radius:CGFloat = 10
var moveSpeed:CGFloat = 50
var lineNum = 0
init(name:String,imageName:String){
super.init(texture: SKTexture(imageNamed: imageName),color:SKColor.clearColor(), size: CGSizeMake(radius*2, radius*2))
self.physicsBody = SKPhysicsBody(circleOfRadius: radius)
self.physicsBody.usesPreciseCollisionDetection = true
self.physicsBody.collisionBitMask = 0
self.physicsBody.contactTestBitMask = playerCategory|killerCategory|scoreCategory
moveSpeed += CGFloat(Data.speedScale) * self.moveSpeed
self.name = name
self.physicsBody.angularDamping = 0
}
}
构造函数中设定了Shape类的一些物理参数,比如物理体的形状大小,碰撞检测掩码等。这里设定usesPreciseCollisionDetection为true是为了增加碰撞检测的精度,常用于体积小速度快的物体。collisionBitMask属性标记了需要模拟物理碰撞的类别,contactTestBitMask属性标记了需要检测到碰撞的类别。这里说的“类别”指的是物体的类别:
let playerCategory:UInt32 = 0x1 << 0;
let killerCategory:UInt32 = 0x1 << 1;
let scoreCategory:UInt32 = 0x1 << 2;
let shieldCategory:UInt32 = 0x1 << 3;
这种用位运算来判断和存储物体类别的方式很常用,上面这段代码写在了NodeCategories.swift文件中。
为了描述Shape的速度随着游戏等级上升而增加,这里速度的计算公式含有Data.speedScale作为参数,关于Data“类”在后面会讲到。
为了让精灵动起来,需要知道动画的移动目的地是什么。虽然SKAction有followPath(path: CGPath?, speed: CGFloat)方法,但是在这里并不实用,因为Player会经常改变路线,所以我写了一个runInMap(map:Map)方法让精灵每次只移动到路径上的下一个节点(之前Map类存储的points属性用到了吧!嘿嘿)
func calDistanceInMap(map:Map)->CGFloat{
if self.lineNum==map.points.count {
return 0
}
switch lineNum%4{
case 0:
return position.y-map.points[lineNum+1].y
case 1:
return position.x-map.points[lineNum+1].x
case 2:
return map.points[lineNum+1].y-position.y
case 3:
return map.points[lineNum+1].x-position.x
default:
return 0
}
}
到此为止Shape类完成了,Killer、Score和Shield类比较简单,继承Shape类并设置自身纹理和类别即可:
class Player: Shape {
var jump = false
var shield:Bool = false {
willSet{
if newValue{
self.texture = SKTexture(imageNamed: "player0")
}
else{
self.texture = SKTexture(imageNamed: "player")
}
}
}
convenience init() {
self.init(name:"Player",imageName:"player")
self.physicsBody.categoryBitMask = playerCategory
self.moveSpeed = 70
self.lineNum = 3
}
func restart(map:Map) {
self.alpha = 1
self.removeAllActions()
self.lineNum = 3
self.moveSpeed = 70
self.jump = false
self.shield = false
self.position = map.points[self.lineNum]
self.runInMap(map)
}
}
Player类的初始位置是螺旋线第四个节点,而且移动速度要略快于其他三种精灵,所以在这里设置为70(Shape默认速度50)。jump和shield是用来标记Player当前状态的属性,其中shield属性还定义了属性监察器,这是Swift中存储属性具有的响应机制,类似于KVO。在shield状态改变时也同时改变Player的纹理。需要注意的是构造器中对属性的改变并不会调用属性检查器,在willSet和didSet中改变自身属性也不会调用属性检查器,因为那样会造成死循环。
restart(map:Map)方法用于在游戏重新开始时重置Player的相关数据。
Swift中用访问者模式处理碰撞
访问者模式是双分派(Double Dispatch)模式的一种实现,关于双分派模式的详细解释,参考我的另一篇文章:Double Dispatch模式及其在iOS开发中实践,里面包含了C++,Java和Obje-C的实现,这次我们用Swift实现访问者模式。
因为SpriteKit中物理碰撞检测到的都是SKPhysicsBody,所以我们的被访问者需要包含一个SKPhysicsBody对象:
class VisitablePhysicsBody{
let body:SKPhysicsBody
init(body:SKPhysicsBody){
self.body = body
}
func acceptVisitor(visitor:ContactVisitor){
visitor.visitBody(body)
}
}
acceptVisitor方法传入的是一个ContactVisitor类,它是访问者的基类(也相当于接口),访问者的visitBody(body:SKPhysicsBody)方法会根据传入的body实例来推断出被访问者的真实类别,然后调用对应的方法来处理碰撞:
func visitBody(body:SKPhysicsBody){
//第二次dispatch,通过构造方法名来执行对应方法
// 生成方法名,比如"visitPlayer"
var contactSelectorString = "visit" + body.node.name + ":"
let selector = NSSelectorFromString(contactSelectorString)
if self.respondsToSelector(selector){
dispatch_after(0, dispatch_get_main_queue(), {
NSThread.detachNewThreadSelector(selector, toTarget:self, withObject: body)
})
}
}
Swift废弃了performSelector方法,所以这里耍了个小聪明来将消息传给具体的访问者。有关Swift中替代performSelector的方案,参见这里
下面让GameScene实现SKPhysicsContactDelegate协议:
protocol DisplayData{
func updateData()
func levelUp()
func gameOver()
func restart()
}
下面是Data结构体代码,大量使用了存储属性的监察器来响应数据变化:
相关文章推荐
- 用Swift和SpriteKit开发iOS游戏
- 初探使用iOS 7 Sprite Kit与Cocos2d开发游戏的对比
- 初探使用iOS 7 Sprite Kit与Cocos2d开发游戏的对比
- Swift版iOS游戏框架Sprite Kit基础教程下册
- iOS游戏框架Sprite Kit基础教程——Swift版上册
- ios游戏开发 Sprite Kit教程:初学者 1
- Sprite Kit Swift游戏开发新手指导手册(4)
- ios游戏开发 Sprite Kit教程(一)
- ios游戏开发 Sprite Kit教程:初学者 3
- iOS游戏框架Sprite Kit基础教程——Swift版上册第一章-pdf
- ios游戏开发 Sprite Kit教程:初学者 3
- 初探使用iOS 7 Sprite Kit与Cocos2d开发游戏的对比
- Swift开发Sprite Kit游戏实践(三):物理推力与碰撞检测
- Swift开发Sprite Kit游戏实践(四):背景音乐与Game Over
- WWDC 2013 Session笔记 - SpriteKit快速入门和新时代iOS游戏开发指南
- ios游戏开发 Sprite Kit教程:初学者 1
- SpriteKit快速入门和新时代iOS游戏开发指南
- 初探使用iOS 7 Sprite Kit与Cocos2d开发游戏的对比(一家之言)
- iOS开发- 游戏屏幕适配(SpriteKit)
- ios游戏开发 Sprite Kit教程(二)