Flutter&Flame游戏实践#11 | 打砖块 - 功能背包


theme: cyanosis

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


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

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

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

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


一、游戏背包

上一篇我们实现了金币的获取,以及商店商品的购买。购买的商品明应该放入背包,并在挂卡中使用。本篇将实现游戏背包的功能,并实现道具在关卡中效果的具体逻辑。

| 使用符文 | 使用道具 | | --- | --- | | 99.gif | 103.gif |


1. 背包界面的展示形式

如下所示,背包通过弹框的形式展示。分为 挡板符文道具 三个 Tab 。其中已购买的挡板可以在背包中进行切换,在主页面可以查看符文和道具的个数,但只能在关卡中使用:

| 挡板 | 符文 | | --- | --- | | image.png | image.png |


2. 背包商品数据

首先在购买商品时,需要将商品加入背包,并且持久化存储。这里将购买的商品封装为 BuyGoods 对象,维护已购买的商品以及对应的数量:并提供 fromMap 方法根据映射关系构建对象,以及 toJson 方法将对象序列化成字符串:

```dart ---->[lib/bricks/06/config/buy_goods.dart]---- class BuyGoods { final Goods goods; final int count;

BuyGoods({ required this.goods, required this.count, });

factory BuyGoods.fromMap(dynamic map) { return BuyGoods( count: map['count'] ?? 0, goods: Goods.fromMap(map['goods']), ); }

Map toJson() => { 'goods': goods, 'count': count, }; }

```


3. 商品的购买与背包数据维护

严格来说,像商品、购买的商品数据,应该通过数据库进行存储,或者通过网络提交到服务器。当前数据并不多单,这里先以首先功能为主,所以也将这些数据存入配置文件中。后续有机会,也可以将数据的存储迁移到数据库或服务器网络。

如下所示,在 GameConfig 中增加 buyGoods 对象表示已购买的道具商品:

image.png

然后再配置信息管理器中,通过 saveGoodsToPackage 方法,将购买的商品加入背包数据里。其中如果背包包含该商品,则让数量 +1 ,否则将商品添加到背包中:

