SpriteKit 是苹果推出的 2D 游戏开发框架,始于 iOS 7,支持大部份🍎平台,提供场景管理、物理引擎、粒子系统等功能,利用 Metal 实现高性能渲染,可以很好地与 GameplayKit 等框架结合。其缺点是不能跨平台且已经很长时间没有更新新功能了,上一次 SpriteKit 有相关 session 还是在 WWDC 17;他的优点是易学好用,虽无更新但仍稳定。
实际上我早就想学习 Game Tech 相关的内容了,还是 17 年的时候就看过 raywenderlich 的 SpriteKit 教程,但由于当时水平不行并没怎么学进去。前段时间也看过一些 Unity、Godot 的内容,但都因为正向反馈比较小没能坚持下去。这次看的是 Apple Game Frameworks and Technologies - Build 2D Games with SpriteKit & Swift,算是很新的书了,作者来自苹果游戏框架团队。
reddit 上有一篇帖子讨论了现如今使用 SpriteKit 是否还有意义?虽然利用这份技术进行商业化的可能性很低,但是它对于我来说,上手以及做出成品的可能性很高,同时也能横向拓宽一点技能。综合这些以及其他原因,🏋️我决定在 2025 年再次学习 SpriteKit。
在有了相关的开发经验之后,相比学习别的游戏框架,这次学习 SpriteKit 的体验还是比较轻松💅的。本文是该书的学习笔记,适合熟悉 UIKit 但对 SpriteKit 一窍不通的读者拿来走🐎观花。
与 SpriteKit 紧密合作的还有 GameplayKit,GameplayKit 不参与动画和视觉渲染等相关内容,它负责提供游戏中常见的游戏机制的实现,比如:实体组件系统(ECS)、状态机、自动寻路、群体跟随移动等。本文不涉及 GameplayKit,将在另一篇文章中讨论其相关的内容。
1. 界面的组成
SpriteKit 使用 SKView 来承载画面内容,展示 SKScene;一般来说我们可以直接使用 UIViewController 的 root view 作为 SKView,同时 SKView 也有一些 HUD 的功能,可以实时显示 FPS、节点数量等数据。鉴于 SKView 也只是一个平平无奇的 view,所以 SpriteKit 是可以和很多 UIKit 的内容一起使用的,经常遇到的有:查看 fps、粒子特效。
在 watchOS,需要使用 WKInterfaceSKScene,在配合 Metal 内容时,需要使用 SKRenderer。
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
let scene = GameScene(size: CGSize(width: 1336, height: 1024))
scene.scaleMode = .aspectFill
scene.backgroundColor = UIColor.white
// Present the scene
view.presentScene(scene)
view.ignoresSiblingOrder = false
view.showsPhysics = true
view.showsFPS = true
view.showsNodeCount = true
}
}
SKScene 用来展示真正的视觉场景,我们可以在 scene 上添加文字、图片、特效等内容。同一个 SKView 可以切换不同的 scene,系统提供了一些内置的动画。为了方便游戏进行过程中镜头移动、适配设备尺寸等需求,scene 的尺寸可以设定成固定大小的,且一般来说都会比 view 的尺寸大。
在 SpriteKit 中,基本在 scene 上的内容都是 SKNode 类,除了能看到的大部分视觉元素 node,还有一些看不见的 node,比如相机节点、引用节点等。相机节点可以调整玩家视角;引用节点有点像 UIKit 里面的 Container View,它可以把一些复杂的节点组合在一起单独放在一个 .sks 文件里,让他作为一个抽象的引用节点在其他的 .sks 文件中使用。
实际上在 SwiftUI 中也是可以优雅地使用 SpriteKit 的,但这本书主要使用的是 UIKit,本文就不僭越了。
2. 动作
为了让 node 能够动起来,我们可以使用 SKAction 来实现。大多数 action 实现了特定的预定义动画,比如平移、旋转、缩放、淡入淡出、切换 texture 、播放声音等,这些动画由 SpriteKit 来完成,如果还需要更自定义的 action 的话,可以创建 customAction,但不要子类化 SKAction。node 在执行完 action 之后会有一个 block 可以执行 action 结束之后的内容,这部分和 UIKit 的动画基本是一样的。
let fadeOut = SKAction.fadeOut(withDuration:2)
node.run(fadeOut) {
skView.presentScene(newScene)
}
当然,在游戏场景下,单纯修改一个属性的动画显得有点不够用了,可能游戏中的角色在旋转平移的同时,还在切换跑步或走路的图片素材,还可能在伴随着发出走路声音,在 SKAction 中,我们可以把 这些所有 actions 链接在一起。
假如一个人一天工作日的生活如上图所示,如果我们使用 SKAction 来实现,可以通过 sequence、group、wait、repeat 的方式,把不同的 action 链接起来,代码可以这样写:
let ☀️ = SKAction.起床
let 🚇 = SKAction.坐地铁
let 💼 = SKAction.上班
let 🍚 = SKAction.吃老乡鸡
let 🐟 = SKAction.摸鱼
let 🥡 = SKAction.吃外卖
let 🎮 = SKAction.玩游戏
let 🐱 = SKAction.撸猫
let 😴 = SKAction.睡觉
let 💼🐟 = SKAction.group([💼, 🐟])
let 🎮🐱 = SKAction.group([🎮, 🐱])
let 🈳 = SKAction.wait(forDuration: 1.0)
someGuy.run(
SKAction.repeat(
SKAction.sequence([☀️, 🚇, 💼, 🍚, 💼🐟, 🥡, 🎮🐱, 🈳, 😴]),
count: 5
)
)
3. 物理世界
谈到游戏离不开物理引擎,SpriteKit 中的 node 可以设置 physicsBody,让节点有一个带形状的碰撞模型。这样在物理引擎生效的时候,body 与 body 之间的接触和碰撞,就可以被观察到。
可以使用 SKFieldNode 来实现类似重力、漩涡力场的场景来使 physicsBody 被移动,也可以直接给 physicsBody 添加一个 SKAction 中的推力,比如 applyForce(_:duration:),来让他往某个方向移动。
设置 node 的 physicsBody shape 也有多种方式,我们需要这边以一架小飞机作为例子:
要注意的点是,虽然大部分东西都不是圆形的,但圆形是性能最好的 shape;texture 的 shape 是基于图片素材的 alpha 通道,所以不可避免会产生很多边和角,这部分会对性能有比较大的影响;也可以像上图第四种,在代码中用点来绘制一个多边形来作为 body shape。
可以将屏幕的边框设置成 Edge-based 的 body,他们只会和非 Edge-based 的 body 发生交互,常用来实现物体不掉出屏幕、空气墙等表现。
在设置 body 的时候,还可以设置 mass,代表这个 body 的质量,比如要模拟大小一样的乒乓球撞铅球的情况时,铅球的质量就应该比乒乓球大的多,这样在物理碰撞时,两球接触后,乒乓球应该会被撞飞,而铅球应该几乎纹丝不动。
上述提到的有质量的物体相互碰撞的场景称之为 collision;在游戏场景下,除了有物体推挤碰撞,还有另一种情况,比如子弹击中了某个障碍物,这个时候障碍物不需要和子弹有物理上力的作用,我们只需要监测到这次接触就可以了,这种情况称之为 contactTest。
你可以给 body 分别设置 category、contactTest 和 collision 的 bitMask,当两个 body 接触的时候,会依照自身设置的 bitMask 来做出是否有碰撞或者接触的回应,在一个场景中,最多可以设置 32 个 bitMask,它的类型是 UInt32。上图中我们需要实现粉色子弹与太空陨石接触后会有回调,绿飞机向右飞行挤压白飞机时两者有正确的物理碰撞效果,代码中是这样实现的:
// UInt32
0b1 => 1
0b10 => 2
0b11 => 3
// Contacts
太空陨石.physicsBody?.categoryBitMask = 0b1
太空陨石.physicsBody?.contactTestBitMask = 0b10
粉色炮弹.physicsBody?.categoryBitMask = 0b10
太空陨石.physicsBody?.contactTestBitMask = 0b1
extension MyScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
if collision == 0b1 | 0b10 {
// handle contact here
}
}
}
// Collosions
绿飞机.physicsBody?.categoryBitMask = 0b11
绿飞机.physicsBody?.collisionBitMask = 0b11
白飞机.physicsBody?.categoryBitMask = 0b11
白飞机.physicsBody?.collisionBitMask = 0b11
4. 生命周期
在 scene 中,你可以通过 action 来让 node 移动、形变。SKAction 已经能做好很多事情了,但总有一些奇怪的需求让我们不得不手动去把握事件发生的时机。在动画发生时,场景中每一帧都在更新,下图是 SpriteKit 会在每一帧做的事情,被称之为 Frame-Cycle Events,开发者可以重写方法,把自己的逻辑,加入到其中。
写一个 SKScene 的子类可以重写帧周期函数;也可以通过实现 SKSceneDelegate 来覆盖原有 scene 上的方法。
5. 瓦片地图
有一个特殊的 node 叫做 SKTileMapNode,他是一个瓦片地图节点,可以很方便的创建和编辑由重复元素组成的场景,在 2D 的横版闯关游戏和俯视角类 RPG 游戏上很有用。SpriteKit 中预设了 4 种不同的网格样式,最常用的就是方块形的 Grid 样式。以下图为例,比如我想创建一片网格 7*7 的草地,我用四张不一样的草地素材设置对应的 Tile Set,设置他们以不同的权重,一键就可以生成地图,让他们随机填充在网格内,有额外需求也可以自行一块块的修改。
除了设置这种普通的 tile Set 之外,还可以设置 8-Way Adjacency Group 类型的 tile set。这种瓦片需要放入 8 个方向共计 13 张图片,有了所有这些方向的素材之后,在地图上填充瓦片,瓦片会自动更新成对应的图片元素。比如下图我填充了场景中的部分瓦片,这些瓦片都是我直接「刷」上去的,我并没有特意设置每个瓦片的图片应该是什么,tile map node 会自动识别边缘、转角等情况,放上对应的瓦片图片。
其他
这本书最后两章使用 GameKit,实现在 Game Center 中的用户登录、首胜、成就、排行榜等功能,并且实现了一个回合制联机的 demo,这些功能对于简单的纯 Apple 平台的游戏来说,是很好的功能补强。但这些内容离我还比较远,就不总结了。
这本书没有中文版,我能在有限时间内做到:读完+跟着书中实践了大部分+查阅官方文档文档+整理本文,很大程度上依仗了沉浸式翻译。所以可以这么说:沉浸式翻译毁了我的英语梦。