一个贪吃蛇游戏!
假设你已经是一个有一些基础的 iOS 开发者
最重要:开发软件所需要的思路
其次:
- iOS 软件开发的一些基础知识
- Swift 语言基础知识
如果你能自己尝试重写一遍这个项目,并且对照代码进行学习,你将会学习到:
- Swift 语言的一些高阶语法和妙用
- 架构上的一些精巧设计
因为时间有限,我不会一步一步讲解如何实现,而会基于写好的代码,提供写代码的思路和步骤。然后在每一部分中挑选一些有价值的内容进行深入一些的讲解。
代码可以在这个地址找到:【TODO: 地址】
UI 部分:
- 顶部当前游戏状态展示
- 中间红色方框内的游戏区域
- 地图(隐形方格)
- 贪吃蛇本身
- 食物
逻辑部分:
- 贪吃蛇在棋盘内能够自由移动
- 吃到食物后的表现:产生新的食物、蛇边长等
- 游戏结束的判定:撞墙、撞到自己等
操作部分:
- 通过滑动手势操作蛇的转向
- 单机手势暂停、恢复
先设计一下需要的模块,以及他们对应的职责
首先拆分 UI 部分,初步看起来需要至少两个 View
- 一个 UILabel,来展示顶部的信息(命名为 LogView)
- 一个 View 用于展示中间的游戏区域(命名为 GameView)
然后对应的逻辑部分,我们可以单独使用一个 Class 来专门处理逻辑(命名为 LogicModel)
他具体负责的内容就对应上面的功能拆分中的逻辑部分
最后还需要有一个 ViewController 作为核心控制器,整合所有的组件。手势的添加也可以放到这个 ViewController 当中。
【TODO: 一个结构图】
具体怎么创建项目我就不再赘述了,这里提供一张最终的项目结构截图
我们重点关注这三部分
- ViewController:核心控制类
- Layout:存放 UI 相关的文件
- Logic:逻辑控制类文件
另外两个部分是:
- Const:预先定义的一些状态
- Extension:有助于简化代码的拓展函数
引入好用的第三方库是必须要学会的技能。通常我们会有几种引入方式,分别是 CocoaPods、Carthage 和 Swift Package Manager
- CocoaPods:
- CocoaPods 是一个流行的依赖管理工具,专为 Swift 和 Objective-C 项目设计。
- 它使用一个名为
Podfile
的文件来定义项目依赖,并通过pod install
命令安装依赖。 - CocoaPods 拥有一个庞大的开源库社区,易于搜索和集成第三方库。
- Carthage:
- Carthage 允许开发者在不使用 Xcode 项目的方式下引入框架。
- 它通过
Cartfile
来管理依赖,使用carthage update
命令来获取和构建依赖。 - Carthage 不修改 Xcode 项目文件,因此与 CocoaPods 相比,它提供了更多的灵活性,但集成到项目中的流程可能更复杂。
- Swift Package Manager (SPM):
- SPM 是 Swift 的原生包管理器,集成在 Xcode 中,无需额外安装。
- 它使用
Package.swift
文件来定义和管理依赖,支持 Swift 项目的依赖和模块化。 - SPM 支持跨平台的 Swift 项目,是苹果官方推荐的依赖管理方式。
总而言之,大型公司和项目都会使用 CocoaPods 进行三方库的管理,而小型项目可以使用 SPM,SPM 和 CocoaPods 也可以结合使用,互相不冲突。
我们本次要引入的是 SnapKit(https://github.com/SnapKit/SnapKit)SnapKit 是一个 Swift 语言的自动布局框架,他有不少优点:
- 简洁的语法:简化 Auto Layout 的代码编写 —— 如果你用过最基础的基于 frame 布局进行的开发,就知道 SnapKit 会有多好用了
- 强类型:减少运行时错误,提升代码质量
- 链式调用:提高代码的可读性和易用性
- 兼容性:支持 iOS、tvOS、macOS 和 watchOS
上述三种引入方式他都是支持的。为了方便我们直接使用 Swift Package Manager 进行引入。具体步骤如下:
- 打开 Xcode 项目。
- 选择项目文件,点击 "File" -> "Swift Packages" -> "Add Package Dependency"
- 输入 SnapKit 的 Git 仓库地址:
https://github.com/SnapKit/SnapKit.git
- 选择所需的版本,然后点击 "Next"
- 确认依赖信息,点击 "Add Package"

接下来我们先写 UI 相关的代码,也就是 Layout 文件夹中的部分
最终会需要新建这么三个文件:
- GameView:游戏区域
- UnitView:游戏区域中每个单位小格子
- LogView:上方的信息展示区域
用最终的截图来展示的话就能很一目了然了:
对于每一个小方格而言,核心会有两个属性。
首先会必须要有一个坐标的概念,代表他所处的具体的位置。
其次,一个小格子一共有四种展示情况,这四种情况分别展示不同的图片样式:
- 蛇头:展示绿色菱形
- 蛇身:展示绿色圆形
- 食物:展示星星
- 空白格子:不展示任何图片
这里我们根据需要,可以判断样式需要使用 enum
/// UnitView 类型
enum UnitViewType {
case snakeHead /// 蛇头
case snakeBody /// 蛇身
case food /// 食物
case normal /// 空白格子
}
而坐标需要使用使用 struct 来定义:
/// 坐标
struct Pos {
var x: Int
var y: Int
}
所以 UnitView 的情况是(简化版):
在 Swift 中,除了我们最熟悉的 Class 之外,struct 和 enum 这两种类型也经常被使用:
- struct (结构体):用于定义自定义数据类型,可以包含多个属性和方法。结构体是引用类型。
- enum (枚举):用于定义一个有固定数量的常量集合,可以有原始值,也可以关联值。枚举可以定义方法。
特性 | struct | enum |
---|---|---|
类型 | 自定义数据类型 | 固定数量的常量集合 |
存储 | 引用类型 | 值类型(默认)或引用类型(如果定义为 class) |
属性 | 可以有多个属性 | 通常没有属性,但可以扩展 |
方法 | 可以定义方法 | 可以定义方法 |
原始值 | 不适用 | 可以有,用于存储额外信息 |
关联值 | 不适用 | 可以有,每个枚举案例可以关联不同的数据类型 |
继承 | 不能被继承 | 不能被继承 |
构造器 | 可以有构造器 | 可以有构造器 |
请注意,Swift 中的 enum
可以非常强大,可以拥有方法、原始值和关联值,使它们在某些情况下可以替代 struct
。然而,struct
由于是引用类型,通常用于定义更复杂的数据结构。
通过给 Enum 增加方法简化代码。比如针对 UnitViewType,我们可以增加一个方法根据类型获取对应展示的图片,头部展示菱形、身体展示圆形等:
这样在使用的时候就能直接调用了,不用再关心具体的逻辑:
在 Swift 中,图片可以直接展示在代码中,这样非常的简洁清晰
那么要如何触发呢,我们如何实现这样的效果呢?其实注释掉这段代码就发现秘密了,需要使用 #imageLiteral 关键字:
具体的过程,可以下载项目之后看具体的注释。可以稍微看一下下图的构造,可以看到第一个标记点,整个棋盘中的元素都是基于刚才写的 UnitView 基本单元格。
另外读代码的时候可以根据第二个标记点开始读,在这里会依次初始化棋盘、蛇、食物。
在 Swift 中,一共有这样几种访问权限:
访问级别 | 定义 | 访问范围 |
---|---|---|
public |
公开 | 跨模块访问 |
internal |
内部 | 模块内访问 |
private |
私有 | 源文件内访问 |
fileprivate |
文件私有 | 定义文件内访问 |
可以注意到,在上面的文件中,部分地方我使用了 public,部分地方我用了 private,这是因为内部的方法我不希望给其他 Class 调用。
那么我们为什么要进行权限控制呢?权限控制的主要目的是为了封装和安全性:
- 封装:隐藏实现细节,只暴露必要的接口,使得代码更易于维护和理解。
- 安全性:限制对敏感数据和功能的访问,防止意外或恶意的修改。
- 模块化:促进代码的模块化,每个模块只关注其内部的职责,降低模块间的耦合。
可以看到我在代码中使用了// MARK: -
标记,这是一种非常好用的注释方式:
- 组织代码:通过添加
// MARK: -
来标记代码的不同部分,使得代码结构更加清晰,便于阅读和维护。 - 导航辅助:许多代码编辑器和 IDE 会使用这些标记来提供更好的导航功能,允许开发者快速跳转到特定的代码段。
- 文档生成:在生成文档时,
// MARK: -
可以帮助文档工具组织和分类内容。
比如这里的导航辅助,我们点击文件上方导航栏:
就可以看到使用 // MARK: -
标记的部分:
类似的方法还有:
// TODO:
标记遗留的 todo// FIXME:
标记需要修复的错误
效果如下图:
这一步的流程还是很简单的,看途中注解就可以比较方便的理解这个流程。
注意这里我们重写了 ViewController 的 viewDidLoad 方法,这是 ViewController 众多生命周期中最经常被使用的一个。
生命周期是指从它被创建到被销毁的整个过程,其中包含了一系列的时机,还有比如
-
init(coder:)
和init(nibName:bundle:)
:- 构造器,用于从 storyboard 或 XIB 文件加载视图控制器。
-
loadView()
:- 当视图控制器需要加载视图时调用,通常在构造器之后。
-
viewDidLoad()
:- 视图加载完成后调用,是设置初始状态和配置 UI 的好地方。
-
viewWillAppear(_:)
:- 在视图即将出现到屏幕上时调用,可以用于配置动画或更新 UI。
-
viewDidAppear(_:)
:- 视图已经出现在屏幕上后调用,可以执行一些需要视图已经可见的操作。
-
viewWillDisappear(_:)
:- 在视图即将从屏幕上消失时调用,可以用于保存状态或清理。
-
viewDidDisappear(_:)
:- 视图已经从屏幕上消失后调用,用于执行一些清理工作。
-
updateViewConstraints(_:)
:- 在更新视图的约束之前调用,用于自定义约束更新逻辑。
-
viewWillLayoutSubviews()
和viewDidLayoutSubviews()
:- 分别在视图的子视图布局之前和之后调用,用于微调布局。
-
viewWillTransition(to:size:with:)
:- 在视图控制器的视图将要改变大小时调用。
-
dealloc
:- 视图控制器被销毁前调用,用于执行清理工作。
为了让贪吃蛇移动起来,我们需要使用 Timer,这是用于定时执行任务的类。
它可以在指定的时间间隔后重复或单次执行代码块。使用 Timer
可以模拟时间相关的功能,如倒计时、游戏循环等。
使用方法如下:
注意,为什么这里的方法需要额外加一个 @objc 的标记?
因为 @objc 属性用于桥接 Swift 和 Objective-C 之间的方法调用,被标记了之后的 Swift 类、属性、方法或其他成员,就可以在 Objective-C 代码中使用。
而 Timer
类实际上源自 Objective-C 运行时,因为 Timer
是 NSObject
的子类。所以为了能够将 Swift 的方法作为目标方法传递给 Objective-C 的 Timer
,就必须加上 @objc
具体的逻辑很简单,但是涉及一个核心知识点:
optional
(可选类型)是一个特殊的类型,用来表示某个位置可能包含一个值,或者根本没有值。在声明时,会加上问号来标记可选类型。比如下图,就代表这个 gameGround 可能是一个 GameView 的,也可能是一个 nil(空值)
因此,我们在使用的时候,可以使用感叹号来强制解析,不过一般我们推荐使用可选绑定(if let
或 guard let
)来安全地解包,比如上图里使用的 guard let 方法。
比较复杂,可以看源代码仔细研究
懒加载
通过 extension 来简化代码
实现 Equatable 来支持使用双等号判断
Swift 变量的 didSet 用法
通过使用 weak 避免循环引用:
高阶函数 contains,以及 $0 的语法糖使用