0基础学习C++做贪吃蛇, 边玩儿边学习!(八)switch分支语句和键盘控制——自由移动的蛇

发布于:2022-11-12 ⋅ 阅读:(1029) ⋅ 点赞:(0)

本节我们添加对键盘输入的响应。

#include <iostream>
#include<conio.h>
#include <windows.h>
using namespace std;
//预定义一些游戏参数
#define LENGTH 5  //初始长度
#define DIRECTION 77 //初始方向
#define LevelSize 15  //级别个数
#define NoSymbol ' '//输出符号
#define BodySymbol 'o'
#define HeadSymbol 'O'
#define POSX 10 //初始位置
#define POSY 10
//必要的全局变量
HANDLE handle; //窗口句柄
COORD body[LENGTH]; //蛇体坐标数组
const int speed[LevelSize] = { 1000,666,444,296,196,130,86,56,36,24,16,10,6,4,2 }; //速度级别

//显示
void draw(COORD pos, char symbol) {
    SetConsoleCursorPosition(handle, pos);//设置handle指向窗口光标位置为pos
    cout << symbol;
}
//返回当前方向
COORD Direction(int g = 0) {
    static int d = DIRECTION;//代表当前方向键值
    static COORD dir;
    if (d + g == 152||!g) return dir;
    d = g;//如果新方向与旧方向不相反且有新方向传入,设置新方向
    switch (d) {//通过按键改变方向
    case 72://上键
        dir.Y = -1;
        dir.X = 0;
        break;
    case 80://下键
        dir.Y = 1;
        dir.X = 0;
        break;
    case 75://左键
        dir.X = -1;
        dir.Y = 0;
        break;
    case 77://右键
        dir.X = 1;
        dir.Y = 0;

    }
    return dir;
}
//预备
void ready() {
    for (int i = 0;i < LENGTH;i++) {
        body[i].X = POSX + i*(Direction(DIRECTION).X);//body数组内元素的横坐标递增
        body[i].Y = POSY+i*(Direction(DIRECTION).Y);//纵坐标不变
        draw(body[i], BodySymbol);//在数组每个元素代表的坐标处打印蛇身
    }
    draw(body[LENGTH - 1], HeadSymbol); // 将数组最后一个元素代表的坐标处重新绘制蛇头
}
//设置级别,返回速度
int Level(char isQuick='r') {
    static int level = 0;
    if (isQuick=='+' && level < LevelSize) ++level;
   else if (isQuick=='-'&&level > 0) --level;
    return speed[level];      
}
//暂停
void pause() {
    while (getch() != VK_SPACE);//只要输入的不是空格就一直循环
}
//按键响应
void todo(int key) {
    switch (key)//判断输入
    {
    case 43://加号
        Level('+');break;
    case 45://减号
        Level('-');break;
    case 32://空格
        pause(); break;
    case 224://四个方向键
      Direction(_getch());
    default:
        break;
    }
}
//设置新的蛇身数组
void setBody(int& h, int& t) {
    body[t].X = body[h].X + Direction().X;//将新方向存入body[t]
    body[t].Y = body[h].Y + Direction().Y;
    h = t;//将t的值赋给h,此时body[h]表示的是新头部
    (t == LENGTH - 1) ? t = 0 : t++;//当t等于LENGTH时令t为0,否则t++
}
//
void play() {
     int head = LENGTH - 1;//head和tail表示头尾所在的下标。
     int  tail = 0;
    while (1) {
        Sleep(Level());//休眠200毫秒
        if (_kbhit())
            todo(_getch());
        draw(body[head], BodySymbol);//变头为体
        draw(body[tail], NoSymbol);//去尾
        setBody(head, tail);
        draw(body[head], HeadSymbol);//画新头
        while (_kbhit()) _getch();//清空缓存区
    }
}
int main() {
    //获取句柄
    handle = GetStdHandle(STD_OUTPUT_HANDLE);
    //定义一个光标信息结构的对象
    CONSOLE_CURSOR_INFO cci;
    //将handle指向的窗口光标信息赋给cci
    GetConsoleCursorInfo(handle, &cci);
    //将光标隐藏
    cci.bVisible = FALSE;
    //设置handle指向窗口光标信息为cci
    SetConsoleCursorInfo(handle, &cci);
    ready();
    play();
    return 0;
}

上述代码可以复制粘贴编译运行,效果如下:(代码中改变速度时的输出(level:x)被我删了,gif没改)
请添加图片描述
GIF动图可能看的不明显,大家自己复制粘贴试试。
我们说一下新增的部分代码。

