在上一篇文章中,咱们已经完成了对《可否关个灯》小游戏的界面和游戏逻辑进行了初步搭建,而且也具有了必定的可玩性。但细心的你会发现,这种「随机过程」的游戏开局,咱们几乎一把都不会赢,由于这并不符合这个游戏的初衷——逆序出开灯的顺序去关灯。git
在现有代码中,每次新开局游戏里各类灯的状态都是以前咱们经过「随机化」Light
模型中的 status
状态作到的,这种作法以前也说过了几乎不可能把全部灯都关掉,所以咱们须要对数据源作一些处理,使之可以经过「配置」去生成游戏开局。github
至此,咱们的 ContentView
已经比较庞大了,并且做为一个 View
它所承载的内容已经到了须要被抽离的时间点,咱们不能再往 ContentView
里塞关卡配置的逻辑了。算法
所以,仍是那句话「计算机科学领域的任何问题均可以经过增长一个间接的中间层来解决」,因此咱们将引入一个 GameManager
来处理关卡配置。GameManager
中负责的主要内容有:shell
新建一个 GameManager
类,并把以前写在 ContentView
中的逻辑都迁移进去。通过一番调整后,咱们的代码就变成了:swift
import SwiftUI
import Combine
class GameManager {
var lights = [
[Light(), Light(status: true), Light()],
[Light(), Light(), Light()],
[Light(), Light(), Light()],
]
/// 经过坐标索引修改灯状态
/// - Parameters:
/// - column: 灯-列索引
/// - size: 灯-行索引
func updateLightStatus(column: Int, row: Int) {
lights[row][column].status.toggle()
// 上
let top = row - 1
if !(top < 0) {
lights[top][column].status.toggle()
}
// 下
let bottom = row + 1
if !(bottom > lights.count - 1) {
lights[bottom][column].status.toggle()
}
// 左
let left = column - 1
if !(left < 0) {
lights[row][left].status.toggle()
}
// 右
let right = column + 1
if !(right > lights.count - 1) {
lights[row][right].status.toggle()
}
}
}
复制代码
ContentView
中的代码被修改成了:数组
import SwiftUI
struct ContentView: View {
var gameManager = GameManager()
var body: some View {
ForEach(0..<gameManager.lights.count) { row in
HStack(spacing: 20) {
ForEach(0..<self.gameManager.lights[row].count) { column in
Circle()
.foregroundColor(self.gameManager.lights[row][column].status ? .yellow : .gray)
.opacity(self.gameManager.lights[row][column].status ? 0.8 : 0.5)
.frame(width: UIScreen.main.bounds.width / 5,
height: UIScreen.main.bounds.width / 5)
.shadow(color: .yellow, radius: self.gameManager.lights[row][column].status ? 10 : 0)
.onTapGesture {
self.gameManager.updateLightStatus(column: column, row: row)
}
}
}
.padding(EdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0))
}
}
}
复制代码
运行工程!发现竟然点不动了!!!给第 17 行代码加上断点,你会发现其实是执行了这个方法的。回顾上篇文章中咱们所阐述的内容,这是由于 lights
变量的修改未触发 SwiftUI
的 diff 算法去检测须要改变的内容致使的,而之因此 lights
变量未被同步修改是由于Light
模型是值类型,值类型的变量在不一样对象间传递时,这个变量会遵循值语义而发生复制,也就是说 GameManager
和 ContentView
里的 lights
是两个彻底不同的变量。而以往咱们传递模型时,模型自己几乎都是引用类型,因此不会出现这种问题。ide
把咱们遗忘的 @State
补上,经过这个加上这个修饰词把 lights
变量与游戏布局绑定起来:函数
class GameManager {
@State var lights = [
[Light(), Light(status: true), Light()],
[Light(), Light(), Light()],
[Light(), Light(), Light()],
]
// ...
}
复制代码
此时再次运行工程,却发生了一个 crash:布局
Thread 1: Fatal error: Accessing State<Array<Array<Light>>> outside View.body
复制代码
再研究一下咱们刚才写的代码,总的来讲,咱们违反了 SwiftUI
单一数据源的规范,致使 SwiftUI
在执行 DSL 解析时,跑的数据源是非本身全部的。所以,咱们要把 lights
这个数据源「转移」给 ContentView
。在解决这个问题以前,咱们还须要明确一点,GameManager
是用来解决 ContentView
中逻辑太多致使代码臃肿的「中间层」,换句话说,咱们要把在 ContentView
中执行的操做都要经过这个「中间层」去解决,所以咱们须要用上 Combine
中的 ObservableObject
协议来协助完成单一数据源的规范,修改后的 GameManager
代码以下所示:ui
class Manager: ObservableObject {
@Published var lights = [
[Light(), Light(status: true), Light()],
[Light(), Light(), Light()],
[Light(), Light(), Light()],
]
// ...
}
复制代码
修改后的 ContentView
代码以下所示:
struct ContentView: View {
@ObservedObject var gameManager = Manager()
// ...
}
复制代码
此时运行工程,问题解决啦!接下来咱们来看看如何配置关卡。咱们须要再明确一点,关卡是游戏开局时就已经要肯定的,因此咱们要在游戏布局渲染以前就要肯定这次游戏开局的关卡,也就是要对 GameManager
的初始化方法搞事情。
在 GameManager
中实现一个便捷构造方法,使得咱们能够在 ContentView
的初始化方法中从新对 gameManager
变量进行初始化,丢进一些咱们真正须要对这次游戏开局时的初始化参数。
class GameManager: ObservableObject {
@Published var lights = [[Light]]()
/// 游戏尺寸大小
private(set) var size: Int?
// MARK: - Init
init() {}
/// 便捷构造方法
/// - Parameters:
/// - size: 游戏布局尺寸,默认值 5x5
/// - lightSequence: 亮灯序列,默认全灭
convenience init(size: Int = 5,
lightSequence: [Int] = [Int]()) {
self.init()
var size = size
// 太大了很差玩
if size > 8 {
size = 7
}
// 过小了没意思
if size < 2 {
size = 2
}
self.size = size
lights = Array(repeating: Array(repeating: Light(), count: size), count: size)
updateLightStatus(lightSequence)
}
// ...
}
复制代码
经过 size
参数控制了游戏布局尺寸,并考虑了一些 UI 上的规整。新增了一个 updateLightStatus(_ lightSequence: [Int])
方法,经过这个方法去作游戏的「随机过程」。
// ...
/// 经过亮灯序列修改灯状态
/// - Parameter lightSequence: 亮灯序列
private func updateLightStatus(_ lightSequence: [Int]) {
guard let size = size else { return }
for lightIndex in lightSequence {
var row = lightIndex / size
let column = lightIndex % size
// column 不为 0,说明非最后一个
// row 为 0,说明为第一行
if column > 0 && row >= 0 {
row += 1
}
updateLightStatus(column: column - 1, row: row - 1)
}
}
// ...
复制代码
由于在 GameManager
的便捷构造方法中传入的 lightSequence
是一个 Int
类型的数组,并且这个数组里元素的实际做用是标记出「亮灯」的顺序,因此咱们不能使用 Swift 中一些函数式的作法去加快「点亮」速度,只能使用原始方法去作了。咱们在 ContentView
中的代码就变成了:
import SwiftUI
struct ContentView: View {
@ObservedObject var gameManager = GameManager()
init() {
gameManager = GameManager(size: 5, lightSequence: [1, 2, 3])
}
var body: some View {
ForEach(0..<gameManager.lights.count) { row in
HStack(spacing: 20) {
ForEach(0..<self.gameManager.lights[row].count) { column in
Circle()
.foregroundColor(self.gameManager.lights[row][column].status ? .yellow : .gray)
.opacity(self.gameManager.lights[row][column].status ? 0.8 : 0.5)
.frame(width: self.gameManager.circleWidth(),
height: self.gameManager.circleWidth())
.shadow(color: .yellow, radius: self.gameManager.lights[row][column].status ? 10 : 0)
.onTapGesture {
self.gameManager.updateLightStatus(column: column, row: row)
}
}
}
.padding(EdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0))
}
}
}
复制代码
此时运行工程,会发现咱们已经配置好了关卡啦~
这个游戏判赢和判输都很是简单,若是把灯全都熄灭了就赢得比赛。若是灯全亮了就是输了。那么咱们能够用一个 lightingCount
变量去记录下当前游戏中灯亮的盏数,新增一个方法 updateGameStatus
:
// ...
/// 判赢
private func updateGameStatus() {
guard let size = size else { return }
var lightingCount = 0
for lightArr in lights {
for light in lightArr {
if light.status { lightingCount += 1 }
}
}
if lightingCount == size * size {
currentStatus = .lose
return
}
if lightingCount == 0 {
currentStatus = .win
return
}
}
// ...
复制代码
在此,为了链接 SwiftUI
使用 currentStatus
变量记录了当前游戏的状态,通过咱们以前的游戏经验,《可否关个灯》游戏的总体状态就三个:
所以咱们能够建立一个枚举去记录下当前的游戏状态:
extension GameManager {
enum GameStatus {
/// 赢
case win
/// 输
case lose
/// 进行中
case during
}
}
复制代码
并把 GameManager
作以下修改:
class GameManager: ObservableObject {
/// 灯状态
@Published var lights = [[Light]]()
@Published var isWin = false
/// 当前游戏状态
private var currentStatus: GameStatus = .during {
didSet {
switch currentStatus {
case .win: isWin = true
case .lose: isWin = false
case .during: break
}
}
}
// ...
}
复制代码
咱们又新增了一个 @Published
修饰的变量 isWin
,用于游戏状被修改时通知 SwiftUI
作视图的更新。
接下来咱们要考虑,当玩家赢得游戏时游戏要从新开始。从新开始游戏本质上只是对 lights
数据源的状态更新,由于此时游戏布局已经生成好,不须要从新渲染。对 GameManager
增长一个新方法:
// ...
/// 便捷构造方法
/// - Parameters:
/// - size: 游戏布局尺寸,默认值 5x5
/// - lightSequence: 亮灯序列,默认全灭
convenience init(size: Int = 5,
lightSequence: [Int] = [Int]()) {
self.init()
var size = size
// 太大了很差玩
if size > 8 {
size = 7
}
// 过小了没意思
if size < 2 {
size = 2
}
self.size = size
lights = Array(repeating: Array(repeating: Light(), count: size), count: size)
start(lightSequence)
}
// MARK: Public
/// 游戏配置
/// - Parameter lightSequence: 亮灯序列
func start(_ lightSequence: [Int]) {
currentStatus = .during
updateLightStatus(lightSequence)
}
// ...
复制代码
对 ContentView
作以下修改:
import SwiftUI
struct ContentView: View {
@ObservedObject var gameManager = GameManager(size: 5, lightSequence: [1, 2, 3])
var body: some View {
ForEach(0..<gameManager.lights.count) { row in
HStack(spacing: 20) {
ForEach(0..<self.gameManager.lights[row].count) { column in
Circle()
.foregroundColor(self.gameManager.lights[row][column].status ? .yellow : .gray)
.opacity(self.gameManager.lights[row][column].status ? 0.8 : 0.5)
.frame(width: self.gameManager.circleWidth(),
height: self.gameManager.circleWidth())
.shadow(color: .yellow, radius: self.gameManager.lights[row][column].status ? 10 : 0)
.onTapGesture {
self.gameManager.updateLightStatus(column: column, row: row)
}
}
}
.padding(EdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0))
}
.alert(isPresented: $gameManager.isWin) {
Alert(title: Text("黑灯瞎火,摸鱼成功!"),
dismissButton: .default(Text("继续摸鱼"),
action: {
self.gameManager.start([3, 2, 1])
}
)
)
}
}
}
复制代码
告知用户赢得比赛,个人作法是在游戏界面中弹出一个 alert
,并经过 GameManager
中的 isWin
变量来控制 alert
出现和隐藏,当 alert
出现时,用户点击 alert
中的「继续摸鱼」便可开始下一局比赛。运行工程,又能够愉快的玩耍啦!
在这篇文章中,咱们对游戏逻辑作了进一步的完善,能够说经过不断的抽象,把游戏逻辑和界面进行了分离。经过这种作法可让后续实现的需求鲁棒性更强!
如今,咱们的需求已经完成了:
赶快把工程跑起来,配置一个属于你本身的关卡,拉上小伙伴来体验一番吧~
GitHub 地址:github.com/windstormey…
来源:个人小专栏《 Swift 游戏开发》:xiaozhuanlan.com/pjhubs-swif…