坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.1.4

前言

昨天我们完成了核心部分

坏蛋Dan:rust基础学习--基于Bevy实现扫雷小游戏day6

今天我们来完善以及优化下

代码过多,建议直接拉github代码


完善and优化

safe start

现在我们的交互的时候点击第一个是有可能点击到炸弹的,按传统扫雷来说,这是正常的,不过我们可以加个选项用来避免这个问题,稍微提升下体验感。

回到main.rs中,我们在BoardOptions资源插入的时候新增一个字段

这个字段是之前就已经在BoardOptions结构体中定义好的了

不过现在还没有效果,因为我们没有实现相关的逻辑。

我们回到lib.rs中, 给spawn_lines新增一个参数safe_start_entity

直接拿第一个空瓦片,然后往里直接插一个Uncover组件,这样第一帧就能直接监听这个事件然后去掉这个覆盖瓦片了。

然后重新运行下,正常初始情况是会自动有一块展示的了

代码可能有些乱,可以直接pull下之前拉的代码


一键重置

现在我们的system中的逻辑是每一帧都会执行的,但如果我们想要停下行为咋办?(吃饭时你妈:还打游戏,先暂停,饭菜都凉了,吃完再打。打csgo中的你:?)

有时候我们是需要有暂停开关的,暂停之后所有的任务都将被堵塞。

但是我们这里不需要,我们想要的是每回游戏不是通过ctrl + c来停止以及cargo run来运行,我们想要通过某个按键就重置一波。

这里就需要借助state[3] [4]

state会被存放到一个栈里面,这里之后简单称之为状态栈。

一个state有四种状态(一个状态有四个状态。。怪怪的):

  • active:处于栈顶的状态。
  • inactive:在栈里,但是没有处于栈顶。
  • exitedstate出栈。
  • entersstate入栈 。

我们现在的BoardPlugin是没法知道当前是哪个state的,所以我们需要和state关联起来,我们来改下lib.rs中的BoardPlugin,让它包含这个字段

  • stateData[5]这货官方没有解释,不过它实现了SendSync,所以它跨线程是安全的,如果不用它在实现Plugin这个trait的时候就会报错,所以这里其实可以猜出Plugin和多线程多少沾点。

现在我们和当前的state关联了,我们来监听几个状态来优化上面的问题

回到build方法中

  • add_system_set[6]: 将一个SystemSet加入到update stages[7]也就是更新阶段,这里可以理解为hooks,会在调度update阶段触发。
  • SystemSet[8]: 就字面上的意思,一堆system的集合,官方的描述是一个builder也就是构建器在同一时间包含的一堆system
  • on_enter[9]: 接收一个stateData,返回一个SystemSet
  • with_system[10] :往这堆system中插入一个system,相当于监听hooks注册回调。
  • on_update[11]:类比on_enter,不过状态是update
  • on_in_stack_update[12]:类比on_enter,不过是状态栈更新(即栈中任何状态发生改变时触发)?(官方文档没解释)

这里事件触发和监听只有在当前state激活的时候才会执行,create_board依旧只会触发一次,而查询处理包含Uncover组件的实体是任何状态有变化时就会执行。

不过现在还是没有变的,因为我们并没有改变入口,也就是main.rs中的状态注册。

不过在注册状态之前,我们先来处理另一个问题,那就是当我们的board状态变成exited,也就是出状态栈了的时候,应该是要被清除的。

我们来再监听exited状态让board清空自己。

加一个entity用来存储自己,然后在create_board中存储自己

然后新增一个cleanup_board方法作为监听on_exit的回调。

这里的清除是很暴力的,它里面所有子孙组件、实体、资源等全都会被清除。

ok,那么自我清除就完成了(拥有较强的自我管理意识)。

现在我们回到入口注册状态。

我们这个app的状态有俩,一个是ing,另一个是out

现在我们重新跑一下你会发现其实没啥改变,因为我们并没有操纵状态,是的,我们要手动操作。