//预定义一些游戏参数
#define LENGTH 5  //初始长度
#define DIRECTION 77 //初始方向
#define LevelSize 15  //级别个数
#define NoSymbol ' '//输出符号
#define BodySymbol 'o'
#define HeadSymbol 'O'
#define POSX 10 //初始位置
#define POSY 10

我们将程序中用到的一些常量都预定义了,方便我们配置游戏(比如可以将初始长度修改为7,改变蛇身符号等)

定义一个标识符为 DIRECTION 的宏,表示初始方向,值为77。77是方向右键的第二个键值。

键值就是当我们敲击键盘时程序接受到的一个用来区分各个键的数值,一般都是其ASCII码。

四个方向键比较特殊,他们一般传入两个键值,第一个都是224.第二个则各不相同。
这里定义了初始方向是右。
POSX和POSY是蛇初始位置的坐标。

const int speed[LevelSize] = { 1000,666,444,296,196,130,86,56,36,24,16,10,6,4,2 }; //速度级别

LevelSize 是上面预定义的,等于15,这里面定义了一个速度数组,里面的值是一组递减的整数,用来作为Sleep()函数的参数。

我们先看play()函数

void play() {
     int head = LENGTH - 1;//head和tail表示头尾所在的下标。
     int  tail = 0;
    while (1) {
        Sleep(Level());//休眠
        if (_kbhit())//如有输入
            todo(_getch());
        draw(body[head], BodySymbol);//变头为体
        draw(body[tail], NoSymbol);//去尾
        setBody(head, tail);
        draw(body[head], HeadSymbol);//画新头
    }
}

可以看到,play()函数就是由前一篇中的move()函数修改而来。
之所以将move改成play,就是因为我们要在这个游戏主循环里实现更多的功能(比如本期要讲的响应键盘事件),所以move一词以不足以涵盖这个作为游戏主循环的函数,改为play更合适。

        Sleep(Level());//休眠

Sleep()不必多言,他的参数是一个单位为毫秒的数,功能是让程序休眠,我们看到,这里将Level()的函数返回值作为了Sleep()的参数,我们往上翻,找到Level()

//设置级别,返回速度
int Level(char isQuick='r') {
    static int level = 0;//静态整形变量,被声明为静态的变量在函数结束后不会被释放
    if (isQuick=='+' && level < LevelSize) ++level;
   else if (isQuick=='-'&&level > 0) --level;
    return speed[level];      
}

我们注意到,Level()函数的参数isQuick有一个初始值 ‘r’,难道形参不都是没有值,依靠实参传值吗?
不然,形参可以拥有默认值,这样我们在调用函数时可以不给实参,函数会按照默认值进行运算,并返回。
再看:

    static int level = 0;//静态整形变量,被声明为静态的变量在函数结束后不会被释放

static关键字用来定义静态变量,他的作用域是所在函数,生命周期是全局。

什么意思呢?就是所谓的静态变量不会随着函数结束被释放(和全局变量一致,相反局部变量),但只可以在函数内部使用(和局部变量一致,相反全局变量)。

这里定义了level后,当函数执行完毕,level的值仍为在此函数内的最后一次修改(不能在其他地方修改和获得他的值),当函数下次被调用,其他局部变量都会重新定义,而此行定义语句不执行,level的值和上次执行后一样。

    if (isQuick=='+' && level < LevelSize) ++level;

if 是c++关键字,他的形式是:

if(条件语句一){
执行……
}
else if(条件语句二){
……}
else if(条件语句三){}
else {
……
}

当一成立,则执行一的代码,并且下一句是整个 if 结构的下一行。
不然,判断二,若成立则执行二的代码,
……
直到else,当以上所有条件都不成立,执行else语句。
这就是计算机编程中赫赫有名的分支结构。只要用好分支结构,循环结构,顺序结构,任何现实需求都可以被转换成计算机编程逻辑。

_getch()函数来自<conio.h>头文件,该函数获取一个键盘输入。(与前面见过的getch()一样,不过这是更规范的用法)
_kbhit()用来检测缓存区是否存在输入,若有,返回一个非0数(真)。

== 表示等于,用来判断符号左右两侧的操作数是否相同,若相等,则为真,不等,则为假。
比如:

int x=5;
if(x==5) cout<<"is 5"<<endl;
else cout<<"not 5"<<endl;

先判断括号内的值。x==5成立,所以 if 的值为真,所以执行输出“is 5”。

if 条件语句可以省略else从句,如果每个分支只有一个语句,可以不写大括号。

level<LevelSize 我们在while()和for()中已经见过类似大小判断的语句,成立为真,不成立为假,如:

