本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
Flutter&Flame 游戏开发系列前言:
该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端
跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。
第一季:30 篇短文章,快速了解 Flame 基础。
[已完结]
第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。
两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。
在之前我们实现了两个类型的小游戏:
- Trex 跳跃碰撞类, 1~4集
- 打砖块 射击消除类,5~12集
接下来,我们将写一下 益智类 的小游戏。扫雷 作为历史悠久的一款益智游戏。在当年游戏匮乏的时代,想必它承载着很多人童年的宝贵回忆。下面几篇 Flutter&Flame 游戏实践,将像素级复刻最经典版的扫雷游戏:
一、扫雷玩法介绍
一款益智游戏,首先要明确:
[1]
. 游戏操作规则。[2]
. 游戏胜利和失败的条件。[3]
. 游戏交互细节。
1. 游戏操作规则
- 闭合的单元格中隐藏着 地雷 或 数字。
- 闭合的单元格可以通过点击打开。
- 单元格中数字表示九个中含 地雷 的数量。
比如下面的紫框中的 1
单元格,表示它所在的九格中(红框) 存在一个地雷。而红框中只有尾翻开的单元格,那么可以推理出左上角的单元格是雷:
此时就可以通过右键将该区域标记为 地雷。这就是扫雷的核心玩法:
2.游戏的胜败条件
- 当点到地雷时,游戏失败。并展示出地图中的所有地雷:
- 游戏胜利的条件是排除所有的地雷,将非雷区全部翻开:
游戏失败 | 游戏胜利 |
---|---|
image.png | image.png |
总的来看,它是一个逻辑推理的益智游戏,规则非常精简,所以很容易上手。复杂的单元格也可以提高游戏的可玩性,是一个非常优秀的游戏玩法设计。
3. 游戏交互细节
下面动态图中展示了扫雷游戏的基本交互,包括:
- 按下及拖动过程中,对应的单元格处于按下状态。
- 抬起时,打开单元格。
- 右键标记、取消旗子。
- 顶部中间的表情展示当前的游戏交互状态,点击时重新开始。
- 左侧 LED 展示雷的数量,右侧 LED 展示使用的秒数。
二、整体界面布局分析
本篇我们先来解决界面设计和交互的问题,在下一章再实现具体的玩法。为了让单元格的尺寸在任何大小下都不失真,这里资源图片全部采用 svg
。也顺便介绍一下 svg 如何在 Flame 中使用。
1. 游戏界面布局
游戏界面在布局上非常简单,顶部展示游戏状态信息,一般称之为 HUD
(Heads-Up Display);下方网格是游戏区域,将作为后期处理的重点部分;除此之外,还有两者之间的边框需要展现:
整体构件结构如下图所示,SweeperLayout
负责整体布局的展示,包括外部的边线框。其中包含 SweeperHud
和 CellManager
两个主题内容作为孩子:
2. 尺寸设计
游戏中的构建尺寸如何规定,是一个棘手的问题。它是自适应屏幕宽高进行缩放,还是固定尺寸,不受窗口尺寸影响。扫雷游戏固定尺寸即可,如果自适应窗口缩放,会导致个数少时单元格非常大。我们希望窗口缩放不影响游戏的尺寸表现。
为了便于修改尺寸,游戏界面中所有的尺寸都基于一个标准尺寸作为单位。这里选取 单元格 尺寸 cellSize。当我们确定了网格的行列数 gridXY ,通过 SizeRes
类进行维护:
1 | ---->[lib/sweeper/game/config/size_res.dart]---- |
一切尺寸相关的数据够可以通过这两个数据 计算得到。比如网格区的宽高是行列数乘以单元格尺寸; Hud 尺寸高度是两个单元格大小;宽度是网格宽度。表情按钮的大小是 1.5 被的单元格大小。这里通过 get 方法提供计算逻辑:
1 | Vector2 get gridSize => Vector2(gridXY.$1 * cellSize, gridXY.$2 * cellSize); |
同样,LED 的尺寸、间隔、矩形区域等数据可以通过 get 方法提供。这也是 SizeRes
命名的原因,将其视为尺寸资源的仓库,方便统一管理和维护:
1 | // hud 矩形区域 |
3. 阴影边线框的实现
俗话说,细节决定成败。仔细观察面板可以发现,其中有很多处阴影边线。包括最外部、单元格外围、HUD 外围、LED 灯外围四个地方。如何展示这些边框呢?
首先,这种边框存在于多个场合,所以需要封装一下便于复用。边框的展现可以通过绘制 矩形 的四条边线实现。其中可以设置边线的 边线宽度、四边颜色。如下所示,封装一个 BorderDecoration
类承载数据,调用 paint
方法,绘制对应矩形的四个边框:
1 | ---->[lib/sweeper/painter/decration/border_decroation.dart]---- |
4. 实现整体布局:SweeperLayout
游戏的外框通过 SweeperLayout
构建展示,其中复写 render 方法,操作 Canvas 进行绘制边线和背景色。一共有三个边线需要绘制,分别封装为三个方法:
1 | class SweeperLayout extends PositionComponent with HasGameRef<SweeperGame> { |
_pintHudBorder
: 绘制 Hud 内部的边线,创建上面写的 BorderDecoration
对象,触发 paint 方法完成绘制边线任务。
注: 其中尺寸相关的数据,封装在 game.sizeRes
中,后面会单独介绍。现在只要知道通过它可以获取尺寸数据即可:
1 | void _pintHudBorder(Canvas canvas) { |
_paintGridBorder
用于绘制网格外围的边线:
1 | void _paintGridBorder(Canvas canvas) { |
_paintGridBorder
用于绘制网格外围的边线:
1 | void _paintOutBorder(Canvas canvas,Rect rect) { |
三、单元格与其管理器
接下来将要完成如下的单元格布局,以及拖拽时间过程中,对应单元格呈现出按压状态。其中单元格通过 svg
图片展示,这里也正好介绍一下 Flame 对 svg 的支持情况:
1. 单元格构件 Cell
这里称单元格为 Cell , 在 Flame 中使用 svg 构件,需要额外添加类库 flame_svg。它是 Flame 官方基于 flutter_svg 封装的构建:
1 | flame_svg: ^1.10.1 |
我们知道 SpriteComponent
是基于 Sprite 渲染呈现内容; 这里 flame_svg 封装了 SvgComponent
构建,基于 Svg
渲染呈现内容。Sprite 基于资源图片得到,同理 Svg 可以通过加载 svg 文件得到。
同样将加载逻辑放在 TextureLoader 类中,通过 loadSvg
方法加载资源列表并放入 _svgMap
映射关系中。提供 findSvg
方法根据文件名,获取 Svg
对象:
1 | ---->[packages/flame_ext/lib/texture_loader.dart]---- |
Cell 单元格如下所示 ,由于一个坐标表示它的位置;默认加载 closed.svg
文件表示闭合单元格;其中目前提供两个方法 pressed
和 reset
分别让单元格更新为按压和闭合状态。
1 | ---->[lib/sweeper/game/heros/cell/cell.dart]---- |
2.单元格管理器
单元格的和之前打砖块中的砖块管理类似,都是遍历行列生成单体。如下所示,在 _createCells
方法中遍历行列数,创建 Cell 对象加入列表。就可以依次排列从此网格:
1 | ---->[lib/sweeper/game/heros/cell/cell_manager.dart]---- |
3. 交互的逻辑分离: GameCellLogic
这里做了一个有趣的尝试,将 构建构建逻辑 和 交互时数据处理逻辑 通过 mixin 进行分离。如下所示,定义了 GameCellLogic
来处理网格整体的交互逻辑:
这样就可以让逻辑更为紧凑,之后修改交互逻辑,只需要在 GameCellLogic
中处理即可。它混入 DragCallbacks ,可以复写 onDragUpdate
方法监听拖拽事件。然后根据触点和单元格尺寸计算出落点的坐标。通过 pressed
方法进行处理,将目标点的 Cell
改为 pressed 状态。最近在看 《代码简洁之道》,对我大有裨益。方法应该短小精炼,只处理必要的事:
1 | ---->[lib/sweeper/game/logic/game_cell_logic.dart]---- |
到这里就完成了拖拽时按压的交互逻辑。将方法独立封装,可以带来很强的复用性,比如要增加点击的按下的事件时,额外混入 TapCallbacks
,复写 onTapDown
方法调用 pressed 即可:
四、HUD 的处理
HUD 中包含三个部分,左侧是地雷个数,中间是按钮,右侧时游戏开始后的秒数。
到这里,游戏中组件的整体结构就非常明确了,如下所示:
1. Led 显示屏的封装:LedScreen
这种 Led 显示屏可能在以后的项目中也能用,可以单独封装起来便于复用。如下所示,我们要封装一个显示屏,可以指定显示屏中数字管的个数,以便更灵活使用:
显示屏封装为 LedScreen
构建,传入数量、宽度、间隔信息。通过这些信息,可以计算出显示屏幕的尺寸 screenSize
。在 onLoad
方法中,遍历 count
次,加入 SvgComponent
展示数字管即可:
1 | ---->[lib/sweeper/game/heroes/hud/led_screen.dart]---- |
这样在 SweeperHud
中通过 _addLedScreen
方法,创建 LedScreen
添加其中即可。至于显示屏的数字变化,将在下一篇结合具体的场景来完善。
1 | --->[lib/sweeper/game/heroes/hud/sweeper_hud.dart]---- |
2. 表情按钮构件:FaceButton
表情按钮看起来非常简单,就是展示一个表情 svg 图像。通过 FaceButton
来完成,其中需要处理点击时的按压效果。混入 TapCallbacks 在按下和抬起事件中切换图片资源:
1 | ---->[lib/sweeper/game/heroes/hud/face_button.dart]---- |
3. 表情按钮构件:FaceButton
虽然表情按钮非常简单,但是其中蕴含着一个很重要的知识点:如何管理表情。
如下所示,在单元格点击和拓展时,如何改变表情呢?
常规来看,想让宫格的事件影响到表情按钮,需要通过世界来一层层找到按钮对象,然后修改其图像。这样无疑非常复杂。按钮是被动地被改变,有没有什么手段能主动让按钮主动监听需要变化的事件呢?
任何构件都可以访问 Game,我们可以把它当成一个 大广播,宫格点击时发送通知。表情按钮相当于收音机,可以主动监听广播的喊话。这就是一个很标准的 监听通知机制。我们有很多种手段来完成这件事,这里先采用 Flutter 内置的 Stream 流来完成(当然你可以使用任何状态管理方式来处理)。
下面定义 GameFaceLogic
就是这个大广播,播报一个 bool 值。通过 activeFace
和 resetFace
可以让广播喊话,发送通知。SweeperGame 只要混入 GameFaceLogic
就具备了大广播的能力:
1 | ---->[lib/sweeper/game/logic/game_face_logic.dart]---- |
- 单元格:广播发送消息
单元格的交互逻辑中,只需要在对应实际触发 activeFace
和 resetFace
方法,就可以发送通知。
1 | ---->[lib/sweeper/game/logic/game_cell_logic.dart]---- |
- 表情按钮:收音机接收消息
此时按钮构建在装载是监听广播,触发 _onFaceChange
方法,修改状态即可。注意在销毁时取消监听。
1 | ---->[lib/sweeper/game/heroes/hud/face_button.dart]---- |
到这里我们就完成了扫雷界面交互上的核心需求,下一篇将实现扫雷的具体功能逻辑,敬请期待。感谢大家的支持,喜欢的话,希望可以点个赞 ~
本文转载自: 掘金