0基础学习C++做贪吃蛇, 边玩儿边学习!(九)静态变量和形参默认值——停走缓急的蛇

点击链接回顾前几篇:
(一)标准输出cout——一条安静的蛇
(二)代码详解和Sleep()——蛇之闪现
(三)SetConsoleCursorPosition光标移动效果——一条前进的蛇
(四)预定义和函数调用——妄图得分的蛇
(五)for循环和作用域——可长可短的蛇
(六)结构体和while循环——各具特色的蛇
(七)数组和移动逻辑——徐徐移动的蛇
(八)switch和if分支语句——自由移动的蛇

这一次我们添加加速、减速,暂停的键盘响应。
首先看一下程序开始部分的修改:

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

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

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

数组元素的单位是毫秒(表示休眠时长),这个数字越小,蛇的速度越快。

我们将move()修改为:

//游戏执行函数
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);//去尾
        turn(head, tail);
        draw(body[head], HeadSymbol);//画新头
    }
}

之所以更改move()为play(),是因为现在我们的游戏主循环不仅仅要实现移动功能,还要实现加速、减速、暂停等功能,这样,自然是play这个名字更好一点。
可以看到,相比之前的move()函数,play()函数有了几点变化:

1,Sleep()的参数不再是具体的数值,而是Level函数的返回值

可以想到,Level()函数一定是返回一个整数,单位毫秒(不同级别,不同速度)

if(_getch())分支语句变成一次函数调用。

        if (_kbhit())//若有输入
            todo(_getch());//执行键盘输入处理

可想而知,只要键盘上某个按键被按下(_kbhit()为真),就会跳转到todo函数,参数是当前键盘输入缓存区的第一个键值,因此,这个函数就是名副其实的 “键盘事件处理函数”
我们来看看:

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

32是空格的键值,这里用pause()实现。
键值是224时的处理就是上一篇move()中的校验语句。

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

43和45是加减号的键值,这里调用了Level()函数来实现级别高低(速度快慢),刚才在play()中也调用了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;

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,你也可以改成任何非加减的符号。)

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

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

看一看pause()

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

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

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

下面放出全部代码:

#include <iostream>
#include<conio.h>
#include <windows.h>
using namespace std;
//预定义一些游戏参数
#define LENGTH 5  //初始长度
#define LevelSize 15  //级别个数
#define NoSymbol ' '//输出符号
#define BodySymbol 'o'
#define HeadSymbol 'O'
#define POSX 10 //初始位置
#define POSY 10
int Arrow = 77;  //初始方向
//必要的全局变量
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 moveby() {
    switch (Arrow) {//通过按键改变方向
    case 72://上键
        return {0, -1};
    case 80://下键
        return {0, 1};
    case 75://左键
        return {-1, 0};
    case 77://右键
        return {1, 0};
    default:
        return {0, 0};
    }
}
//预备
void ready() {
    for (int i = 0;i < LENGTH;i++) {
        COORD dir = moveby();
        body[i].X = POSX + i * (dir.X);//body数组内元素的横坐标递增
        body[i].Y = POSY+i*(dir.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://四个方向键
        int newArrow = _getch();
        if (Arrow + newArrow != 152)
            Arrow = newArrow;
        break;
    }
}
//设置新的蛇身数组
void turn(int& h, int& t) {
    //定义一个COORD型变量dir初值为{0,0}
    COORD dir = moveby();
    //用新的蛇头坐标覆盖原来的蛇尾坐标
    body[t].X = body[h].X + dir.X;
    body[t].Y = body[h].Y + dir.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) {
       // while (_kbhit()) _getch();//清空缓存区
        Sleep(Level());//休眠
        if (_kbhit())//若有输入
            todo(_getch());//执行键盘输入处理
        draw(body[head], BodySymbol);//变头为体
        draw(body[tail], NoSymbol);//去尾
        turn(head, tail);
        draw(body[head], HeadSymbol);//画新头
    }
}
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;
}

其中turn()中按照Arrow的值来确定方向坐标变化的语句被拆分成了一个新函数moveby(),ready()函数也有了些变化,这些变化是由什么引起的呢?
——因为我们将蛇的长度和初始位置,初始方向宏定义了(为了便于修改),这样,在ready()就需要确定蛇身的朝向,直接的办法是将turn()中按照Arrow修改dir的代码复制一份到ready()中,但这不符合代码编写的原则:

DRY 原则(Don’t Repeat Yourself)

不要重复自己,是指不要出现两段代码——他们实现了同一语义功能。

可以看到,不论是在ready()中还是turn()中,已经被拆分到moveby()中的这段代码都实现同一语义功能——根据键值返回下一步的坐标变化量。
第九篇结束,下一篇,我们加入“食物”,来让我们的贪吃蛇在进食中茁壮成长!

  • 14
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值