int x=5;
int y=6;
if(x>y) cout<<"x is bigger!"<<endl;
else if(x<y) cout<<"y is bigger"<<endl;
else cout<<"x equals y"<<endl;

最后一个没见过的符号:&& ,表示且。
什么是且呢?且就是指要同时满足左右两边都为真,比如:

if(x>0&&y>0) cout<<"both of them are greater than 0"<<endl;
else cout<<"one of them is less than or equal to 0"endl;

所以,回到我们的代码:

if (isQuick=='+' && level < LevelSize) ++level;
//若输入的是加号且当前level小于LevelSize,就让level 加1。

下一句也不难理解:

   else if (isQuick=='-'&&level > 0) --level;
   //另外,如果输入的是减号且当前level大于0,就让level减1。

最后一句:

    return speed[level]; 
    //将改变后的level作为下标,返回speed数组的第level个(即第level级的速度。)

好了,现在我们知道了Level()函数的作用,当有参数时(这个参数可能是char型字符 ‘+’ 或 ‘-’ ),就将他内部维护的,具有全局生命周期的static int 型 level做改变(增加或者减少),当没有参数时,直接返回level。(这里默认值为 ‘r’ 没什么特殊作用,我只是想表示read,即读取level,你也可以改成任何非加减的符号。)

这样,我们的Level()函数就兼具SetLevel()和GetLevel()的作用。

好了,我们跟着Level()的返回语句返回play()。

        Sleep(Level());//休眠
        if (_kbhit())//如有输入
            todo(_getch());

第一句,相当于Sleep(speed[level]),即休眠speed数组第level项毫秒。
第二句,若有输入(_kbhit()函数表示当前缓存区是否有输入,若有,则返回1,若无,则返回0),就执行todo()函数,并将_getch()作为参数。

一般的,键盘中的输入都会进入窗口的输入缓存区(STD_INPUT),我们对输入的操作其实都是针对此,比如cin(),scanf(),getch()

这两行的执行顺序是:
1:执行_kbhit()。
2,若有输入(_kbhit()返回1),则执行分支语句(下一行),若无(返回0),跳过if 语句
3:先用_getch()获取键盘缓存区的第一个字符,
4:然后将此字符作为参数调用todo()函数。

我们看todo()

//按键响应
void todo(int key) {
    switch (key)//判断输入
    {
    case 43://加号
        Level('+');break;
    case 45://减号
        Level('-');break;
    case 32://空格
        pause(); break;
    case 224://四个方向键
      Direction(_getch());
    default:
        break;
    }
}

所谓的参数传递就是:int key=_getch();
所以key就是我们敲击的按键的键值。(一般是字符的ASCII)
我们又注意到一个新的关键字 switch
用法如下:
形式:

   switch (expression)
    {
    case /* constant-expression */:
        /* code */
        break;
    case /* constant-expression */:
        /* code */
        break;
    default:
        break;
    }

先计算expression的值,然后拿他和每个case后的值对比,若相等,就从此case的冒号后执行。

break;
//此语句表示跳出while循环,或if 循环,或switch语句

所以todo中的switch分支语句表示,按照key可能的值执行不同的操作:
当key等于43(加号的ASCII码),表示我们输入的是加号,就让level加1。
当key等于45(减号),用 '-'作为参数调用level。
当key的值是32(空格),调用pause(),让程序暂停。
当key的值是224(方向键的第一个键值),调用Direction()函数,并获取第二个键值(_getch())作为参数。

现在我们知道了,在Sleep()中无参调用level(),可以获得当前level的speed,level函数起的作用类似GetLevel(),而在todo()中传参调用level(),可以将当前level调高或调低。作用类似SetLevel().

看一看pause()

//暂停
void pause() {
    while (getch() != VK_SPACE);//只要输入的不是空格就一直循环
}

简单讲,就是按一下空格暂停(进入pause()),再按一下继续(pause()返回)。

VK_SPACE 是windows.h中的宏定义,就是32(空格的键值)

我们最后看一看Direction()

//返回当前方向
COORD Direction(int g = 0) {
    static int d = DIRECTION;//代表当前方向键值
    static COORD dir;
    if (d + g == 152||!g) return dir;
    d = g;//如果新方向与旧方向不相反且有新方向传入,设置新方向
    switch (d) {//通过按键改变方向
    case 72://上键
        dir.Y = -1;
        dir.X = 0;
        break;
    case 80://下键
        dir.Y = 1;
        dir.X = 0;
        break;
    case 75://左键
        dir.X = -1;
        dir.Y = 0;
        break;
    case 77://右键
        dir.X = 1;
        dir.Y = 0;

    }
    return dir;
}