我们这里监听了俩键盘点击事件,分别是CGC会清除,也就是触发on_exithooks,而G则是重新执行一波这个流程。

现在我们可以不停止程序而重置游戏了。


优化assets

我们还遗漏了一些逻辑:插旗和判断胜利/失败的逻辑。

不过在这之前,我们先来优化下我们assets相关的代码,之前我们是直接硬编码获取的,这样不太好,一般是通过配置项的方式获取。

我们回到resources文件夹下创建一个board_assets.rs的文件。

没啥好说的,我们把前面静态资源相关的都抽出来了,比如上色、炸弹图啥的。

last方法返回切片最后一个元素。

别忘了在mod.rs中导出

那么我们回过头来用它来替换lib.rs中的资源相关的代码。

乱死了,这就是前期没考虑好后期要遭得罪。如果看的眼睛疼,可以看原文章或者直接拉github代码即可。

那么接下来应该就是插入assets了,不过在这之前我们还需要解决一个问题。

我们的资源也就是BoardAssets得在create_board之前加载。

资源加载得在system中,而create_boardstartup_system中,压根没得在它前面插入。

在它之后插又有问题,拿不到assets

那咋办呢?好办,直接堵塞这个create_boardsystem即可,我们把AppState切换到Out,这样初始就堵塞了,然后等加载完资源之后再给他InGame

现在我们重新运行正常

现在我们的资源是配置项的了,也就是说可以自定义了。


降低开发阶段编译要求

现在我们的项目启动非常的慢,因为依赖很多,我们来改下开发阶段的编译要求等级,这个之前在bevy入门那篇文章中说过,这里直接贴代码了

现在稍微快了那么一点。

更多的优化你可以去看入门那篇,里面有提到各种方案来减少编译的时间。

等级相关的:https://bevy-cheatbook.github.io/pitfalls/performance.html


剩余部分:胜利失败逻辑以及插旗

在实现之前,需要确定胜利的条件:整个瓦片图只有炸弹的覆盖瓦片没有被掀开,或者容错几个炸弹,在结束之后如果小于容错个数就算胜利。

我们回到events.rs文件中

我们新增了三个事件

  • BoardCompletedEvent:用于在我们board结束后,这样app就能使用我们的插件去侦测是否胜利等。
  • BombExplosionEvent:看名字就知道是和炸弹有关的事件,这个事件用于炸弹被掀起头盖的时候,也是暴露给app的, 我们的插件不做阻断,这个行为交由app去做。
  • Tile mark event:和TileTriggerEvent行为类似,它是监听鼠标右键,并且不是掀开头盖骨,而是插旗。

然后回到board.rs文件中,我们给Board加个字段以及俩用于事件中的方法

  • marked_tiles:自然是用于存放被flag了瓦片。
  • unmark_tile:移除这个瓦片上的flag
  • is_completed:是否完成游戏。

然后我们来修改下board.rs文件中的tile_to_uncover方法,现在我们不能直接掀起头盖骨了,得先知道这个覆盖瓦片是否被mark flag了,如果是则无作为才对(参考传统扫雷)。

不过这里还有个问题,那就是空瓦片周围的瓦片如果被mark flag了,那么它应该是会被掀开的,包括flag也得被清除。即:只有点击的flag不能处理,其余flag都能处理。

回到board.rs中的try_uncover_tile方法中

掀开的同时去掉这个mark

那么差不多了,我们可以实现这个mark行为的逻辑了

在同文件中创建try_toggle_mark方法

然后我们来发送事件。

回到systems/uncover.rs文件中

然后监听鼠标右键按压事件,到input.rs

发送事件的流程已经打通,接下来就是接收事件了,老规矩,我们在systems/文件夹下创建mark.rs文件。

uncover做的类似,这里是执行插旗逻辑,没啥好说的。

然后别忘了在mod.rs中导出。

那么万事俱备了,就差在app中注册了。

回到lib.rs

ok,现在我们重新运行,正常情况是可以插旗了。

