Swift 游戏开发之「能否关个灯」() - V2EX
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 9609 次点击
    这是一个创建于 2234 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    第一个游戏我们将基于 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 Contentiew: 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
    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     Solana     3171 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 35ms UTC 00:31 PVG 08:31 LAX 17:31 JFK 20:31
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86