先看函数头:

COORD Direction(int g = 0)

Direction()是一个带一个int 型参数(默认为0)的,返回值为COORD的函数。

前两句都是定义静态变量,d是键值,初始化为DIRECTION(宏定义为77,右),dir 是一个坐标数组,表示方向(具体实现后面讲)
第三句:

    if (d + g == 152||!g) return dir;

如果输入方向和当前方向相反(72(上)+80(下)=152,75(左)+77(右)=152),或者g为0,直接返回当前dir。
|| 表示或,左右两边只要有一个为真,表达式为真。

注意到 || 和 && 的区别了吗?

运算符是单目运算符(只有一个操作数),表示将操作数取非(真变假,假变真),即g为真时,!g为假,g为假时,!g为真。

因为在if 中只有返回语句(进入if 分支后直接返回,不再执行剩余代码),所以我们不用将下一句 d=g 包裹在else中,因为之后的所有句子都只有在 if 不成立时才执行。

d=g将g(输入的方向键)赋给d(当前方向键),然后进行switch分支语句判断。
我们发现,我们分别在上下左右四种情况下赋dir为:
{0,1}、{0,-1}、{1,0}、{-1,0}
具体原因后面讲。

有人可能注意到,当参数为空(默认0)或方向相反时,我们返回了dir,可dir或许还没有初始化。其实不必担心这个问题,因为ready()做了修改,在其中调用了Direction(),这也是Direction()的第一次调用,这次后dir就有了初值。

现在我们已经看完了todo中的调用的三个函数:
Level():用来设置方向;
pause():用来暂停;
Direction():用来设置方向;

可见,todo就是名副其实的键盘事件处理函数,他处理了所有我们设想到的所有键盘输入(暂停和继续,加速和减速,方向)。

我们已经知道了level的用途(Sleep()中),pause()的用途,那么Direction函数设置的dir,又是如何影响蛇的前进方向的呢?

让我们回到play()

void play() {
     int head = LENGTH - 1;//head和tail表示头尾所在的下标。
     int  tail = 0;
    while (1) {
        Sleep(Level());//休眠
        if (_kbhit())//如有输入
            todo(_getch());
        draw(body[head], BodySymbol);//变头为体
        draw(body[tail], NoSymbol);//去尾
        setBody(head, tail);
        draw(body[head], HeadSymbol);//画新头
    }
}

在主循环中,剩下的四句几乎和之前的move()函数一模一样,不同的是turn()变成了setBody()。
我们记得,turn()的作用是:舍弃保存的蛇尾坐标,并在那个位置保存下一个头部的坐标,然后重定向head和tail(通过引用 h 和 t )。
为了节省开支,我们没有每移动一次就重新给数组中所有元素赋值(新的位置),而是通过旧标新位,记录头尾的方法每次只改变蛇身数组的一个值。

记得之前的turn()中我们是直接将蛇头坐标X加一(向右)Y不变的值赋给蛇尾处来实现移动(向右),那么添加了方向控制后该如何移动也就呼之欲出了:
按照 dir 的值获得新头坐标。
OK!看setBody()

//设置新的蛇身数组
void setBody(int& h, int& t) {
    body[t].X = body[h].X + Direction().X;//将新方向存入body[t]
    body[t].Y = body[h].Y + Direction().Y;
    h = t;//将t的值赋给h,此时body[h]表示的是新头部
    (t == LENGTH - 1) ? t = 0 : t++;//当t等于LENGTH时令t为0,否则t++
}

对照之前的turn()函数,想想Direction()是干嘛的?返回什么?两句赋值后改变了什么?

还记得前面说的。为什么要将 dir 的值设为{0,1}、{0,-1}、{1,0}、{-1,0} 呢?
想来原因大家已经知道了。

好了,源程序我们看完了(ready处也有些改变,可以自己看看),最后总结一下游戏主循环:

    while (1) {
        Sleep(Level());//休眠
        if (_kbhit())//如有输入
            todo(_getch());
        draw(body[head], BodySymbol);//变头为体
        draw(body[tail], NoSymbol);//去尾
        setBody(head, tail);
        draw(body[head], HeadSymbol);//画新头
    }

1:按照当前级别休眠。
2:处理输入
(1)暂停和继续
(2)调整级别
(3)调整方向
3:按照新方向前进

本篇结束,下一篇我们加入食物,分数,围墙,来一场真正的贪吃蛇大冒险!


网站公告

今日签到

点亮在社区的每一天
去签到