第一個遊戲咱們將基於 SwiftUI
來完成。主要想驗證的問題有兩點:git
SwiftUI/UIKit
這種咱們平常接觸到的 UI 框架是否可以作遊戲?《可否關個燈》是我在大一時去「中國科學技術館」作志願者時發現的一個小遊戲。結合當時「綠色環保」的理念,這個小遊戲火得不行,排了很久的隊纔到我,半個多小時後,我幾乎每次都是差一個「燈」就通關了,但每次都不行。github
爲了避嫌,我把這個遊戲改成了《可否關個燈》。這個小遊戲的規則很是簡單,開始遊戲後,會「隨機」點亮一些燈,接着咱們就能夠開始玩了,想辦法去關掉這些燈,須要注意的是每一盞燈的開關會連帶其附近的燈進行開關,以下圖所示: swift
從上述內容咱們能夠把邏輯先寫出來:網絡
邏輯梳理完了,看上去不足以稱爲一個「遊戲」,咱們來把這個邏輯給補充完整,讓它看起來像個遊戲:框架
在這裏解釋一下什麼是「燈的隨機過程」。遊戲的開局已經給定了一些燈的狀態,並且做爲一個遊戲,它必定是能夠把燈所有滅掉的,但若是咱們不是按照開始「亮燈」的順序去逆序的「滅燈」,是必定無法把全部燈都滅掉的。dom
所以,這個遊戲的核心邏輯咱們也就理解了,是圍繞 「亮燈」的順序去逆序出「滅燈」的順序,比較考驗玩家的想象能力。在這個遊戲中,咱們須要作的事情有:佈局
打開 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
值,這樣每次運行工程時,生成的佈局都不同,達到了咱們的目的!
至此,咱們已經完成的需求有:
萬事開頭難,實際上咱們已經把這個遊戲的核心部分給完成了,在下一篇文章中,咱們將繼續完成剩下的 case,趕快試試看你能不能把全部的燈都熄滅吧~
GitHub 地址:github.com/windstormey…
來源:個人小專欄《 Swift 遊戲開發》:xiaozhuanlan.com/pjhubs-swif…