Flutter&Flame游戏实践#08 | 打砖块 -关卡设计


theme: cyanosis

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter\&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]\ 第二季:从休闲游戏实践,进阶 Flutter\&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》【github 项目首页】 查看。


一、关卡数据的设计

上一篇我们实现了打砖块游戏的基本玩法,目前砖块只是简单的堆砌,本章我们将着专注于游戏关卡内容的设计:


1. 砖块的显隐

首先看一下,如何决定砖块的显示和隐藏,如下所示,想要通过数据,将想要去除的砖块移除。从而达到关卡的可设计性:

我们可以通过一个二维的点阵,来决定砖块展示的样式。如下所示, 1 代表展示砖块, 0 代表不展示砖块:

dart List<List<int>> tiles = [ [1,1,1,1,1,1,1,1,1], [1,1,1,1,1,1,1,1,1], [0,1,0,0,1,0,0,1,0], [0,1,0,0,1,0,0,1,0], [1,1,1,1,1,1,1,1,1], [1,1,1,1,1,1,1,1,1], ];

这样,我们就不需要在 BrickManager 通过行列来添加砖块,可以解析 tiles 数据来完成:

dart ---->[lib/bricks/05/heroes/bricks.dart]---- List<Brick> _createBricks() { List<Brick> bricks = []; for (int i = 0; i < tiles.length; i++) { List<int> rows = tiles[i]; for (int j = 0; j < rows.length; j++) { if (rows[j] == 1) { Brick brick = Brick(j + tiles.length * i); brick.x = 64.0 * j; brick.y = 32.0 * i; bricks.add(brick); } } } return bricks; }


2. 关卡数据的初步设计

对于关卡功能来说,目前希望每关卡可以指定砖块的排列方式、小球运行的速度两个参数。如下所示,定义一个 Level 类用于维护一个关卡中的数据:

```dart class Level { final int id; final List > tiles; final double ballSpeed;

const Level({ required this.id, required this.tiles, required this.ballSpeed, }); } ```


这样我们只要准备关卡的相关的数据,就可以实现不同的关卡。选关时只有加载对应关卡的数据,重新开始游戏即可。效果如下:

| 第一关 | 第二关 | | --- | --- | | | |

目前简单起见,先将关卡的数据通过产量维护在内存中。如下所示是两个关卡的数据:

dart ---->[lib/bricks/05/model/level.dart]---- const Map<int, Level> kLevels = { 1: Level( id: 1, tiles: [ [1, 0, 0, 1, 0, 1, 0, 0, 1], [0, 1, 0, 1, 1, 1, 0, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 1, 0, 0, 1, 0, 0, 1, 0], [0, 1, 0, 0, 1, 0, 0, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 1, 0, 1, 1, 1, 0, 1, 0], [1, 0, 0, 1, 0, 1, 0, 0, 1], ], ballSpeed: 350, ), 2: Level( id: 2, tiles: [ [1, 1, 1, 1, 0, 1, 1, 1, 1], [1, 1, 1, 1, 0, 1, 1, 1, 1], [1, 1, 1, 1, 0, 1, 1, 1, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 1, 1, 1], [0, 1, 1, 0, 0, 1, 1, 1, 1], [0, 1, 1, 0, 0, 1, 1, 1, 1], ], ballSpeed: 360, ), };


3. 代码中对关卡的维护

游戏界面中的关卡数时游戏过程中的状态数据,目前先在 BricksGame 中维护,方便其他地方进行访问。 levelNum 的 set 方法可以更新 _levelNum 的值,并重新开始游戏;nextLever 方法将当前关卡数 +1,方面外界调用,进入下一关:

```dart ---->[lib/bricks/05/bricks_game.dart]---- int _levelNum = 1;

Level get level => kLevels[_levelNum]!;

set levelNum(int level) { if (_levelNum != level) { _levelNum = level.clamp(1, kLevels.length); // 设置关卡时,重置砖块管理器 restart(); } }

void nextLever() => levelNum = _levelNum + 1; ```

砖块管理器根据 game 中的激活关卡 level 对象中的数据,构建砖块即可:

游戏界面中的关卡文字也是同理。可以看出,对于 Flame 而言,游戏状态数据的管理,最简单直接的方式就是放入到游戏主类中。在下层的构件中,可以通过 game 实例来访问或修改数据:


