基于80x86汇编语言的推箱子游戏
一、游戏背景介绍
推箱子是一款众所周知的益智游戏,此游戏只有一个目标:将游戏中的所有箱子推到指定位置。简单易懂的游戏规则是此款游戏成功的根本原因。
此游戏具有如下特点:简便的操作、复杂的关卡以及层级递升的难度。简便的操作是吸引玩家的基础,简单易懂的操作不仅为玩家提供了方便,也为游戏的流行打下了坚实的基石。游戏中的关卡设计是游戏的重要环节,层级递升的关卡难度是游戏引人入胜的制胜法宝。相反,糟糕的难度设计只会令玩家厌烦此游戏,使游戏失去乐趣。
这是一个来自日本的古老游戏。起初,这是为了解决在箱子繁多的仓库中,如何在狭窄且障碍重重的道路上,把箱子从初始位置推到给定的目标位置。这款游戏看似没有什么难度,但实际上是对空间推理能力和逻辑思维的考察。将所有的箱子移动到指定的位置并非易事,稍有不慎箱子就会被推入死角,无法继续移动,导致闯关失败。玩家也要合理的安排推入箱子的次序,不妥当的推入顺序,无疑是自行增大了游戏难度。
关卡1设计如图1所示:
这是在x86环境下, 用汇编语言编写的一款简单的单机推箱子小游戏,“小人”(在本游戏中的“小人”为颜色不同于箱子)的移动用方向键控制,“小人”每次只能推动一个箱子。本游戏完整的实现了游戏提示列表中列出的游戏功能、“小人”的移动控制、“小人”推动箱子的移动以及游戏关卡通过的胜利判定等功能。游戏中通过输入相应操作的首字母实现游戏提示列表中列出的游戏功能。其中:P表示开始游戏、C表示选择关卡、R表示重新开始此关卡、M表示游戏音乐的开启与关闭、E表示退出游戏。为了保证游戏输入的兼容性,接收到非菜单中列出的字母时,弹出Error!提示用于表示用户输入了无关字母,并提示用户重新输入菜单列表中列出的字母,而非在接收到无关字母后直接闪退,通过这种方式优化玩家的游戏体验。不如人意的是,此游戏存在两个弊端:不可以回退到“小人”的上一步位置、不可以记录闯关的战绩。
游戏开始后,通过方向键移动“小人”的位置,“小人”在遇到箱子的阻挡后将会推动箱子移动,但是“小人”不可以拉动箱子,这也符合了游戏本身的设计。玩家需要通过自己的操作使“小人”推动箱子到达指定位置,待所有的箱子均到达指定位置后,游戏将会自动跳转到第2关。在玩家进行操作的同时,玩家可随时输入菜单列表中的字母,进行相对应的游戏操作,符合玩家的游戏操作需要。例如:若玩家在游戏过程中发现无论如何操作都无法将箱子移动到指定位置,即可按R键重新开始游戏,这提升了玩家的游戏体验,丰富了游戏的灵活性。
关卡2设计如图2所示:
关卡1与关卡2十分相似,游戏逻辑基本相同。但关卡2相较于关卡1来说,顺利完成关卡所需移动的步数更多,完成关卡所需时间也长于第一关,这一关进一步考察玩家的空间推理能力和逻辑思维能力。由于最短操作次数的增多使得操作出现错误的可能性加大,需要玩家更加仔细谨慎,才能顺利通过关卡2。当然,游戏难度增加也使游戏更有趣,增加了游戏的可玩性。
二、核心算法思想
2.1 游戏信息绘制模块
本模块的主要任务是将游戏中的墙壁、目标位置、提示信息、箱子、人物输出到游戏窗口上。为了完成上述信息的输出,将这些信息抽象为合适的字符代替。上述信息可分为静态信息以及动态信息。游戏中的墙壁、目标位置、提示信息为静态信息,在游戏过程中不会改变。而箱子和人物均为动态信息,其随着玩家的控制而移动位置。
静态信息的解决方案:以字符串的形式将静态信息存储到开辟的新的段内存中。在游戏主程序段开始执行后,首先输出存储到内存中的墙壁字符串以及提示信息字符串,完成游戏界面以及游戏提示的打印。在不同的关卡中游戏中箱子的目标位置不尽相同,故此在输出箱子的目标位置前首先应判断当前所在关卡。当前所在关卡信息存储在DATAAREA段中的名为level的偏移地址中。level的值为31H时,则当前位于第一关。若其为32H,则当前位于第二关。判断关卡位置后,打印关卡对应的箱子目标位置。
动态信息的解决方案:箱子或人物在被玩家移动位置后,需要在之前的位置打印空字符。根据存储在DATAAREA段中的名为loc_x与loc_y的偏移地址,重新判定人物或箱子的新位置,并在新位置输出其相对应的字符,保证原位置以及新位置的信息显示正确。
2.2 游戏输入控制模块
本模块的主要任务是接收玩家输入的按键执行对应的操作。游戏中需要通过接收玩家输入的按键,控制游戏重新开始,游戏音效的开启与关闭,关卡选择,游戏退出以及控制人物移动等重要游戏功能。游戏中本功能的主要实现在主程序的Get_act段,在获取到用户输入的按键后与游戏中的操作按键进行逐一比对。比对游戏中已经设定的按键即可确定用户输入的按键,进而跳转到按键对应的功能实现程序段中,继续执行相应操作,从而保证人物的正常移动、游戏跳转以及游戏功能的实现。
2.3 人物移动控制模块
本模块的主要任务是控制人物的正确移动。接收到人物移动的输入指令后,将光标移动到人物移动后的位置,读取光标位置的字符,将其与墙壁字符以及箱子字符进行比较。如果当前位置为墙壁字符,人物移动后将会撞墙,将光标重新放回人物所在位置,并发出蜂鸣器警报提示人物撞墙,正常返回游戏。如果当前位置为箱子字符,人物移动后会推到箱子。字符判断完成后,记录人物移动的方式,用于判断箱子是否可以移动时确定需要判断的目标位置。判断箱子是否可以移动的思路如下:将光标移动到人物位置的移动方向两个位置处的字符,是否为墙壁位置即可确定箱子是否可以移动。例如:人物向上移动,则判定人物位置的上两个位置处,是否是墙壁或者箱子对应的字符。如果此处不是墙壁字符,则箱子可以被推动,否则,发出蜂鸣器警报提示人物不可移动。这样就可以保证人物可以移动到空白位置或者推动单个箱子,而不是移动到墙壁位置,或者同时推动两个箱子,保证游戏逻辑的准确。
2.4 游戏胜利判定模块
本模块的主要任务是判定箱子是否推到目标位置以及判定关卡是否通过。箱子被推动后,需要判定箱子是否落在目标位置。由于在确定的关卡中,箱子的目标位置是确定的。所以只需要判断箱子被推动后的所在的位置,是否与特定的目标位置重合。如果,位置重合数量小于特定的目标位置数量,证明玩家尚未通过关卡,玩家需要继续游戏推动箱子到指定位置。反之玩家通过关卡,此时清空游戏数据,重新绘制游戏地图为下一关地图,跳转到下一关后继续进行游戏。
三、核心算法流程图
游戏中最繁琐的功能实现部分是判定箱子是否可以移动。上面已经详细介绍了判定箱子是否可以移动的算法,为直观的表述算法的原理以及运作过程,给出此算法的流程图如图3所示:
游戏中的人物控制也是一项十分艰巨的任务。相较于上述箱子的判定而言,人物控制需要考虑的特殊情况更多,过程也更加复杂。为直观的表述此算法的原理以及运作过程,给出此算法的流程图如图4所示:
四、开发中遇到的问题
4.1 问题一:动态信息显示到屏幕上后无法修改
项目开发初期,希望通过输出字符串,一次性将所有的信息全部呈现在屏幕上。为此分别开辟内存空间用于存储替代游戏中的墙壁、目标位置、提示信息、箱子以及人物所用的字符串。将所有的字符串输出到屏幕后,发现玩家利用按键控制人物进行移动时,无法确定将人物移动到哪个位置上。开始,我希望在人物移动到的新位置处输出替代人物的字符串,并在人物移动前的位置上输出空字符串。这个方法看似可行,但是在实际应用时,无法确定人物的具体位置。于是将人物的x,y坐标存储在两个变量中。人物移动后修改两个变量的值,并利用其修改人物的输出位置。
通过对比参考后,很容易发现:游戏中箱子的位置也是随着玩家控制不断变化的。为此将要输出到屏幕的信息进一步分类为静态信息以及动态信息,实现信息的分类管理。利用解决人物问题的思路,完成了箱子信息的动态输出。
4.2 问题二:人物、箱子的移动控制
项目开发前期,在对人物控制的编写过程中发现,人物可以移动到墙壁、箱子所在位置,甚至是游戏区域外。为了解决问题,就要确定一个或多个目标位置,通过其对应的字符判断人物是否可以正常移动。在解决此问题的过程中,发现目标位置的确定与人物的移动方向有关。故此,将人物的移动方向存储到DI以及temp中。在接收到玩家输入的按键后,修改两变量的值,保存人物移动的方向信息。目标位置确定后,读取目标位置的字符串,即可判定人物是否可以正常移动。箱子的移动控制与人物的移动控制基本相似,只是需要判断的目标位置相较于人物的移动控制更多,情况也略复杂一些。
4.3 问题三:多关卡数据的存储与转换
项目开发中期,在关卡的切换问题上一直没有任何思路。关卡切换后应该将屏幕数据清空,再重新显示第二关的地图,顺着高级语言的思路一直没有找到合适的解决方案。经过查阅资料,发现汇编语言与高级语言不尽相同,深入了解后发现解决此问题十分简单。只需要分配一个用于存储当前所在关卡的变量,判断变量的值输出不同关卡对应的字符串即可,而非向高级语言一样考虑其扩展性以及可重用性。
4.4 问题四:箱子推到目标位置以及通过关卡的判定
项目开发后期,必须要确定箱子是否被推到了指定位置。在开始解决此问题时,采用了控制“小人”移动的思路,即:移动光标到指定位置后读取此位置的字符,判断此字符是否为目标位置所对应的字符。引入一个新的变量,用于存储正确推动到指定位置上的箱子数量。这个方案看似可行,但是如果箱子被推到了目标位置后又被移出,就会出现误判。由于每个关卡的目标位置是确定的,所以只需要将光标依次移动到确定的目标位置,判断光标处字符是否与代替箱子所用的字符一致。只有全部匹配才能顺利通过该关卡,这也符合游戏的设定。
4.5 问题五:游戏音效的播放
在完善项目时,发现玩家操控的“小人”撞到墙壁后,游戏不会出现提示信息,这对玩家十分不友好。为解决这个问题,我查阅了有关资料,深入了解利用汇编语言调用蜂鸣器运作发声。并编写了相关代码,使得“小人”撞到墙壁以及“小人”试图推动两个箱子时,发出蜂鸣器警报,提示玩家出现操作错误。
为了增强游戏的互动性和可玩性,我查阅了运用汇编语言播放游戏背景音乐的相关资料。通过编写music子程序,利用此程序段读取MDATA段中存储的游戏音乐频率数据以及音乐时间数据,播放游戏的背景音乐。
六、心得体会
学习汇编语言的过程中,我发现学习汇编语言不如学习高级语言那样容易。在和C/C++类似的高级语言中,程序的逻辑十分简洁明了、易于掌握。汇编语言则与此不同,汇编语言更接近于机器语言,写起来比高级语言更加繁琐,出现错误调试起来也不是很方便。进行项目编写时,代码实现的过程十分艰难。但比较好的是,运行结果是令人满意的,虽然缺少了推箱子游戏的倒退一步和记录战绩的功能,但是推箱子的基本逻辑功能已经完整实现。
在我看来,学习汇编语言不仅要学习其用法,也要通过其了解计算机基本的体系结构。一学期下来,我遇到了许多之前不熟悉的概念,在开始理解这些概念时十分困难。随着自己亲力亲为编写小游戏,我发现这些概念也并不是如我开始认为那般困难。所以,只有将学到的概念真正用到实处。才能真正意义上的理解并熟练掌握。
这次的项目编写提高了我自主学习以及解决实际问题的能力。同时,我也看到了自己的诸多不足之处,认清了今后的努力方向,也对以后进一步学习相关课程奠定了坚实基础。