V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iOS 开发实用技术导航
NSHipster 中文版
http://nshipster.cn/
cocos2d 开源 2D 游戏引擎
http://www.cocos2d-iphone.org/
CocoaPods
http://cocoapods.org/
Google Analytics for Mobile 统计解决方案
http://code.google.com/mobile/analytics/
WWDC
https://developer.apple.com/wwdc/
Design Guides and Resources
https://developer.apple.com/design/
Transcripts of WWDC sessions
http://asciiwwdc.com
Cocoa with Love
http://cocoawithlove.com/
Cocoa Dev Central
http://cocoadevcentral.com/
NSHipster
http://nshipster.com/
Style Guides
Google Objective-C Style Guide
NYTimes Objective-C Style Guide
Useful Tools and Services
Charles Web Debugging Proxy
Smore
pjhubs
V2EX  ›  iDev

Swift 游戏开发之「能否关个灯」(〇)

  •  1
     
  •   pjhubs ·
    windstormeye · 2019-09-02 23:05:05 +08:00 · 9174 次点击
    这是一个创建于 1943 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    第一个游戏我们将基于 SwiftUI 来完成。主要想验证的问题有两点:

    • SwiftUI/UIKit 这种我们日常接触到的 UI 框架是否能够做游戏?
    • 如何建立起游戏开发的思维?

    《能否关个灯》是我在大一时去「中国科学技术馆」做志愿者时发现的一个小游戏。结合当时「绿色环保」的理念,这个小游戏火得不行,排了好久的队才到我,半个多小时后,我几乎每次都是差一个「灯」就通关了,但每次都不行。

    馆内的关灯游戏(图片来源网络)

    为了避嫌,我把这个游戏改为了《能否关个灯》。这个小游戏的规则非常简单,开始游戏后,会「随机」点亮一些灯,接着我们就可以开始玩了,想办法去关掉这些灯,需要注意的是每一盏灯的开关会连带其附近的灯进行开关,如下图所示: 逻辑示意图

    逻辑梳理

    从上述内容我们可以把逻辑先写出来:

    • 每一盏灯的开关会影响其 「上下左右」 灯的状态(取反);
    • 灯只有「开」和「关」两种状态;
    • 胜利的条件是:关掉所有灯;

    逻辑梳理完了,看上去不足以称为一个「游戏」,我们来把这个逻辑给补充完整,让它看起来像个游戏:

    • 加入计时器。记录每把游戏经历过的时间;
    • 加入关卡难度配置。可以调整为 4x4、5x5 或其它难度;
    • 加入灯的随机过程。让每次游戏开局时灯的状态可控;
    • 加入历史记录功能。

    在这里解释一下什么是「灯的随机过程」。游戏的开局已经给定了一些灯的状态,而且作为一个游戏,它一定是可以把灯全部灭掉的,但如果我们不是按照开始「亮灯」的顺序去逆序的「灭灯」,是一定没法把所有灯都灭掉的。

    因此,这个游戏的核心逻辑我们也就理解了,是围绕 「亮灯」的顺序去逆序出「灭灯」的顺序,比较考验玩家的想象能力。在这个游戏中,我们需要做的事情有:

    • [ ] 灯状态的互斥
    • [ ] 灯的随机过程
    • [ ] 游戏关卡难度配置
    • [ ] 计时器
    • [ ] 历史记录
    • [ ] UI 美化

    游戏框架搭建

    打开 Xcode11 ( >= beta 7 ),新建一个 iOS 工程,并勾选 SwiftUI。SwiftUI 的语法细节在此不做展开,你可以参考我的这两篇文章 SwiftUI 如何实现更多菜单?SwiftUI 怎么和 CoreData 结合?来查看更多关于 SwiftUI 的基础内容。

    构建灯的模型

    对于一个「灯」来说,抽象其模型目前我们只需要一个状态值 status 即可,用于记录该灯的开关状态,且默认值为 false,也就是「熄灭」状态。

    struct Light {
        /// 开关状态
        var status = false
    }
    

    游戏布局

    我们先默认设置游戏尺寸为 3x3 大小的九宫格,我们可以先快速的搭建出布局框架:

    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 值,这样每次运行工程时,生成的布局都不一样,达到了我们的目的! 灯的随机过程

    后记

    至此,我们已经完成的需求有:

    • [x] 灯状态的互斥
    • [x] 灯的随机过程
    • [ ] 游戏关卡难度配置
    • [ ] 计时器
    • [ ] 历史记录
    • [ ] UI 美化

    万事开头难,实际上我们已经把这个游戏的核心部分给完成了,在下一篇文章中,我们将继续完成剩下的 case,赶快试试看你能不能把所有的灯都熄灭吧~

    GitHub 地址:https://github.com/windstormeye/SwiftGame

    来源:我的小专栏《 Swift 游戏开发》:https://xiaozhuanlan.com/pjhubs-swift-game

    30 条回复    2019-09-10 11:14:31 +08:00
    b00tyhunt3r
        1
    b00tyhunt3r  
       2019-09-03 02:12:22 +08:00 via iPad
    是真心喜欢 swift,和苹果一样 elegant
    就是实在太封闭~
    Fuluhu
        2
    Fuluhu  
       2019-09-03 02:36:40 +08:00 via iPhone
    酷,小时候玩过类似的游戏。话说有个 app 叫 occupy box 理念差不多,不过不是关灯主题的,而且没有初始的“灯”,算是玩家自己 freestyle
    Majirefy
        3
    Majirefy  
       2019-09-03 07:08:18 +08:00
    有意思~~~~

    单纯从语言上,最喜欢 C#,然而跨平台开发的 Xamarin 这几年发展不太理想……
    hahaayaoyaoyao
        4
    hahaayaoyaoyao  
       2019-09-03 08:17:00 +08:00 via Android
    gnome lightoff
    pjhubs
        5
    pjhubs  
    OP
       2019-09-03 08:43:41 +08:00
    @b00tyhunt3r 封闭不是借口嘛~喜欢就去试试看,一门语言而已,我觉得没必要加上太多的枷锁哈哈哈~
    pjhubs
        6
    pjhubs  
    OP
       2019-09-03 08:44:22 +08:00
    @Fuluhu 这类型的游戏非常多啦~我这个小专栏的目的也在与和大家一起通过 Swift 实现一些之前经常玩的小游戏!
    pjhubs
        7
    pjhubs  
    OP
       2019-09-03 08:45:05 +08:00
    @Majirefy 跨平台需要解决的问题比较复杂~
    Neoth
        8
    Neoth  
       2019-09-03 09:06:58 +08:00
    砸地鼠的核
    ShuangFan
        9
    ShuangFan  
       2019-09-03 09:09:01 +08:00
    麻叶~这不是我小时候十几块钱买的掌机上面的游戏么 emmmmmmm
    fuxinya
        10
    fuxinya  
       2019-09-03 09:12:48 +08:00 via Android
    我记得最强大脑以前挑战过这个项目,只不过是两人对战,红蓝双方相互灭灯
    pjhubs
        11
    pjhubs  
    OP
       2019-09-03 09:16:18 +08:00
    @ShuangFan 哈哈哈哈是的!一起来玩耍呀~
    pjhubs
        12
    pjhubs  
    OP
       2019-09-03 09:16:32 +08:00
    @fuxinya 对!这个游戏非常有意思!
    roryzh
        13
    roryzh  
       2019-09-03 09:26:25 +08:00
    额 黑白棋..
    tomoya92
        14
    tomoya92  
       2019-09-03 09:29:19 +08:00


    PS:楼主你用的啥录 GIF 的软件?
    MengQuadra
        15
    MengQuadra  
       2019-09-03 09:34:57 +08:00
    POJ 3279 _(:з」∠)_
    lxrmido
        16
    lxrmido  
       2019-09-03 10:07:20 +08:00
    第一眼看成 “ Switch 游戏开发”……
    pjhubs
        17
    pjhubs  
    OP
       2019-09-03 10:09:38 +08:00
    @roryzh 黑白棋的逻辑会比这个需要考虑的问题更多一些
    pjhubs
        18
    pjhubs  
    OP
       2019-09-03 10:09:47 +08:00
    pjhubs
        19
    pjhubs  
    OP
       2019-09-03 10:10:00 +08:00   ❤️ 1
    @tomoya92 gifox
    DylanZ
        20
    DylanZ  
       2019-09-03 11:04:31 +08:00
    很小的时候在掌机(售价 10 元 rmb,画面是一堆马赛克,只能玩俄罗斯方块和赛车)上玩过。
    pjhubs
        21
    pjhubs  
    OP
       2019-09-03 11:05:07 +08:00
    @DylanZ 👍
    weijidong
        22
    weijidong  
       2019-09-03 11:19:44 +08:00 via iPhone
    已买
    djyde
        23
    djyde  
       2019-09-03 11:24:00 +08:00
    非常喜欢 Swift.

    楼主博客很不错,关注了。
    djyde
        24
    djyde  
       2019-09-03 11:26:23 +08:00
    博客可以开放 rss 吗
    pjhubs
        25
    pjhubs  
    OP
       2019-09-03 14:08:40 +08:00
    @weijidong 非常感谢~
    pjhubs
        26
    pjhubs  
    OP
       2019-09-03 14:09:37 +08:00
    @djyde 不开 RSS 主要顾虑到一些其他问题哈~我先继续权衡一下
    weijidong
        27
    weijidong  
       2019-09-03 19:22:31 +08:00 via iPhone
    @pjhubs 我是自学编程的,向在大厂的大佬学习
    pjhubs
        28
    pjhubs  
    OP
       2019-09-03 19:57:07 +08:00
    @weijidong 太难得了!这条路自驱很重要!
    weijidong
        29
    weijidong  
       2019-09-03 20:12:44 +08:00 via iPhone
    @pjhubs 向大佬学习
    asHold
        30
    asHold  
       2019-09-10 11:14:31 +08:00
    666 想起了那道枚举的算法题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2769 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 42ms · UTC 14:01 · PVG 22:01 · LAX 06:01 · JFK 09:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.