此时当某一关卡的砖块被全部击碎,可以通过按钮触发 下一关卡

| 关卡通过 | 进入下一关 | | --- | --- | | | |

在代码中,点击事件只需要移除 GameSuccessMenu 弹出菜单,并触发 game.nextLever 即可切换到下一关卡。

dart void toNextLevel(){ widget.game.am.play(SoundEffect.uiClose); widget.game.overlays.remove('GameSuccessMenu'); widget.game.nextLever(); }


二、实现游戏选关

既然支持有关卡的,那么选择关卡的功能自然要安排上。上一篇在主页中添加了 选择关卡 的按钮,接下来实现一下右图所示的关卡选择功能。其中未通关的关卡需要锁定,禁止进入:

| 游戏主界面 | | | --- | --- | | | |


1. 关卡界面 LevelPage

这里关卡界面也是作为一个浮层进行维护的,其中界面布局是 Flutter 原生组件。通过 LevelPage 进行展示:

关卡选择界面是一个可滑动的网格布局,可以使用 GridView 组件来实现。其中封装 LevelItem 组件,负责展示单体的关卡按钮。组件需要的数据有 关卡名是否锁定 以及点击事件回调函数:

```dart --->[lib/bricks/05/overlays/leverpage/levelitem.dart]---- class LevelItem extends StatelessWidget { final bool locked; final ValueChanged onTapItem; final int level;

const LevelItem({ super.key, this.locked = true, required this.level, required this.onTapItem, }); ```

在构建逻辑中,可以根据 locked 数据决定不同的界面表现。比如在构造时传入 locked 为 true 时,使用小锁的图标,否则使用文字:

另外,锁定的关卡呈灰色,这里并没有单独使用一张灰色图片。而是使用 ColorFiltered 组件添加灰色的滤色效果。如果锁定的话直接返回,否则才嵌套 GestureDetector 处理点击事件:

```dart const ColorFilter greyscale = ColorFilter.matrix( [ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]);

```

然后通过 GridView 构建滑动的网格布局,这里先放置 30 个关卡,激活前 10 个来测试展示效果。后面会结合实际数据来处理:

点击时,触发 onSelectLevel 选择关卡进入。只要为 game.levelNum 设置值即可:

dart void onSelectLevel(int level) { game.overlays.remove("LevelPage"); game.levelNum = level; game.overlays.remove("HomePage"); }


2.关卡数据的随机生成

每一个关卡理论上来说应该精心设计砖块的排布数据,但这里为了节约时间,通过写一个数据解析器,通过随机数来自动生成砖块排布的数据。每个关卡中:

  • 1\]. 砖块宽高 `9*10`

  • 3\]. 随机生成的最终结果以 json 的形式存储为关卡数据文件。在游戏初始化时加载数据。


如下所示,每一行通过 formLine 方法封装,由于左右对称。可以先生成一半的数据,然后把另一半反向放入列表尾部。

```dart Random random = Random();

List formLine(int count) { List line = []; int half = count ~/ 2; for (int i = 0; i <= half; i++) { line.add(random.nextInt(2)); } line.addAll(line.sublist(0,half).reversed); return line; } ```

上面的数据就对应着游戏关卡中的一行:

一共有 10 行数据,只要遍历生成 10 次 formLine 即可。这样就生成了随机的二维点阵:

```dart void main() { List > tile = [];

for (int i = 0; i < 10; i++) { tile.add(formLine(9)); } print(tile.join('\n')); } ```

将其作用于界面上,效果如下。这样我们就随机生成了一个关卡中的砖块布局:

然后可以进一步封装一个 gen 方法生成关卡数据。比如现在关卡的小球速度依次 + 10 :

dart Level gen(int index) { List<List<int>> tiles = []; for (int i = 0; i < 10; i++) { tiles.add(formLine(9)); } return Level( id: index, tiles: tiles, ballSpeed: 340 + 10.0 * index, ); }


3.随机关卡数据的存储和加载

下面代码中随机 30 关的数据,并将其通过 json.encode 编码为字符串实现序列化,存储到资源文件中:

