在上一篇文章中,咱們已經完成了對《可否關個燈》小遊戲的界面和遊戲邏輯進行了初步搭建,而且也具有了必定的可玩性。但細心的你會發現,這種「隨機過程」的遊戲開局,咱們幾乎一把都不會贏,由於這並不符合這個遊戲的初衷——逆序出開燈的順序去關燈。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…