dart ---->[lib/bricks/06/config/game_config.dart#GameConfigManager]---- Future<void> saveGoodsToPackage(Goods goods) { List<BuyGoods> result = List.of(config.buyGoods); List<BuyGoods> buyGoods = config.buyGoods.where((e) => e.goods.src == goods.src).toList(); if(buyGoods.isEmpty){ result.add(BuyGoods(goods: goods, count: 1)); }else{ BuyGoods buy = buyGoods.first; BuyGoods newBuyGoods = BuyGoods(goods: buy.goods,count:buy.count+1); result.removeWhere((e) => e.goods.src == goods.src); result.insert(0, newBuyGoods); } config = config.copyWith(buyGoods: result); return saveConfig(); }

然后再购买商品的时,调用 saveGoodsToPackage 保存商品:

image.png

由于之前已经通过 activePaddles 记录了购买的挡板,这里 buyGoods 就不再额外记录了。在 BricksGame 中提供一个 buyPaddleGoods 的 get 方法获取已购买的挡板商品列表:

```dart ---->[lib/bricks/06/bricks_game.dart#BricksGame]---- List get buyPaddles => configManager.config.activePaddles .map((e) => PaddleType.values[e]) .toList();

List get buyPaddleSrcs => buyPaddles .map((e) =>e.src) .toList();

List get buyPaddleGoods => goodsManager.goods(GoodsType.paddle) .where((e) => buyPaddleSrcs.contains(e.src)).toList(); ```

此时购买商品后就可以在 shared_preferences.json 中查看到购买商品的信息,此时你也可以通过编辑器修改配置信息,来达到 "作弊" 目的。这也是为什么配置文件不是很好的原因。

image.png


二、背包界面构建

商品的数据准备完毕之后,接下来将完成对背包界面的构建:

| 购买商品 | 背包展示商品 | | --- | --- | | 无标题项目.gif | 97.gif |


1. 背包界面:PackagePage

添加一个 PackagePage 浮层界面表示背包菜单页,通过 PackagePage 组件展示:

image.png

在代码中通过 game.overlays 添加和移除 PackagePage 即可打开/关闭背包界面:

```dart ---->[打开背包界面]---- game.am.play(SoundEffect.uiOpen); game.overlays.add('PackagePage');

---->[关闭背包界面]---- game.am.play(SoundEffect.uiClose); game.overlays.remove('PackagePage'); ```


背包的面板图片背景,使用之前封装的 NineImageWidget 进行宫格缩放。其中布局结构并不复杂,顶部标题和下部内容区域竖直排列;关闭按钮通过 Stack 组件叠放在右上角:

image.png

image.png


2. 背包主题体内容:PackageContent

背包的主体内容通过 PackageContent 组件单独维护,其中有点击 Tab 更改内容数据的需求,所以 PackageContent 选用 StatefulWidget。状态类中定义整型的 _activeIndex 表示激活 Tab 的索引,通过 buyGoods 方法根据激活索引得到对应购买类型的商品列表:

```dart ---->[lib/bricks/06/overlays/packagepage/packagecontent.dart]---- class PackageContent extends StatefulWidget { final BricksGame game;

const PackageContent({super.key, required this.game});

@override State createState() => _PackageContentState(); }

class _PackageContentState extends State { int _activeIndex = 0;

List get buyGoods { if (_activeIndex == 0) { return widget.game.buyPaddleGoods .map ((e) => BuyGoods(count: 1, goods: e)) .toList(); } else { return widget.game.configManager.config.buyGoods .where((e) => e.goods.type.index == _activeIndex) .toList(); } } } ```


购买的商品列表通过 Wrap 组件包裹展示。每个商品通过 PackageGoodsCell 组件完成构建逻辑。具体界面构建的细节比较简单,就不一一展开了。可以查看源码 package_content.dart

image.png

dart Widget goodsContent() { if (buyGoods.isEmpty) { return const Expanded( child: Center( child: Text('暂无道具', style: TextStyle(color: Colors.white)), ), ); } return Wrap( spacing: 8, runSpacing: 8, children: buyGoods .map((e) => PackageGoodsCell( buyGoods: e, game: widget.game, onSelect: _onSelectGoods, )) .toList(), ); }


3. 背包中的交互事件

点击 Tab 时,更新激活索引并重新构建。这样 buyGoods 将根据新索引,计算出当前索引对应的商品,从而保证界面上展示商品的准确性:

dart ---->[lib/bricks/06/overlays/package_page/package_content.dart]---- void _onTabChange(int index) { setState(() { _activeIndex = index; }); }


在主页面中,背包内可以切换激活挡板,游戏关卡内不希望切换面板;只有关卡中才可以使用道具商品。此时可以通过 overlays.isActive 来校验是否存在主界面,另外在 BricksGame 中添加一个 useGoods 方法,用于关卡内使用道具的逻辑操作,将在后续实现:

```dart ---->[lib/bricks/06/overlays/packagepage/packagecontent.dart]---- void _onSelectGoods(Goods goods) { if(widget.game.hasHomePage){ if (goods.type == GoodsType.paddle) { widget.game.switchPaddle(goods.src); setState(() {}); } }else{ if (goods.type != GoodsType.paddle) { widget.game.useGoods(goods); } } }

---->[lib/bricks/06/bricks_game.dart#BricksGame]---- bool get hasHomePage => overlays.isActive('HomePage');

void useGoods(Goods goods) { } ```


三、商品功能的实现

这样背包展示购买的商品功能就完成了。下面完成最后一步:实现商品的实际能力。包括 碎石符文挡板功能功能道具 三个方面。


1. 使用符文消除一类砖块

在关卡界面中点击背包,在符文中可以选择对应砖块的符文,使用后,消除关卡中所有对应砖块。如下所示,击碎所有的普通蓝色砖块:

| 关卡内打开背包 | 使用符文消除蓝色砖块 | | --- | --- | | 98.gif | 99.gif |

使用符文道具时需要做两件事:

  • 减少道具数 ;使用道具也维护在 GameConfigManager 中,和添加道具类似,useGoodsInPackage 方法将对应道具数量 -1 并持久化存储:

dart ---->[lib/bricks/06/config/game_config.dart#GameConfigManager]---- Future<void> useGoodsInPackage(Goods goods) { List<BuyGoods> result = List.of(config.buyGoods); BuyGoods buyGoods = config.buyGoods.singleWhere((e) => e.goods.src == goods.src); if(buyGoods.count==1){ result.removeWhere((e) => e.goods.src == goods.src); }else{ BuyGoods newBuyGoods = BuyGoods(goods: buyGoods.goods,count:buyGoods.count-1); result.removeWhere((e) => e.goods.src == goods.src); result.insert(0, newBuyGoods); } config = config.copyWith(buyGoods: result); return saveConfig(); }

  • 触发击碎效果 : 击碎对应符文的砖块非常简单,只要从 brickManager中查找资源名称和符文一致的砖块列表,遍历触发 onBrickWillRemove 方法移除砖块即可 :

dart ---->[lib/bricks/06/bricks_game.dart#BricksGame]---- void useGoods(Goods goods) { //减少道具数 configManager.useGoodsInPackage(goods); if (goods.type == GoodsType.rune) { //击碎砖块 List<Brick> bricks = world.brickManager.children .whereType<Brick>() .where((b) => b.src == goods.src) .toList(); for(Brick brick in bricks){ world.onBrickWillRemove(brick); } } }


2. 挡板的特殊效果实现

这里在回顾一下六个挡板的特殊能力:

image.png

粉红幸运 : 关卡道具出现概率 +5% 蓝色雷霆 : 每击碎第10个砖块,击碎当前列所有砖块 紫色秘宝 : 每击碎10个方块,获得随机道具 黄色宝盆 : 砖块击碎时,获取金币概率 +10% 红色嗜血 : 每击碎第10个砖块,击碎当前行所有砖块。 天蓝双星 :每次发射两颗球。


首先看两个简单的增加概率的挡板 粉红幸运黄色宝盆。 增加道具出现的概率,只需要在 PropManager 中,将道具出现的概率增加 0.05 即可:

image.png

同理,为黄色挡板时,金币出现的概率在 PlayWorld#createCoin 方法中,将金币出现的概率 +0.1 :

image.png


蓝色雷霆红色嗜血 类似,每击碎 10 个砖块触发效果。蓝色消除当前列,红色消除当前行:

| 蓝色雷霆 | 红色嗜血 | | --- | --- | | image.png | image.png |

对于每 10 个砖块的消除时机,可以在 PlayWorld 中增加 _breakCount 计数器。砖块击碎会统一走 onBrickWillRemove 方法,所以可以在其中计数。每到达 10 个触发 handlePaddleEffect 方法处理挡板特效,并重置计数器:

```dart ---->[lib/bricks/06/bricks_game.dart#PlayWorld]---- int _breakCount = 0;

void onBrickWillRemove(Brick brick) { breakCount++; if(breakCount==10){ handlePaddleEffect(brick); _breakCount = 0; } brick.removeFromParent(); propManager.fallOrNot(brick.id); createCoin(brick.absolutePosition + brick.size / 2); game.am.play(SoundEffect.uiSelect); } ```

击碎横向和纵向的砖块,主要是从砖块管理器中取出对应的砖块列表,逐一击碎。这里为了方便根据行列查找砖块,为 Brick 增加了行列数据,在构造时传入:

image.png

```dart void handlePaddleEffect(Brick brick) { if(game.paddleType==PaddleType.blue){ List bricks = brickManager.children .whereType () .where((b) => b.column==brick.column) .toList(); for(Brick brick in bricks){ onBrickWillRemove(brick); } }

if(game.paddleType==PaddleType.red){ List bricks = brickManager.children .whereType () .where((b) => b.row==brick.row) .toList(); for(Brick brick in bricks){ onBrickWillRemove(brick); } } } ```


紫色秘宝 每击碎10个方块,获得随机道具。在 handlePaddleEffect 中继续校验,当挡板是紫色时,在 100 毫秒后随机掉落一个 PropComponent 道具:

dart if(game.paddleType==PaddleType.purple){ Vector2 position = brick.absolutePosition + brick.size / 2; await Future.delayed(const Duration(milliseconds: 100)); int index= game.random.nextInt(Prop.values.length); PropComponent prop = PropComponent(Prop.values[index]); prop.position = position; add(prop); prop.fall(); }


最后 天蓝双星 每次可以发射两颗球。小球的发射是 play 方法的运行,所以在其中校验当挡板类型是 PaddleType.azure 时,延迟 200 ms 添加一个小球:

| 发射两颗球 | | | --- | --- | | 100.gif | 101.gif |

dart void play() { if (game.status == GameStatus.ready) { List<Ball> balls = children.whereType<Ball>().toList(); if (balls.isNotEmpty) { balls.first.run(); if (game.paddleType == PaddleType.azure) { Future.delayed(const Duration(milliseconds: 200)).then((value) { addBall(autoPlay: true); }); } game.status = GameStatus.playing; } } }

到这里,六个挡板的特殊能力就完成了,下面看一下两个功能道具的代码实现。


3. 道具功能

如下所示,两个道具分别用于查看关卡内道具,以及随机掉落一个道具:

| 查看关卡道具 | 随机掉落道具 | | --- | --- | | 102.gif | 103.gif |

道具功效的处理逻辑在 useGoods 方法中,根据选择商品的 src 决定是哪个道具。展示关卡内的功能道具,只要改变道具管理器的 priority 就行了,三秒后将 priority 归 0 即可隐藏道具。掉落随机道具,只需要在 world 中添加坠落的随机 PropComponent 即可:

```dart void useGoods(Goods goods) async{ /// 略同... if(goods.type==GoodsType.function){ if(goods.src=='propshow5s.png'){ world.propManager.priority = 10; await Future.delayed(const Duration(seconds: 3)); world.propManager.priority = 0; }

if(goods.src=='prop_random.png'){
  int index = random.nextInt(Prop.values.length);
  PropComponent prop = PropComponent(Prop.values[index]);
  prop.position = Vector2(kViewPort.width/2, 300);
  world.add(prop);
  prop.fall();
}

} } ```


到这里,挡板、符文和道具的功能就已经实现完毕,购买的商品也可以加入背包。目前打砖块的玩法和代码实现就告一段落了,有其他想法的朋友可以继续拓展玩法。下一篇,将对打砖块项目进行优化,为游戏交互过程中增加一些特效,使其在视觉上体验更好一些。

  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值