初识SpriteKit:做一个FlappyHaru小游戏

前两天周末闲着,花了两个晚上写了个FlappyHaru,其实就是个FlappyBird游戏,只是我把主角换成了团长Haruhi…
最终的项目我放到了github上.项目地址.

why?

周六交了论文定稿后,想找个游戏玩玩,突然才发现我手机上已经很久没安装过游戏了,跟着我就莫名思考起为什么我手机上没有游戏…周六突然想到之前下载过FlappyBird的iOS版源码,看了一点点就被遗弃在某个文件夹里面了.诶!干脆自己来写一个FlappyBird吧(最后变成Flappy团长),然后我就开始干了.期间找到一个不错的教程,方一雄FlappyBird教程,节约了我后面写这个FlappyHaru不少时间.

spriteKit是什么?

SpriteKit是苹果iOS7新推出的2D游戏引擎,Sprite Kit 提供了包括 动画精灵,形状,粒子(火焰,烟雾),动画,物理效果,音频和视频等.
关于Sprite Kit 得先了解以下的东西.

Scenes(场景)

在cocos2d的游戏场景的视觉层.他提供了物体(如树木,汽车,飞机和头像等)的背景

Actions(动作)

苹果设计的Actions非常简洁,可以实现大部分动画功能,比如一些常见的动作:移动 淡入淡出 缩放 旋转 动画纹理 组动画等等. 当然,也可以自己自定义想要的动作来操作对象.

Physics(物理引擎)

spriteKit提供重力,碰撞等物理效果.这个项目只用到这两点.

##SpriteKit的坐标

通常iOS的界面坐标系中原点(0,0)是在左上角,而在spriteKit中原点是在左下角.并且提供anchorPosition来标识SKSpriteNode的锚点,每个node的锚点可以理解为这个node的固定点.比如打篮球时,你可以拿着篮球以一个脚为固定点,左右的旋转.篮球中你这个脚一动那就是走步犯规,spriteKit中这个你可以自由设置范围是(0,0)到(1,1),默认是(0.5,0.5),即一个node的中心点.
另外,zposition则是一个node在z轴的位置,用来控制node的前后位置.

程序逻辑

创建一个Scenes(场景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func showGameScene()
{
if let skview = self.view as? SKView {
if skview.scene == nil {
// create scene

let whScale = skview.bounds.size.height / skview.bounds.size.width
let scene = GameScene(size: CGSize(width: 320, height: 320 * whScale))
skview.showsFPS = true //显示帧数
skview.showsNodeCount = true //显示节点数
skview.showsPhysics = false //显示物理信息
skview.ignoresSiblingOrder = true //
scene.scaleMode = .AspectFill
skview.presentScene(scene)
}
}
}

创建需要的Node

在一个场景中,放入的各种物体称之为Node(也叫节点),FlappyHaru中背景图,地面,障碍物,haruhi和她的帽子每个都是一个node.下面是背景和前景(地面)的代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func setBackGround() {
let bg = SKSpriteNode(imageNamed: "Background")
bg.anchorPoint = CGPoint(x: 0.5, y: 1.0)
bg.position = CGPoint(x: size.width/2, y: size.height)
bg.zPosition = ImageLayer.backGround.rawValue

worldNode.addChild(bg)

gameStartPoint = size.height - bg.size.height
gameHeight = bg.size.height

let leftdown = CGPoint(x: 0, y: gameStartPoint)
let rightdown = CGPoint(x: size.width, y: gameStartPoint)

self.physicsBody = SKPhysicsBody(edgeFromPoint: leftdown, toPoint: rightdown)
self.physicsBody?.categoryBitMask = physicsLayer.gameGround
self.physicsBody?.collisionBitMask = 0
self.physicsBody?.contactTestBitMask = physicsLayer.gameRole
}

func setFrontground() {
for i in 0..<groundCount {
let fg = SKSpriteNode(imageNamed: "Ground")
fg.anchorPoint = CGPoint(x: 0, y: 1.0)
fg.position = CGPoint(x: 0 + CGFloat(i) * fg.size.width, y: gameStartPoint)
fg.zPosition = ImageLayer.frontGround.rawValue
fg.name = "front"
worldNode.addChild(fg)
}
}

每个SKSpriteNode都有他的name,可以用node的enumerateChildNodesWithName方法来寻找到指定node.

运动和游戏逻辑

把要用到的node添加进去后,界面就基本完成.在override func update(currentTime: NSTimeInterval) 和 override func touchesBegan(touches: Set, withEvent event: UIEvent?)设置界面的刷新和点击效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

guard let click = touches.first else {
return
}
let clickpoint = click.locationInNode(self)

switch currentStatus {
case .mainMenu:
if clickpoint.y < size.height * 0.15 {
toLearning()
}
else if clickpoint.x < size.width/2 {
switchtoTeach()
}
else {
toRating()
}
break
case .teach:
switchtoPlay()
break
case .play:
//主角飞
gameRoleFly()

break
case .falldown:
break
case .showScore:
break
case .end:
switchToNewGame()
break
}
}