dart void main() { List<Level> levels = []; for (int i = 0; i < 10; i++) { levels.add(gen(i+1)); } String data = json.encode(levels); String filePath = path.join(Directory.current.path,'assets','data','bricks_levels.json'); File(filePath).writeAsString(data); }

对象想要序列化成 json ,需要实现 toJson 的方法,根据成员创建 map 对象:

```dart class Level { /// 略同...

Map toJson() => { 'id': id, 'tiles': tiles, 'ballSpeed': ballSpeed, }; } ```


现在游戏中就可以抛弃掉之前临时打工的 kLevels 关卡映射。现在所有的关卡数据都集中在 bricks_levels.json 中。所以我们只需要在游戏主类中读取问价,反序列化解析成 Level 列表即可。之前当前 level 获取方式,可以修改为 _levels 列表根据激活关卡的索引获取:

```dart ---->[lib/bricks/05/bricks*game.dart]---- List _levels = []; Level get level => _levels[*levelNum-1];

Future loadLevels() async { String path = 'assets/data/bricks_levels.json'; String data = await rootBundle.loadString(path); List list = json.decode(data) as List; _levels = list.map(Level.fromMap).toList(); } ```

现在就可以真正地进行选关挑战了。目前关卡的解锁功能还没有实现,接下来一起来完成吧 ~

| 第3关 | 第10关 | | --- | --- | | | |


三、游戏中持久化数据维护

为了不让游戏退出后,解锁的关卡被重置,需要将解锁的最大关卡数被持久化存储。之前在小恐龙跳跃中已经介绍过使用 shared_preferences 数据持久化存储的方案。另外,像金币数、钻石数、音效的开启等配置项,也需要进行数据的持久化。


1. 游戏配置参数 GameConfig

这里先将游戏的配置参数统一交由 GameConfig 类进行维护,其中承载着目前需要的数据信息:

```dart ---->[lib/bricks/05/config/game_config.dart]---- class GameConfig { /// 最大解锁关卡数 final int maxUnLockLevel; // 绿水晶个数 final int blueCrystal; // 金币个数 final int coin; // 是否开启音效 final bool enableSoundEffect; // 是否开启背景音乐 final bool enableBgMusic;

GameConfig({ required this.maxUnLockLevel, required this.blueCrystal, required this.coin, required this.enableSoundEffect, required this.enableBgMusic, }); } ```


为 GameConfig 提供三个辅助方法,便于调用:

  • fromMap 方法: 通过 json 解析成的 Map 创建 GameConfig 实例,
  • toJson 方法 : 便于将 GameConfig 编码成字符串,用于持久化存储。
  • copyWith 方法: 便于通过已有的 GameConfig 对象,修改若干属性,创建新的对象。

``` factory GameConfig.fromMap(dynamic map) { return GameConfig( maxUnLockLevel: map['maxUnLockLevel'] ?? 1, blueCrystal: map['blueCrystal'] ?? 0, coin: map['coin'] ?? 0, enableSoundEffect: map['enableSoundEffect'] ?? true, enableBgMusic: map['enableBgMusic'] ?? true, ); }

Map toJson() => { 'maxUnLockLevel': maxUnLockLevel, 'blueCrystal': blueCrystal, 'coin': coin, 'enableSoundEffect': enableSoundEffect, 'enableBgMusic': enableBgMusic, };

GameConfig copyWith({ int? maxUnLockLevel, int? blueCrystal, int? coin, bool? enableSoundEffect, bool? enableBgMusic, }) => GameConfig( maxUnLockLevel: maxUnLockLevel ?? this.maxUnLockLevel, blueCrystal: blueCrystal ?? this.blueCrystal, coin: coin ?? this.coin, enableSoundEffect: enableSoundEffect ?? this.enableSoundEffect, enableBgMusic: enableBgMusic ?? this.enableBgMusic, ); ```


2.配置信息管理类

这里将配置信息的加载和存储通过 GameConfigManager 类进行维护,其中:

  • 持有 SharedPreferences 对象,并在构造方法中赋值。
  • 持有当前游戏的配置信息 config 对象,该对象通过 loadConfig 方法进行初始化。
  • 通过 saveConfig 方法,将 config 对象序列化成字符串,使用 sp 持久化存储.

