见一叶落,而知三秋至。
虽然还处于能够把控的状态,但现在的map.cpp代码结构已经越来越复杂,对象之间的知识和责任链也越来越不清晰。开发组小Q计划对代码再做一次重构。
但还没等理出头绪,小Pa却提出了新的需求:用现在的游戏机制,能实现贪吃蛇么?开发组的负责人小Q思考了一下表示:做一定的扩展之后能够实现。
贪吃蛇还有地图么?不过先不管地图的事,看一看为了实现贪吃蛇的功能,现在游戏机制还需做哪些调整。
首先,必须引入一个新的键盘检测函数,getch()函数能够直接响应任意键,但毕竟是阻塞函数。意思是当程序运行到这里后,就会暂停,等待用户的输入。
对于现阶段的迷宫游戏来说,这样的功能已经足够了。但贪吃蛇不行,必须在用户不按键盘时,仍然保持移动。这就必须引入kbhit()函数,这个函数的作用是检测键盘,如果发现用户有按下键盘,再调用getch()获得所按的键,否则可以运行其他代码,比如蛇的移动。加上kbhit()后,由于处理RPG和贪吃蛇的键盘控制逻辑有明显的不同,所以将具体处理逻辑拆分成两个函数。增加一个地图的模式属性(mode)对地图做一个区分,究竟是RPG地图,还是贪吃蛇地图。新的键盘监听函数,源代码如下。
void listen(){
int a;
while(1){
if (kbhit()){
a= getch();
if (currentMap.mode==1) {
handleKeyRPG(a);
} else {
handleKeySnake(a);
}
} else {
if (currentMap.mode==1) {
// 处理NPC的移动
} else {
// 处理蛇的自动移动
autoSnake();
}
}
}
}
其实RGP游戏中,我们看到会自己走动的NPC,也必须依靠kbhit()函数才能实现。在这里预留了将来处理NPC自由行动的位置。具体handleKeySnake和autoSnake两个函数具体做了什么,让我们先看一个关键问题:现在的英雄怎样显示和隐藏。
以前的英雄只有一个字符,所以它的当前位置只需x,y两个变量来记录。现在变成一条蛇(或者在RPG游戏中被称作“火车”模式),蛇身上有很多位置,每个都必须记录。在不断增加记录头部位置的同时,还需删除尾部的记录。这种操作逻辑,比较自然地让我们相对“队列”这样一种数据结构。
所谓队列结构,与真实的排队是相似。它的特点用一句话概括就是先进先出。蛇的长度就是队的长度,蛇头就是队列尾,新的数据不断增加到队列尾;而蛇尾恰恰是队列头,就的数据从这里删除,并同时从屏幕上消除。
标准的队列有三个函数来实现这一功能:
与队列操作结合后,贪吃蛇控制模式,操作流程与相关代码逻辑所在位置如下:
由于代码比较分散,我们只看其中重点的几段代码:
void autoSnake(){
DWORD t1 = GetTickCount();
if (t1- pTime>= 200){
trySnake(hero.dirx, hero.diry);
pTime= t1;
}
}
这里值得注意的是GetTickCount函数,获取当前的毫秒数。用当前毫秒数与上次毫秒数之差实现每200毫秒移动一次。
移动控制的主逻辑代码在trySnake
void trySnake(int dx, int dy){
int tox, toy;
tox= hero.x+ dx;
toy= hero.y+ dy;
// 看是否撞墙
if (currentMap.mapInfo[toy][tox]== ' ') {
// 不撞墙,检测糖果
testMove(tox, toy);
// 无论是否吃到糖果,移动都继续
hero.dirx= dx;
hero.diry= dy;
hideTail();
hero.snakeTo(tox, toy);
showHero();
} else {
// 撞墙,触发事件
testEvent("hitwall_"+ currentMap.name);
}
}
生成新糖果的代码:
void newSugar(string aEaten){
if (aEaten!=""){
eventList.erase(aEaten);
}
// 移动糖果
int x1= rand()%(currentMap.w/2- 2)* 2+ 2;
int y1= rand()%(currentMap.h- 3)+ 1;
Event evt1;
evt1={"move", "Snake,"+int2str(x1)+","+int2str(y1), "sugar", ""};
//cout << evt1.getTriggerText() << endl;
eventList.insert(make_pair(evt1.getTriggerText(), evt1));
showSugar(x1, y1);
}
这里值得注意的是怎样产生一个随机的位置。
函数rand()可以产生一个0~RAND_MAX的随机数,我们知道RAND_MAX是一个很大的数字就好了。
我们现在希望这个数字出现在一个范围内,具体来说,横坐标在2到地图宽度范围内,纵坐标在1到地图高度内。
我们使用求余数(%)的机制来实现区间随机数的获取。
一般地说,如果想获取从a到b之间的随机数(包含a与b,a
rand()%(b-a+1)+ a
其余代码比较简单,这里就不一一贴上来了。读者到文章底部的下载地址下载本课对应代码即可。
很快小Pa拿来了她设计的新地图,不但加了贪吃蛇地图,还加了一个两个游戏的总入口。
小Pa的兴趣很高,看她现在的状态,还打算设计更多的游戏模式。
但开发组小Q可是越来越郁闷了,为了实现贪吃蛇的功能,原本已经不甚清晰的代码结构,现在更是雪上加霜。
但小Q还是努力又增加了最后一个功能。由于现在的地图大小差别很大,以前固定大小的控制台窗口有时会导致显示上的混乱。于是在载入地图的函数中,增加了根据当前地图的大小,来设置控制台窗口大小的功能。
void setConsoleSize(int w, int h){
string str1;
str1="mode con cols="+ int2str(w* 2)+" lines="+ int2str(h);
system(str1.c_str());
}
这个函数本当是放在tools.cpp之中,但小Q为了省事,随手把它放在了map.cpp中。从一件小事,可以看到开发人员的心态一件发生了改变。
现在的贪吃蛇游戏逻辑,还存在有几个很明显的问题。
这些问题都能解决,但会导致现有系统架构更加混乱。
如果这样走下去,不知道是问题先解决掉,还是开发人员先逃离。
本教程每节课的源代码,统一下载地址
链接:https://pan.baidu.com/s/1q4aoYesre1PHaCoV8gkhDQ
提取码:8den