Swift 遊戲開發之「可否關個燈」(〇)

前言

第一個遊戲咱們將基於 SwiftUI 來完成。主要想驗證的問題有兩點:git

  • SwiftUI/UIKit 這種咱們平常接觸到的 UI 框架是否可以作遊戲?
  • 如何創建起遊戲開發的思惟?

《可否關個燈》是我在大一時去「中國科學技術館」作志願者時發現的一個小遊戲。結合當時「綠色環保」的理念,這個小遊戲火得不行,排了很久的隊纔到我,半個多小時後,我幾乎每次都是差一個「燈」就通關了,但每次都不行。github

館內的關燈遊戲(圖片來源網絡)

爲了避嫌,我把這個遊戲改成了《可否關個燈》。這個小遊戲的規則很是簡單,開始遊戲後,會「隨機」點亮一些燈,接着咱們就能夠開始玩了,想辦法去關掉這些燈,須要注意的是每一盞燈的開關會連帶其附近的燈進行開關,以下圖所示: swift

邏輯示意圖

邏輯梳理

從上述內容咱們能夠把邏輯先寫出來:網絡

  • 每一盞燈的開關會影響其 「上下左右」 燈的狀態(取反);
  • 燈只有「開」和「關」兩種狀態;
  • 勝利的條件是:關掉全部燈;

邏輯梳理完了,看上去不足以稱爲一個「遊戲」,咱們來把這個邏輯給補充完整,讓它看起來像個遊戲:框架

  • 加入計時器。記錄每把遊戲經歷過的時間;
  • 加入關卡難度配置。能夠調整爲 4x四、5x5 或其它難度;
  • 加入燈的隨機過程。讓每次遊戲開局時燈的狀態可控;
  • 加入歷史記錄功能。

在這裏解釋一下什麼是「燈的隨機過程」。遊戲的開局已經給定了一些燈的狀態,並且做爲一個遊戲,它必定是能夠把燈所有滅掉的,但若是咱們不是按照開始「亮燈」的順序去逆序的「滅燈」,是必定無法把全部燈都滅掉的。dom

所以,這個遊戲的核心邏輯咱們也就理解了,是圍繞 「亮燈」的順序去逆序出「滅燈」的順序,比較考驗玩家的想象能力。在這個遊戲中,咱們須要作的事情有:佈局

  • 燈狀態的互斥
  • 燈的隨機過程
  • 遊戲關卡難度配置
  • 計時器
  • 歷史記錄
  • UI 美化

遊戲框架搭建

打開 Xcode11 ( >= beta 7 ),新建一個 iOS 工程,並勾選 SwiftUI。SwiftUI 的語法細節在此不作展開,你能夠參考個人這兩篇文章 SwiftUI 如何實現更多菜單?SwiftUI 怎麼和 CoreData 結合?來查看更多關於 SwiftUI 的基礎內容。ui

構建燈的模型

對於一個「燈」來講,抽象其模型目前咱們只須要一個狀態值 status 便可,用於記錄該燈的開關狀態,且默認值爲 false,也就是「熄滅」狀態。spa

struct Light {
    /// 開關狀態
    var status = false
}
複製代碼

遊戲佈局

咱們先默認設置遊戲尺寸爲 3x3 大小的九宮格,咱們能夠先快速的搭建出佈局框架:3d

import SwiftUI

struct ContentView: View {
    
    var lights = [
        [Light(), Light(), Light()],
        [Light(), Light(), Light()],
        [Light(), Light(), Light()],
    ]
    
    var body: some View {
        ForEach(0..<lights.count) { rowindex in
            HStack {
                ForEach(0..<self.lights[rowindex].count) { columnIndex in
                    Circle()
                        .foregroundColor(.gray)
                }
            }
        }
    }
}
複製代碼

此時運行工程是下圖這個樣子的。

第一個佈局

雖然,咱們什麼間距都沒有設置,各個圓形之間間距是 Apple 根據其人機交互指南自動設置一個默認值,而且 SwiftUI 若是咱們什麼佈局都不寫的前提下是居中佈局的。咱們能夠利用 SwiftUI 的優秀佈局能力把遊戲主佈局變爲這樣:

import SwiftUI

struct ContentView: View {
    
    var lights = [
        [Light(), Light(status: true), Light()],
        [Light(), Light(), Light()],
        [Light(), Light(), Light()],
    ]
    
    /// 圓形圖案之間的間距
    private let innerSpacing = 30
    
    var body: some View {
        ForEach(0..<lights.count) { rowindex in
            HStack(spacing: 20) {
                ForEach(0..<self.lights[rowindex].count) { columnIndex in
                    Circle()
                        .foregroundColor(self.lights[rowindex][columnIndex].status ? .yellow : .gray)
                        .opacity(self.lights[rowindex][columnIndex].status ? 0.8 : 0.5)
                        .frame(width: UIScreen.main.bounds.width / 5,
                               height: UIScreen.main.bounds.width / 5)
                        .shadow(color: .yellow, radius: self.lights[rowindex][columnIndex].status ? 10 : 0)
                }
            }
                .padding(EdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0))
        }
    }
}
複製代碼

