# Wafflenano小游戏-推箱子 **Repository Path**: fancifulnarrator/push-box ## Basic Information - **Project Name**: Wafflenano小游戏-推箱子 - **Description**: 一个基于黑胡桃实验室`Waffle nano`开发版和`Joystick Shield`游戏摇杆模块开发的推箱子小游戏 - **Primary Language**: Python - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-08-20 - **Last Updated**: 2022-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Waffle小游戏——推箱子 设计流程和感想 ## 引言 ​ 今天,我将为大家带来一个基于黑胡桃实验室`Waffle nano`开发版和`Joystick Shield`游戏摇杆模块开发的推箱子小游戏,以讲述这一次`openharmony`游戏开发的流程与乐趣。 ​ 这可能不是一个很有技术力的案例,只是还原了一个非常简单的项目制作过程,包含其中的磕磕碰碰,遇到的各种困难和挑战。属于一个最基础的例子,希望能够给你一些帮助和启发。 ​ 项目中用到的一些模型的草图和界面的文案放在了这个项目开源的[仓库](https://gitee.com/fancifulnarrator/push-box)中,完整的代码也在其中,如果你想要复现这个案例,欢迎参考。 ## 设计思路 ​ 推箱子是经典的规则简单,关卡固定,易于理解的游戏。即使一个任何游戏都不会玩的人,告诉他上下左右的移动方式,推到点上的目标,他都能够理解。游戏主体的设计步骤主要分三部分:设计物体模型,地图关卡等基本要素;实现移动,推动等游戏玩法;完成胜利判断,选关逻辑,主界面等优化设计。 ### 一.地图和模型 ​ 在推箱子这个完全由方块组成的游戏中,我们只需要考虑一共有哪几种方块。简单的统计后,得出了下面的表格。 | 游戏物体 | 代号 | | ---------------------- | ---- | | 墙壁 | A | | 道路 | B | | 箱子 | C | | 目标点 | D | | 已到达目标点的箱子 | E | | 游戏主角 | F | | 踩在目标点上的游戏主角 | G | ​ 那么接下来,我们需要设计地图代码和每个物体的模型。 #### 地图设计 ##### 低效的储存方式,列表 ​ 对于一个完全方形的地图来说,二维数组,或者说`Python`中的二维列表自然是我们脑海中首当其冲的代码化形式。最大长宽x、y的地图就开`x*y`的二维列表,是什么物体就在列表对应的坐标中记录相应的代号,到显示的时候直接一一对应的显示物体,以达到打印整张地图的目的。直到整个游戏做完,我都是这样想的,实际效果如下(伪代码): ```python class allMaps: map_001 = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 2, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 4, 2, 4, 3, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 3, 2, 4, 5, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 1, 1, 4, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] ...... ``` ​ 因为`Wafflenano`自带的屏幕分辨率为240*240,所以我为了方便把其分为边长15的`16*16`个正方形,每一个二维数组的size都是`16*16`,最后的结果——是内存不足。可惜的是,当时我并没有意识到问题根源所在,而是从错误的角度尝试解决问题,先是将原定的十个地图砍成5个,又将一些功能删减,删除。 ​ 实际上,这种地图储存方式是有问题的,这是毋庸置疑的。比如我上面的地图,开了256个地址却只使用了40个,大量的内存被浪费。而且这样写在代码量上也有很大影响。 ##### 合理的储存方式,散列表 ​ 地图其实并不是方形的,而是一串信息,表示了每个坐标的情况。因此散列表是储存的较好选择,而且,Python的字符串是不可变类型,这一点点好处在后文说明。还可以在每一串地图信息的固定位置储存一些相关信息,比如以下代码,我在前两位储存了主角的位置,以便使用。 ```python class Maps: def __init__(self, screen): self.screen = screen self.map_001 = "4402A03A04A12A13D14A22A23B24A25A26A27A30A31A32A33C34B35C36D37A40A41D42B43C44F45A46A47A50A51A52A53A54C55A63A64D65A73A74A75A" ``` #### 模型设计 ​ 在之前的地图设计中,`16*16`的设计使每一个游戏物体在`240*240`的地图中都显得非常小,再加上没有认真设计模型,最终效果很糟糕,具体情况如下: ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo1.jpg) ​ 其中每一个物体的尺寸都是`15*15`,设计也只有简单的寥寥几根线条,配色也很难看。而造成这一切的主要原因除了不会画画,就是对于当前设计场景的认识不明确了。这是一个很小的屏幕,并不像pc显示屏那么大,那么清晰,电脑游戏的设计思路可能是统一化的地图区域,并且尽可能的在当前的界面的边角处显示各种信息和提示,而小屏幕就应该在有限的空间里提升图像质量,加上简单的提示。经过一番学习和模仿后,界面设计成了下面这样: ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo2.jpg) ​ 虽然也称不上多好看,但是可以说不用特地解释,也基本上能让人看懂每个物体想表达什么意思了。 ​ 其中每个物体的具体参数可以在我的[仓库](https://gitee.com/fancifulnarrator/push-box)中找到。 ​ 到这里,一个不会动的地图就设计完成了。接下来的工作就是怎样让这个地图“可以玩”了。 ### 二:移动、推动和胜利 #### 移动 == 推动 ​ 主角的每次移动,按照我们这种“7种游戏物体”的思想,本质上就是游戏物体的变化。那么,只要判断出移动之后“出发地”和“目的地”的情况,从而改变当前的地图,最后把新的地图打印出来,一次移动行为就完成了。由此看来,移动是两个目标的变化,推动箱子也只不过是在中间加了个箱子罢了,只要对三个对象判断不就行了吗?统计所有可能的情况后,得到了以下的表格: | 变化前(文字) | 变化后(文字) | 字母表示 | | :-------------------------------------- | ------------------------------------ | ---------- | | 主角 -> 道路 | 道路 主角 | FB -> BF | | 主角 -> 目标点 | 道路 踩在目标点上的主角 | FD -> BG | | 主角 -> 箱子-> 道路 | 道路 主角 箱子 | FCB -> BFC | | 主角 -> 箱子-> 目标点 | 道路 主角 完成箱子 | FCD -> BFE | | 主角 -> 完成箱子-> 道路 | 道路 踩在目标点上的主角 箱子 | FEB -> BGC | | 主角 -> 完成箱子-> 目标点 | 道路 踩在目标点上的主角 完成箱子 | FED -> BGE | | 踩在目标点上的主角 -> 道路 | 目标点 主角 | GB -> DF | | 踩在目标点上的主角 -> 目标点 | 目标点 踩在目标点上的主角 | GD -> DG | | 踩在目标点上的主角 -> 箱子-> 道路 | 目标点 主角 箱子 | GCB -> DFC | | 踩在目标点上的主角 -> 箱子-> 目标点 | 目标点 主角 完成箱子 | GCD -> DFE | | 踩在目标点上的主角 -> 完成箱子-> 道路 | 目标点 踩在目标点上的主角 箱子 | GEB -> DGC | | 踩在目标点上的主角 -> 完成箱子-> 目标点 | 目标点 踩在目标点上的主角 完成箱子 | GED -> DGE | #### 通关判断 ​ 如何实时判断玩家是否通过了本关?一开始,还在用二维列表储存地图时,我的想法是通关了,就代表每个目标点位置上都有一个箱子,或者说物体E,只要每次移动后用一个判断函数判断一次就可以实时的知道是否通关了,写法如下: ```python def isWin(self, Maplevel, Nowmap): if Maplevel == 1: if Nowmap[2][7]==6 and Nowmap[4][10]==6 and Nowmap[5][5]==6 and Nowmap[7][8]==6: return True elif Maplevel == 2: if Nowmap[4][10]==6 and Nowmap[5][10]==6 and Nowmap[6][10]==6: return True elif Maplevel == 3: if Nowmap[5][5]==6 and Nowmap[5][6]==6 and Nowmap[6][5]==6 and Nowmap[6][6]==6: return True elif Maplevel == 4: if Nowmap[6][6]==6 and Nowmap[7][6]==6 and Nowmap[7][7]==6 and Nowmap[7][8]==6 and Nowmap[7][9]==6: return True elif Maplevel == 5: if Nowmap[5][4]==6 and Nowmap[6][4]==6 and Nowmap[7][4]==6: return True return False ``` ​ 后来,随着地图储存方式的改变,索引特定位置不再是简单的事,不过,一种新的,属于字符串的,更优秀的方法出现了:既然“箱子”和“到达目标点的箱子”是两种游戏物体,那么只要代表箱子的字母“C”不存在于整个地图中,这一关一定是已经完成了。而因为Python字符串的方便性质,这个函数非常简单: ```python def isWin(self, data): return True if "C" not in data else False ``` ### 三:主界面与选关 #### 主界面 ​ 在原本的思路中,主界面只会出现一次,其中包含了各种信息。 ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo3.JPG) ​ 信息有游戏物体解释和玩法提示。 ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo4.JPG) ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo5.JPG) ​ 在优化之后,游戏中界面不再有任何提示,除了“按A键返回主界面“(简化为A:M)。而主界面成了一个随时可以返回的地方,由它来完成”重玩本关“、”关卡选择“等功能。另外把“游戏物体解释”这种可能不是必要的功能删除了。 ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo6.JPG) ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo7.JPG) #### 选关 ​ 地图在储存时是字符串形式,而Python的字符串是一种不可变类型,因此是将字符串转换为列表,修改其中的值,然后再转回字符串。 ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/GIF1.gif) ## 程序结构 ​ 程序的核心思想就是将所有方法放入库中,在主函数中调用。 ​ 整个项目的思维导图如下: ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo11.JPG) ​ 具体代码表现如下: ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo9.jpg) ![](https://gitee.com/fancifulnarrator/push-box/raw/master/image/photo10.jpg) ## 成果展示 ```HTML ```