```dart ---->[lib/bricks/05/config/game_config.dart]---- class GameConfigManager { static const _kConfigKey = 'bricks-game-config-key'; final SharedPreferences sp; late GameConfig config;

GameConfigManager(this.sp);

void loadConfig() { String data = sp.getString(_kConfigKey) ?? "{}"; config = GameConfig.fromMap(jsonDecode(data)); }

Future saveConfig() => sp.setString(_kConfigKey, jsonEncode(config)); } ```

另外基于 saveConfig 方法,可以提供一些修改某个配置项的方法,方便调用:

dart /// 解锁下一关 Future<void> unlockNextLevel() { config = config.copyWith(maxUnLockLevel: config.maxUnLockLevel + 1); return saveConfig(); } /// 增加绿水晶 Future<void> addBlueCrystal({int count = 1}) { config = config.copyWith(blueCrystal: config.blueCrystal + count); return saveConfig(); } /// 增加金币 Future<void> addCoin({int count = 1}) { config = config.copyWith(coin: config.coin + count); return saveConfig(); } /// 修改背景音乐是否激活 Future<void> changeEnableBgMusic(bool enable) { config = config.copyWith(enableBgMusic: enable); return saveConfig(); } /// 修改音效是否激活 Future<void> changeEnableSoundEffect(bool enable) { config = config.copyWith(enableSoundEffect: enable); return saveConfig(); }


3. 配置管理器的使用

这里让游戏主类持有 GameConfigManager 对象,方便访问,并在 onLoad 回调在进行创建。另外,声音的配置之前是在 AudioManager 中维护的,此时可以让其依赖 GameConfigManager ,获取配置信息:

```dart ---->[lib/bricks/05/bricks_game.dart]---- GameConfig get config => configManager.config;

late GameConfigManager configManager;

@override FutureOr onLoad() async { sp = await SharedPreferences.getInstance(); configManager = GameConfigManager(sp); configManager.loadConfig(sp); am = AudioManager(configManager); /// 略同... } ```

这样在相关的时机调用 GameConfigManager 方法,更新 config 配置对象即可,同时也会持久化存储到本地的 xml 中。比如当砖块全部被击碎时,可以触发 unlockNextLevel 解锁下一关,以及 addBlueCrystal 增加一个绿水晶。如下是存储的配置文件具体信息:

dart void checkSuccess() { if (brickManager.children.isEmpty) { game.am.play(SoundEffect.uiClose); game.overlays.add('GameSuccessMenu'); game.configManager.unlockNextLevel(); game.configManager.addBlueCrystal(); game.status = GameStatus.gameOver; } }


四。本章小结

本章我们实现了打砖块游戏的选关功能,进一步拓展了游戏玩法。并通过随机生成的方式制作出 30 个关卡。另外,也完成了一些配置数据的持久化存储的功能,这样配置信息就不会随着游戏结束而重置。

接下来,我们将进一步拓展游戏玩法,设计一些道具进一步增加游戏的可玩性。

相关推荐
谙弆悕博士4 小时前
【附C++源码】从零开始实现 2048 游戏
java·c++·游戏·源码·项目实战·2048
沃普天科技4 小时前
TYPE C全功能10G数据放大延长PS8353 PS8780 PS8778 8K60HZ
驱动开发·游戏·计算机外设·电脑·ar·硬件工程·vr
liulian09168 小时前
Flutter 网络状态与内容分享库:connectivity_plus 与 share_plus 的 OpenHarmony 适配指南
网络·flutter
KKei16388 小时前
Flutter for OpenHarmony 学习视频播放器技术文章
学习·flutter·华为·音视频·harmonyos
KKei16389 小时前
Flutter for OpenHarmony 健身计划与运动打卡APP
flutter·华为·harmonyos
KKei163810 小时前
Flutter for OpenHarmony 在线考试与自测系统APP技术文章
flutter·华为·harmonyos
阿斯加德D10 小时前
我的世界生活大冒险整合包下载高版本2026最新分享
测试工具·游戏·游戏程序·生活·材质
liulian091612 小时前
Flutter 依赖注入与设备信息库:get_it 与 device_info_plus 的 OpenHarmony 适配指南
flutter
胖胖熊℡12 小时前
C语言打字母游戏,多版本(117-118)
游戏
两水先木示12 小时前
【Unity】使用AI辅助开发Unity游戏流程
游戏