昨天晚上突发奇想,想设计一款小游戏,自己设计就算了(O(∩_∩)O),还是来款经典的,在网上找到了奇牛学院 Martin 老师的视频课程,做了一晚上,基本的开发现在已经完成了。游戏开发与普通的程序开发设计还是有一些区别的,因为它的算法设计和实现更多样化,本篇博文就详细此款游戏的实现思路与算法设计,为自己做个记录~
EasyX 是针对 C++ 的图形库,可以帮助我们快速上手图形和游戏编程,官网安装即可:https://easyx.cn/
现在最新版本已经适配到 VS2019,注意安装时要把帮助手册安装上,这样便于我们快速查阅EasyX为我们提供的各种库函数接口。
本项目就是基于EasyX库开发90坦克大战游戏。
实现一款和经典的《90坦克大战》一样的游戏,任务是消灭敌对坦克,保护己方领地。防止敌方打破我方围墙而把Boss鹰打败。
下面是此款游戏的实现界面:
下面是成功的情景:己方坦克消除了出现在地图上的所有敌对坦克(不一定和敌对坦克的总量相同,有可能下台坦克还没有出场,地图中的所有坦克就被全部消灭)
下面是失败的情形之一:己方坦克被敌对坦克击中(还有一种是Boss被击中,之后的讲解中会演示这种情景)
游戏初始化部分,主要是在游戏进入的菜单界面设计。如下图所示:
游戏的初始化主要完成以下几个模块:
首先我们需要进行画布的大小设计,在这里我们采用650 * 650像素的大小,注意这个画布大小非常重要,因为我们之后的地图设计和游戏算法实现,需要计算像素点位置。
然后我们需要添加我们的logo图片,还要绘制两个矩形框,里边是说明和启动游戏,可以直接使用EasyX 库为我们提供的一系列绘图函数即可:
上面的坐标参数都是经过详细的测试得出的,是适用于画布大小的。注意不同的画布大小,每个元素的坐标参数都应该进行测试,最终得到合适的元素位置。
画出整体界面之后,我们便可以定义鼠标事件了,EasyX 库也为我们提供了一些捕获鼠标事件的函数,我们主要用到了下面的函数:
// 这个函数用于获取一个鼠标消息。如果当前鼠标消息队列中没有,就一直等待。
MOUSEMSG GetMouseMsg();
上述的MOUSEMSG结构体这用于保存鼠标消息,因此我们在程序中必须定义它,
下图是这个结构体的uMsg成员的取值:
上述搞定了之后,点击“开始”按钮,我们便进入了游戏界面,如下图:
那么首先分析,不考虑坦克,我们的地图有以下几个元素:
这些元素都是以矢量图的形式存在的,那么我们如何将它们拼接在一起呢?我们可以继续使用我们绘制画布时提到的像素概念,我们将整个画布分为26*26的像素单位,如下图所示:
如上图,我们将整个地图(画布)分为了很多小的元素块,因此我们便可以进行地图的绘制了,建立坐标系,计算每一个小元素块的坐标位置,我们只需计算有元素的地方,比如可消除的墙、不可消除的墙、己方领地、boss鹰等。
我们采用了二维数组的方式,以数字0表示空地,数字1表示可消除的墙,数字2表示可消除的墙,数字3表示Boss位置。
那么对于地图的绘制就可以完全按照地图数组中定义的元素位置来进行了,根据地图数组中的不同值输出不同的元素图片即可。
注意,我们的Boss占了4个元素块,这对于我们之后的炮弹碰撞检测是很重要的条件限制。
地图绘制好后,就该是我方坦克出场了,如下图:
它的位置在己方领地左侧,这个很好实现,我们只需计算出它在地图画布中的位置,然后将其输出到屏幕上即可,这里坦克同一占4个元素块(很重要,之后碰撞检测要用到)
我们接下来分析一下我们应该为己方坦克定义哪些成员:
基于上述分析,我们定义的坦克结构体如下(请注意x,y的位置示坦克所属的四个元素块的左上方的坐标位置):
根据上图,可以看到,我们可以实现热键控制己方坦克的上下左右运动,原地转向,并且若有墙等阻挡物坦克是不能前进的,下面讲一下设计思路。
我们的坦克是四个元素块,在地图数组map中我们有标识所有障碍物,0表示空地,因此,坦克能否前进必须要进行一些条件判断,如是否走到了地图的边界?是否有障碍物阻挡等等,那么这就分为了四种情况。
一定要注意x,y的位置在坦克所属的四个元素块的左上方,因此向右和向下的条件判断需要给当前坐标增加2而不是1。
坦克的动态运动过程也很容易实现,就是根据方向提供相应的矢量图,例如,向前行进一步,之前的坦克区域被黑色覆盖,矢量图在新的位置出现。关于坦克的动态运动有两种情况:
我们先来看一下子弹的运动过程:
坦克初始化完毕后,子弹该上场了,己方坦克由玩家自己控制炮弹的发射,而敌方坦克是由系统自动控制发炮的。我们这里先讲解己方炮弹的设计过程。
炮弹也是动态的,因此它的运动过程和坦克一样,因此它设计过程如下:
我们己方坦克和子弹都已经设计完毕了,那么敌方坦克该出场了。它与我们上面讲述的己方坦克最大的区别就是它的出场、前进、发射炮弹都是由系统自动控制的,
这里我们简单起见,游戏设置敌方坦克的数量为15个,每个敌方坦克出场后在map地图数组上的值为100 - 114,之前我们设计的己方坦克出场后在地图数组上的值为200,这样便将他们区分开来,方便我们之后进行碰撞检测。
那么敌方坦克的结构定义与己方坦克是相同的。我们在初始化时默认出场三台坦克,它们的位置分别位于左中右三个部分。默认的方向是向下的,之后每隔5s出场一台坦克。当然每台坦克都有要攻击的目标(这里有两个:己方坦克与老鹰Boss),具体的选路算法我们之后在讲解。
这里我们主要看一下三台坦克出场后,第四台坦克的位置问题,首先,坦克的出场位置一定是在地图最上的一行,默认情况下所有的坦克依次从左边、中间、右边的位置出场,但是有可能某台坦克按照之前的约定位置出场,但是该位置已经有其他坦克了,因此新坦克不能再这里出场,我们设计算法不断的在第一行进行搜索,直到找到了空闲位置便出场。
请注意我光标所指的坦克的运动轨迹:
敌方坦克攻击目标(Boss或己方坦克)只有四种情况(在同行我们可归结为其中任意一种)
具体的选路算法也很容易理解,如果我们要攻击Boss,由于Boss的坐标是固定的(12,24),因此我们只需要知道敌方坦克的位置便可以确定要攻击的目标在坦克的哪个方位,例如Boss在敌方坦克的左下角,那么敌方坦克只有两种选择,向左或者向下,具体选择哪种方案我们交给随机值去处理,使得游戏更具有随机化。
如果要攻击己方坦克,那么己方坦克是运动的,但是其在map数组中的位置也是动态同步变化的,因此我们完全可以和处理攻击boss的方案一样去处理该类情况。
接下来便是真枪实弹的开火了~
子弹也是个动态运动的元素,因此它的动态实现和坦克的动态实现大致相似,这里不再赘述。
由于我们对于己方坦克炮弹的碰撞检测和敌对坦克的碰撞检测在同一函数中实现,因此我们关注以下问题:
下面请注意己方坦克攻击墙体的销毁情况(最后一行):
敌方坦克炮击算法的描述如下:
游戏结束有下面几种情况:
那么我们只需要在碰撞检测函数中返回相应的退出码即可,然后在外部调用处接收。我们根据不同的退出码向屏幕打印不同的退出界面。