利用了 Light 模型中的 status 狀態值去控制了每一個「燈」(圓形)的顏色和透明度,以顯得咱們真的把「燈」給點亮了,調整了一下「燈」和「燈」之間的間距,讓它們顯得不那麼擁擠,同時爲了表現出真的「點亮」了燈,使用陰影來表示出燈的「光暈」,並把數據源 lights 中的一個模型的 status 值設置爲了 true。此時運行工程,你會發現咱們遊戲的主佈局完成了:

第二個佈局

修改燈的狀態

完成了佈局後,咱們須要去修改「燈」的狀態。以前,咱們已經經過 lights 這個變量去做爲管控佈局中「燈」的模型,咱們須要對這些模型進行處理便可。還要給「燈」加上「點亮」操做,至關於須要給每一個「燈」添加上觸摸手勢,並在觸摸手勢的回調處理事件中,維護與之相關的狀態變化。

import SwiftUI

struct ContentView: View {
    
    var lights = [
        [Light(), Light(status: true), Light()],
        [Light(), Light(), Light()],
        [Light(), Light(), Light()],
    ]
    
    /// 圓形圖案之間的間距
    private let innerSpacing = 30
    
    var body: some View {
        ForEach(0..<lights.count) { row in
            HStack(spacing: 20) {
                ForEach(0..<self.lights[row].count) { column in
                    Circle()
                        .foregroundColor(self.lights[row][column].status ? .yellow : .gray)
                        .opacity(self.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.lights[row][column].status ? 10 : 0)
                        .onTapGesture {
                            self.updateLightStatus(column: column, row: row)
                    }
                }
            }
                .padding(EdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0))
        }
    }
    
    /// 修改燈狀態
    func updateLightStatus(column: Int, row: Int) {
        // 對「燈」狀態進行取反
        lights[row][column].status.toggle()
    }
}
複製代碼

開開心心的寫出上述的狀態修改代碼,但 Xcode 報了 Cannot assign to property: 'self' is immutable 的錯誤,這是由於 SwiftUI 在執行 DSL 解析還原成視圖節點樹時,不容許有「未知狀態」或者「動態狀態」,SwiftUI 須要明確的知道此時須要渲染的視圖究竟是什麼。咱們如今直接對這個數據源進行了修改,想要經過這個數據源的變化去觸發 SwiftUI 的狀態刷新,須要借用 @Stata 狀態去修飾 lights 變量,在 SwiftUI 內部 lights 會被自動轉換爲相對應的 setter 和 getter 方法,對 lights 進行修改時會觸發 View 的刷新,body 會被再次調用,渲染引擎會找出佈局上與 lights 相關的改變部分,並執行刷新。修改咱們的代碼:

struct ContentView: View {
    
    // 加上 `@State`
    @State var lights = [
        [Light(), Light(status: true), Light()],
        [Light(), Light(), Light()],
        [Light(), Light(), Light()],
    ]

    // ...
}
複製代碼

此時運行工程,會發現咱們已經能夠完美的把「燈」給點亮啦~

給「燈」加上狀態修改

燈狀態的互斥

完成了「燈」的交互後,咱們須要對其進行「狀態互斥」的工做。回顧前文所描述的遊戲邏輯,再看這張圖,

邏輯示意圖

咱們須要完成的邏輯是,當中間的「燈」被「點擊」後,與之相關「上下左右」的四個「燈」和它本身的狀態須要取反。修改以前更新燈狀態的方法 updateLightStatus 爲:

// ...

/// 修改燈狀態
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()
    }
}

// ...
複製代碼

運行工程,咱們能夠和這個遊戲開始愉快的玩耍了~

燈狀態的互斥

燈的隨機過程

如今遊戲的雛形已經具有,但目前很是死板,每次開局都是第一行中間的燈被點亮,咱們須要加上游戲開始時的隨機開局。從咱們目前掌握的源碼帶來看,須要對數據源 lights 下手。遊戲初始化時的狀態數據來源於 lights 中所記錄的模型狀態,咱們須要對這裏邊的模型狀態值在初始化時進行隨機過程。因此能夠對 Light 模型進行以下修改:

struct Light {
    /// 開關狀態
    var status = Bool.random()
}
複製代碼

經過 Bool.random() 讓模型初始化時都生成不同的 Bool 值,這樣每次運行工程時,生成的佈局都不同,達到了咱們的目的!

燈的隨機過程

後記

至此,咱們已經完成的需求有:

  • 燈狀態的互斥
  • 燈的隨機過程
  • 遊戲關卡難度配置
  • 計時器
  • 歷史記錄
  • UI 美化

萬事開頭難,實際上咱們已經把這個遊戲的核心部分給完成了,在下一篇文章中,咱們將繼續完成剩下的 case,趕快試試看你能不能把全部的燈都熄滅吧~

GitHub 地址:github.com/windstormey…

來源:個人小專欄《 Swift 遊戲開發》:xiaozhuanlan.com/pjhubs-swif…

相關文章
相關標籤/搜索