昨天我们完成了核心部分
坏蛋Dan:rust基础学习--基于Bevy实现扫雷小游戏day6
今天我们来完善以及优化下
代码过多,建议直接拉github
代码
现在我们的交互的时候点击第一个是有可能点击到炸弹的,按传统扫雷来说,这是正常的,不过我们可以加个选项用来避免这个问题,稍微提升下体验感。
回到main.rs
中,我们在BoardOptions
资源插入的时候新增一个字段
这个字段是之前就已经在BoardOptions
结构体中定义好的了
不过现在还没有效果,因为我们没有实现相关的逻辑。
我们回到lib.rs
中, 给spawn_lines
新增一个参数safe_start_entity
。
直接拿第一个空瓦片,然后往里直接插一个Uncover
组件,这样第一帧就能直接监听这个事件然后去掉这个覆盖瓦片了。
然后重新运行下,正常初始情况是会自动有一块展示的了
代码可能有些乱,可以直接pull
下之前拉的代码
现在我们的system
中的逻辑是每一帧都会执行的,但如果我们想要停下行为咋办?(吃饭时你妈:还打游戏,先暂停,饭菜都凉了,吃完再打。打csgo中的你:?)
有时候我们是需要有暂停开关的,暂停之后所有的任务都将被堵塞。
但是我们这里不需要,我们想要的是每回游戏不是通过ctrl + c
来停止以及cargo run
来运行,我们想要通过某个按键就重置一波。
state
会被存放到一个栈里面,这里之后简单称之为状态栈。
一个state
有四种状态(一个状态有四个状态。。怪怪的):
active
:处于栈顶的状态。inactive
:在栈里,但是没有处于栈顶。exited
:state
出栈。enters
:state
入栈 。我们现在的BoardPlugin
是没法知道当前是哪个state
的,所以我们需要和state
关联起来,我们来改下lib.rs
中的BoardPlugin
,让它包含这个字段
stateData
[5]这货官方没有解释,不过它实现了Send
和Sync
,所以它跨线程是安全的,如果不用它在实现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
。
现在我们重新跑一下你会发现其实没啥改变,因为我们并没有操纵状态,是的,我们要手动操作。
我们这里监听了俩键盘点击事件,分别是C
和G
,C
会清除,也就是触发on_exit
的hooks
,而G
则是重新执行一波这个流程。
现在我们可以不停止程序而重置游戏了。
我们还遗漏了一些逻辑:插旗和判断胜利/失败的逻辑。
不过在这之前,我们先来优化下我们assets
相关的代码,之前我们是直接硬编码获取的,这样不太好,一般是通过配置项的方式获取。
我们回到resources
文件夹下创建一个board_assets.rs
的文件。
没啥好说的,我们把前面静态资源相关的都抽出来了,比如上色、炸弹图啥的。
last
方法返回切片最后一个元素。
别忘了在mod.rs
中导出
那么我们回过头来用它来替换lib.rs
中的资源相关的代码。
乱死了,这就是前期没考虑好后期要遭得罪。如果看的眼睛疼,可以看原文章或者直接拉github
代码即可。
那么接下来应该就是插入assets
了,不过在这之前我们还需要解决一个问题。
我们的资源也就是BoardAssets
得在create_board
之前加载。
资源加载得在system
中,而create_board
在startup_system
中,压根没得在它前面插入。
在它之后插又有问题,拿不到assets
。
那咋办呢?好办,直接堵塞这个create_board
的system
即可,我们把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
,因为这个事件是点到了炸弹才会触发的。
最后别忘了导出事件和注册这个事件
我们来运行下
此时已经无法点击了,只有按下C
和G
重置才能继续交互。
接下来我们来实现胜利的逻辑,这块和失败差不多。
我们直接在systems
中创建一个completed.rs
即可。
不过在这之前,我们来改下is_failed
字段,把它改成通用的need_stop_listening_pressed
,因为它不应该用来表示胜利/失败状态,已经有别的地方判断过了。
其它地方同步改动,这里就不贴上代码了。
然后回到completed.rs
中
这里省事直接把fail.rs
中的copy
过来改了下文案,如果你有其他想法可自行实现。
不过切记要换掉监听的事件
同样别忘了导入导出,代码我就不贴了。
好家伙,玩了几把就为了一个胜利的截图,结果发现了个bug
这里调用is_completed
的代码位置是错的
应该调整到board.try_uncover_tile
后面
现在我们重新运行下
这回就成功啦~
这一块耗时有些久了,一方面是最近有个内容比较多的需求,没得摸鱼。另一方面这块有些点涉及到新知识,原文章又没解释,官方文档也没多说。。。。。。
现在这个项目已经是完成了的,明天我们来结合wasm_pack
打包成webAssembly
,这样就能放到浏览器上了。
编辑于 2023-02-16 00:41・IP 属地广东