点击链接回顾前几篇:
(一)标准输出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()中的这段代码都实现同一语义功能——根据键值返回下一步的坐标变化量。
第九篇结束,下一篇,我们加入“食物”,来让我们的贪吃蛇在进食中茁壮成长!