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…

相关文章
相关标签/搜索