前言
在上一篇文章中,我们已经了解了通过 UIKit 可以模拟的物理场景,相对来说比较有限,但在完成一些简单需求的时还能稍微应付一下。
在这篇文章中,我们将关注 SpriteKit 的初体验,如何从零开始搭建出符合 SpriteKit 开发哲学的物理世界。
初体验
工程创建
通过 Xcode 创建新工程时,我们不需要使用 Xcode 提供的默认 Game 模版,因为我们的这个游戏本质上是基于 app 的架构去实现的,底层驱动也是 Cocoa Touch 框架,只不过我们需要通过 SpriteKit 中几个特殊的场景类来承载具体的游戏逻辑实现,因此,选择 signle View 模版工程即可。
(为了偷懒,我还是选择的 Game 模版...
性能监控
在进行游戏开发时,我们最需要关心的就是「性能」本身,很多人认为现在设备硬件条件已经非常好了,可以不用太关注性能,但从我个人的角度出发,如果在出发某个场景你写的逻辑渲染耗时 500ms,而我经过优化的逻辑只需要 100ms 即可完成渲染,这应该就是程序员的追求吧~
在我们新建的项目中开启性能监控非常简单。任何基于 SpriteKit 的「物体」想要添加到其中的物理世界中,我们需要一个「容器」去承载,而这个容器在 SpriteKit 中就是 SKView。
因此,我们对游戏的性能监控也回落到了对某个 SKView 的性能监控上,删除 Game 模版中的多余代码后,整理如下:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
let scene = GameScene(size: view.frame.size)
scene.scaleMode = .aspectFill
view.presentScene(scene)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override var prefersStatusBarHidden: Bool {
return true
}
}
此时我的 GameScene 里调整如下,通过 SKShapeNode 创建了一个小球:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
let ball = SKShapeNode(circleOfRadius: 10)
ball.fillColor = .red
addChild(ball)
ball.position = CGPoint(x: size.width / 2, y: 40)
}
}
SKView or SKScene
大部分初学者会对 SpriteKit 中的这两个类感到困惑,SKView 继承自 UIView,是 UIView 的子类,而 SKScene 的最终父类是 SKNode,SKNode 和 UIView 是两个完全不同的类型。
SKScene 可能会与我们常规的思维不太一样,因为它不是继承自 UIView,因此也就没有所谓的 viewWillxxx 等方法,取而代之的 didMove 方法。我们可以在这个方法中作为初始化场景的入口:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var contentCreated = false
override func didMove(to view: SKView) {
if !contentCreated {
createContent()
contentCreated = true
}
}
private func createContent() {
let ball = SKShapeNode(circleOfRadius: 10)
ball.fillColor = .red
addChild(ball)
ball.position = CGPoint(x: size.width / 2, y: 40)
}
}
需要注意的是 SpriteKit 会自动划分在具备使用 Metal 渲染引擎的设备上开启 Metal 渲染,在不具备使用的设备上使用 OpenGL ES 进行渲染。SpriteKit 相当于是一个独立与底层硬件的 framework,只需要提供渲染接口即可工作,也就是说,我们不需要手动管理让 SpriteKit 和哪一个渲染引擎进行关联,这一切都是全自动的。
在上面的代码中,我使用了一个 contentCreated 变量在 didMove 方法中进行了标记,这是因为我们的 BGPlayScene 有可能会多次被重复添加到某个 SKView 上,底层的渲染引擎会自动协助我们缓存已经被渲染过的内容,这样可以节省提高一定的性能。
就像刚才我所说的一样,SpriteKit 是一个独立的 framework,在 iOS 和 macOS 平台上也会自动抹掉平台差异性,比如我对 BGPlayView 设置的背景颜色,我不需要区分当前工程运行的环境到底是哪个平台,因为 SpriteKit 会帮助我们自动将 .blue 根据工程运行的平台转换为对应的 UIColor 或者 NSColor。
在上文的代码中,我们已经通过 SKShapeNode 来创建出一个「精灵」,也就是我们的后边会用到的小球。
加入重力
如果我们想要给一个 SKSpriteNode 具有物理特性,需要创建一个 SKPhysicsBody 对象,然后赋给节点的 physicsBody 属性。
class GameScene: SKScene {
// ...
private func createContent() {
let ball = SKShapeNode(circleOfRadius: 10)
ball.fillColor = .red
addChild(ball)
ball.position = CGPoint(x: size.width / 2, y: 400)
ball.physicsBody = SKPhysicsBody(rectangleOf: ball.frame.size)
}
}
得益于 SpriteKit 框架的便捷,当我们将一个 SKPhysicsBody 关联到 SKShapeNode 上时,被关联的精灵的运动将自动符合物理学特征。这会导致精灵出现以下情况:
- SpriteKit 的物理引擎开始跟踪施加在这个物体上的一切外力,比如重力;
- 每一帧上都会根据这些外力对该精灵的应该处在的位置和角度进行计算;
- 该精灵会和其它同样关联了
SKPhysicsBody的经历发生碰撞。
此时,我们运行代码,会发现红色的小球直接掉出屏幕,因为我们还未给 GameScene 中添加地面。我们需要创建一个固定的精灵,它是静止不动的,以便其它物体能够撞在它上面。
class GameScene: SKScene {
// ...
private func createContent() {
let ball = SKShapeNode(circleOfRadius: 10)
ball.fillColor = .red
addChild(ball)
ball.position = CGPoint(x: size.width / 2, y: 400)
ball.physicsBody = SKPhysicsBody(rectangleOf: ball.frame.size)
let ground = SKSpriteNode(color: .gray, size: CGSize(width: size.width, height: 200))
ground.position = CGPoint(x: size.width / 2, y: ground.size.height / 2)
addChild(ground)
ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
ground.physicsBody?.isDynamic = false
}
}
此时运行工程,我们可以看见小球下落时停在了地面上。在 SpriteKit 中有两种物体。运动物体,能够受外力影响,能够在场景中运动。静止物体,不受外力影响,固定在一个地方,运动物体能够和它发生碰撞。
在上文的代码中,我们将 ground 的 physicsBody 属性 isDynamic 设置为 false,这个物体将不再受外力的影响,同时立即停止运动和旋转(如果之前是在运动的话),但是,我们依然可以通过改变 ground 的位置和角度或使用动作 SKAction 来改变它的位置。
修改精灵刚体形状
现在,我们小球具备了刚体属性,但小球是圆形的,而小球的刚体外形却不是圆形的,我们可以通过打开 SKView 的 showsPhysics 属性来进行查看。
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
// ...
view.showsPhysics = true
}
}
}
运行工程,通过一个 for 循环来增加掉落在地面上的小球数量。可以发现,此时众多小球的刚体外形是矩形,我们应该调整其刚体外形为圆形。
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private func createContent() {
for _ in 0..<10 {
let ball = SKShapeNode(circleOfRadius: 10)
ball.fillColor = .red
addChild(ball)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 10)
ball.position = CGPoint(x: size.width / 2, y: 400)
}
// ...
}
}
修改精灵的速度
如果我们想要改变小球的速度,可以通过修改小球精灵 physicsBody 的 velocity 的属性,这是修改物体速度的最简单方法,这是一个 CGVector 类型的属性,以像素/秒为单位表示移动速度。
ball.physicsBody?.velocity = CGVector(dx: 200, dy: 200)
注意,直接修改小球速度确实能达到一个很好的效果,我们可以用这种方式设置物体的初速度,这一点非常重要!!!对后续从发射台发射小球时的帮助非常大!
创建一个墙壁
创建墙壁最有效的方式是使用「边缘碰撞体」。
class GameScene: SKScene {
private func createContent() {
// ...
let wall = SKNode()
wall.position = CGPoint(x: 0, y: 0)
wall.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: 0, y: 0, width: size.width, height: size.height))
addChild(wall)
}
}
「边缘碰撞体」也是一种碰撞体,但是它仅仅只是一个线条,或者多个连接在一起的线条,它没有体积、没有质量,它只是静态物体。
有两种不同的边缘碰撞体:edgeLoop 和 edgeChain。edgeChain 由连接在一起的多条险段的集合;前者是由起点、终点以及两点之间的连接线组成。
总结
在这篇文章中,我们把思维转向来 SpriteKit,并通过 SpriteKit 的一些封装好的 API 完成游戏的开局设置,搭建好了一个初步的游戏框架,下一篇文章中,我们将继续完善这个游戏框架,往其中填充内容。
我们现在完成的内容有:
- 游戏讲解;
- 熟悉 2D 编程(ing);
- 刚体碰撞与检测(检测未完成);
- 小球的发射与方块的消除;
- 游戏逻辑完善。
GitHub 地址: github.com/windstormey…