325412de021447191116035fa055d604
Swift 游戏开发之「方块弹珠」(二)

前言

在上一篇文章中,我们已经了解了通过 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 的最终父类是 SKNodeSKNodeUIView 是两个完全不同的类型。

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 的经历发生碰撞。
top Created with Sketch.