官方代码到这里就结束了,但实际上这里还有胜利/失败的判断逻辑没写。。。

所以我这里做了补充


失败逻辑

我们事件注册等已经实现了,剩下的其实就只有监听到对应事件之后执行的行为逻辑没有实现。

我们先来完成失败的,失败的条件自然就是点击到了炸弹(这里就不考虑支持容错了)

然后失败的结果就是堵塞不允许再点击其它的瓦片,除非用于键盘按C清除。

我这里做简单的处理方案:

设置一个标志位,通过这个标志位来限制是否继续监听事件

先回到board.rs文件中

加了一个标志位:is_failed,另外加了几个计算位置的方法,到时候我们会用到。

然后我们回到input.rs文件中,我们在这里做的事件监听

然后我们回到board_assets.rs文件中加几个assets,我们等会会用到。

然后我们回到main.rs中注册这些资源

看到这里你应该已经知道了这个是一个遮罩

最后回到systems中新建fail.rs

这里我在board实体里创建一个子实体:cover_board,它用来遮罩整个扫雷,然后在它里面正中央还有一个GAME OVER!的文案,用来提示已经结束游戏。

注意这里开头我把is_failed的状态设置为了true,因为这个事件是点到了炸弹才会触发的。

最后别忘了导出事件和注册这个事件

我们来运行下

此时已经无法点击了,只有按下CG重置才能继续交互。

接下来我们来实现胜利的逻辑,这块和失败差不多。


胜利逻辑

我们直接在systems中创建一个completed.rs即可。

不过在这之前,我们来改下is_failed字段,把它改成通用的need_stop_listening_pressed,因为它不应该用来表示胜利/失败状态,已经有别的地方判断过了。

其它地方同步改动,这里就不贴上代码了。

然后回到completed.rs

这里省事直接把fail.rs中的copy过来改了下文案,如果你有其他想法可自行实现。

不过切记要换掉监听的事件

同样别忘了导入导出,代码我就不贴了。

好家伙,玩了几把就为了一个胜利的截图,结果发现了个bug

这里调用is_completed的代码位置是错的

应该调整到board.try_uncover_tile后面

现在我们重新运行下

这回就成功啦~


总结

这一块耗时有些久了,一方面是最近有个内容比较多的需求,没得摸鱼。另一方面这块有些点涉及到新知识,原文章又没解释,官方文档也没多说。。。。。。

现在这个项目已经是完成了的,明天我们来结合wasm_pack打包成webAssembly,这样就能放到浏览器上了。

参考

  1. ^Bevy Minesweeper: Safe Start https://dev.to/qongzi/bevy-minesweeper-part-7-1ko2
  2. ^Bevy Minesweeper: Generic States https://dev.to/qongzi/bevy-minesweeper-part-8-4apn
  3. ^state https://docs.rs/bevy/0.9.1/bevy/ecs/prelude/struct.State.html
  4. ^state例子 https://github.com/bevyengine/bevy/blob/v0.9.1/examples/ecs/state.rs
  5. ^stateData https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/trait.StateData.html
  6. ^add_system_set https://docs.rs/bevy/0.9.1/bevy/app/struct.App.html#method.add_system_set
  7. ^add_default_stages https://docs.rs/bevy/0.9.1/bevy/app/struct.App.html#method.add_default_stages
  8. ^systemSet https://docs.rs/bevy/0.9.1/bevy/ecs/prelude/struct.SystemSet.html
  9. ^on_enter https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.on_enter
  10. ^with_system https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.with_system
  11. ^on_update https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.on_update
  12. ^on_in_stack_update https://docs.rs/bevy/0.9.1/bevy/ecs/schedule/struct.SystemSet.html#method.on_in_stack_update
  13. ^flags https://dev.to/qongzi/bevy-minesweeper-part-9-534e
  14. ^flags https://dev.to/qongzi/bevy-minesweeper-part-10-5hie

编辑于 2023-02-16 00:41・IP 属地广东