//MARK: Update
override func update(currentTime: NSTimeInterval) {

if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
} else {
dt = 0
}
lastUpdateTime = currentTime

switch currentStatus {
case .mainMenu:
break
case .teach:
break
case .play:
updateFrontGround()
updateGameRole()
detectHitBlock()
detecHitGround()
updateScore()
break
case .falldown:
updateGameRole()
detecHitGround()
break
case .showScore:
break
case .end:
break

}
}

在update中先判断游戏的状态,然后在不同的状态下进行界面更新,flappyHaru中主要是更新node的位置.下面是跟新主角haruhi的代码.

1
2
3
4
5
6
7
8
9
10
11
func updateGameRole()
{
let acceleration = CGPoint(x: 0, y: gravity)
roleSpeed = roleSpeed + acceleration * CGFloat(dt)
gameRole.position = gameRole.position + roleSpeed * CGFloat(dt)

//碰到地面 停止
if gameRole.position.y - gameRole.size.height/2 < gameStartPoint {
gameRole.position = CGPoint(x: gameRole.position.x, y: gameStartPoint + gameRole.size.height/2)
}
}

每个node可以设置它的物理大小,下面就是主角haruhi的创建代码.使用CGPathCreateMutable来描绘出haruhi的大致物理大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func setRole()
{
gameRole.position = CGPoint(x: size.width * 0.2, y: gameHeight * 0.4 + gameStartPoint)
gameRole.zPosition = ImageLayer.gameRole.rawValue

let offsetX = gameRole.size.width * gameRole.anchorPoint.x
let offsetY = gameRole.size.height * gameRole.anchorPoint.y

let path = CGPathCreateMutable()

CGPathMoveToPoint(path, nil, 0 - offsetX, 0 - offsetY)
CGPathAddLineToPoint(path, nil, 4 - offsetX, 6 - offsetY)
CGPathAddLineToPoint(path, nil, 6 - offsetX, 16 - offsetY)
CGPathAddLineToPoint(path, nil, 5 - offsetX, 23 - offsetY)
CGPathAddLineToPoint(path, nil, 15 - offsetX, 22 - offsetY)
CGPathAddLineToPoint(path, nil, 19 - offsetX, 18 - offsetY)
CGPathAddLineToPoint(path, nil, 18 - offsetX, 8 - offsetY)
CGPathAddLineToPoint(path, nil, 20 - offsetX, 5 - offsetY)
CGPathAddLineToPoint(path, nil, 19 - offsetX, 1 - offsetY)

CGPathCloseSubpath(path)

gameRole.physicsBody = SKPhysicsBody(polygonFromPath: path)
gameRole.physicsBody?.categoryBitMask = physicsLayer.gameRole
gameRole.physicsBody?.collisionBitMask = 0
gameRole.physicsBody?.contactTestBitMask = physicsLayer.gameBlock | physicsLayer.gameGround

worldNode.addChild(gameRole)
}

使用spriteKit提供的SkAction来实现动画效果.
下面这段代码即是控制haruhi带的帽子的移动:

1
2
3
4
5
6
func gameCapAction() {
let moveup = SKAction.moveByX(0, y: 12, duration: 0.15)
moveup.timingMode = .EaseInEaseOut
let movedown = moveup.reversedAction()
gameCap.runAction(SKAction.sequence([moveup,movedown]))
}

last

写完这个FlappyHaru其实只了解到了spriteKit的一些基础,spriteKit还有很多东西需要学习.
我把难度反复调高调低玩了二十多分钟左右…oh,感觉这游戏真不适合我,也难怪我手机上很久没有游戏了,手机游戏太不适合我了,如果以后遇到特别想做的游戏,我应该才会重新拾起对游戏的热情.

FlappyHaru的具体完整项目地址可以在这里找到.