第二章教程16:贪吃蛇

本次教程内容:

  • 键盘检测函数
  • 队列
  • 记录系统时间
  • 随机数
  • 改变控制台窗口大小

见一叶落,而知三秋至。

虽然还处于能够把控的状态,但现在的map.cpp代码结构已经越来越复杂,对象之间的知识和责任链也越来越不清晰。开发组小Q计划对代码再做一次重构。

但还没等理出头绪,小Pa却提出了新的需求:用现在的游戏机制,能实现贪吃蛇么?开发组的负责人小Q思考了一下表示:做一定的扩展之后能够实现。

小Pa于是高兴地回去准备地图了。

贪吃蛇还有地图么?不过先不管地图的事,看一看为了实现贪吃蛇的功能,现在游戏机制还需做哪些调整。
首先,必须引入一个新的键盘检测函数,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游戏中被称作“火车”模式),蛇身上有很多位置,每个都必须记录。在不断增加记录头部位置的同时,还需删除尾部的记录。这种操作逻辑,比较自然地让我们相对“队列”这样一种数据结构。

所谓队列结构,与真实的排队是相似。它的特点用一句话概括就是先进先出。蛇的长度就是队的长度,蛇头就是队列尾,新的数据不断增加到队列尾;而蛇尾恰恰是队列头,就的数据从这里删除,并同时从屏幕上消除。

标准的队列有三个函数来实现这一功能:

  • push()      从队列尾推进一个新的数据
  • front()     看看队列头的数据(坐标)
  • pop()       从队列头弹出一个旧的数据


与队列操作结合后,贪吃蛇控制模式,操作流程与相关代码逻辑所在位置如下:

  • 1、英雄默认有一个当前移动方向,每200毫秒,自动向这个方向移动(autoSnake)
  • 2、与移动方向垂直的方向键,改变这个移动方向,键盘控制后,重新计时(setSnake)
  • 3、与移动方向相同或相反的方向键,无作用(setSnake)
  • 4、移动时,先看是否撞墙(trySnake)(即RPG模式下的不可移动),如果撞墙触发撞墙事件(testEvent)。
  • 5、进行糖果检测(testEvent),如果遇到糖果,英雄长度加1,本糖果消失,同时产生新的糖果(newSugar)。
  • 6、尾部隐藏(hideTail):获得队列头的位置,隐藏它,队列头部数据弹出。
  • 7、头部位置推入队列尾(hero.snakeTo)。
  • 8、显示英雄(用原来RPG逻辑就可以)(showHero)。

由于代码比较分散,我们只看其中重点的几段代码:

    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<b),可以这样写

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中。从一件小事,可以看到开发人员的心态一件发生了改变。

课程小结:

现在的贪吃蛇游戏逻辑,还存在有几个很明显的问题。

  • 1、新出糖果如果和蛇身重叠,会被蛇尾删除,很难找到。
  • 2、蛇自己咬自己不会导致失败
  • 3、多次进入贪吃蛇地图后,以前的糖果会成为隐藏糖果,但仍然有效

这些问题都能解决,但会导致现有系统架构更加混乱。
如果这样走下去,不知道是问题先解决掉,还是开发人员先逃离。


本教程每节课的源代码,统一下载地址
链接:https://pan.baidu.com/s/1q4aoYesre1PHaCoV8gkhDQ 
提取码:8den 

 

发布了24 篇原创文章 · 获赞 0 · 访问量 4564

猜你喜欢

转载自blog.csdn.net/xiaorang/article/details/105025542