点击链接回顾前几篇:
(一)标准输出cout——一条安静的蛇
(二)代码详解和Sleep()——蛇之闪现
(三)SetConsoleCursorPosition光标移动效果——一条前进的蛇
(四)预定义和函数调用——妄图得分的蛇
(五)for循环和作用域——可长可短的蛇
(六)结构体和while循环——各具特色的蛇
这是上期给出的代码。
#include <iostream>
#include<conio.h>
#include <windows.h>
//初始长度
#define LENGTH 5
//输出符号
#define NoSymbol ' '
#define BodySymbol 'o'
#define HeadSymbol 'O'
using namespace std;
//窗口句柄
HANDLE handle;
//蛇体坐标数组
COORD body[LENGTH];
//显示
void draw(COORD pos, char symbol) {
SetConsoleCursorPosition(handle, pos);//设置handle指向窗口光标位置为pos
cout << symbol;
}
//预备
void ready() {
for (int i = 0;i < LENGTH;i++) {
body[i].X = i;//body数组内元素的横坐标从0到LENGTH-1递增
body[i].Y = 10;//纵坐标不变
draw(body[i], BodySymbol);//在数组每个元素代表的坐标处打印蛇身
}
draw(body[LENGTH - 1], HeadSymbol); // 将数组最后一个元素代表的坐标处重新绘制蛇头
}
//倾向
void turn(int& h, int& t) {//h表示需要存储蛇头坐标的元素的下标,t表示蛇尾的
body[t].X = body[h].X + 1;//将头部前面一格的位置保存在body[t]中
h = t;//将t的值赋给h,此时body[h]表示的是新头部
(t == LENGTH - 1) ? t = 0 : t++;//当t等于LENGTH时令t为0,否则t++
}
//移动
void move() {
int head = LENGTH - 1;//head和tail表示头尾所在的下标。
int tail = 0;
while (1) {//while(1)恒成立,此为无限循环
Sleep(200);//休眠200毫秒
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();
move();
return 0;
}
我们继续看这句:
COORD body[LENGTH];
我们知道,LENGTH是宏,表示的是蛇的长度,宏定义为5,COORD是Windows API中的坐标结构体类型,表示屏幕坐标。可是变量标识符后面怎么还有一个[]呢?
type name[num];
定义一个tyoe型的数组name,大小为num。
所以这句语句意为:定义一个COORD型的数组body,大小为LENGTH。
什么是数组呢?
数组是一个有序的同类元素序列。
当你定义一个数组,程序就会在内存开辟一块足够存放num个type型数据的空间,你可以在数组中存放num个此类型数据。
数组的性质:
有序: 数组是有序的,每个元素都拥有位置
同类: 数组有类型,其类型就是其中元素的类型,他们都是同类型
那么,如何表示数组中的元素呢?
body[0]
表示body数组中的第0个元素。0叫数组的下标
注:数组定义时[]中的数字表示数组的长度,而引用数组中的元素时表示元素的下标,即他在数组中的位置,下标从0开始,到LENGTH-1结束(共length个),所以不能访问body[LENGTH],因为body中最后一个元素是body[LENGTH-1]
我们接着看代码,很快会看到数组的实际应用。
//显示
void draw(COORD pos, char symbol) {
SetConsoleCursorPosition(handle, pos);//设置handle指向窗口光标位置为pos
cout << symbol;
}
这是一个void类型的函数(没有返回值),接受一个COORD型参数和一个char型参数,将光标位置设置为坐标pos,并在此打印一个symbol图案。
可是看到,此函数的功能就是在给定位置输出给定字符,简洁明了。
我们在编程时尽量让每个函数功能单一,实现简单,这样便于我们寻找bug和修改。
//预备
void ready() {
for (int i = 0;i < LENGTH;i++) {
body[i].X = i;//body数组内元素的横坐标从0到LENGTH-1递增
body[i].Y = 10;//纵坐标不变
draw(body[i], BodySymbol);//在数组每个元素代表的坐标处打印蛇身
}
draw(body[LENGTH - 1], HeadSymbol); // 将数组最后一个元素代表的坐标处重新绘制蛇头
}
下面这两句是赋值语句。
body[i].X = i;
body[i].Y = 10;
body[i]表示body数组中的第 i 个元素。 . 是struct对象用来访问结构体成员变量的符号,所以这句话意为:
把 i 的值赋给body数组第 i 个元素的X成员,即将蛇的第 i 个身体坐标的X(横坐标)变成i。
下一句语句意为:把10赋给body数组第 i 个元素的Y成员(表示纵坐标)
然后,在每个元素代表的坐标处打印了一个BodySymbol字符。
经过此for循环,我们将body中的五个COORD型坐标的X从0到4依次赋值(i从0变到4),将他们的Y通通变成了10.
最后一句则在body的最后一个元素处重新打印了HeadSymbol字符。
我们知道窗口坐标中X从左向右递增,Y从上到下递增,因此我们可以想象:执行到这里后,窗口在第十行0处开始,向右5单位长的地方出现了一条Bodysymbol组成的直线。
新建一个Cpp文件,将draw()函数和ready()函数复制过去,在主函数里调用ready()函数,如图:
如图:这是一条停泊在第十行的蛇,他的每节身体都是独立输出的,且我们知道他身体的坐标,这样方便我们以后计算他是否接触食物或墙壁,又或者是否撞到了自己
继续:
//移动
void move() {
int head = LENGTH - 1;//head和tail表示头尾所在的下标。
int tail = 0;
while (1) {//while(1)恒成立,此为无限循环
Sleep(200);//休眠200毫秒
draw(body[head], BodySymbol);//变头为体
draw(body[tail], NoSymbol);//去尾
turn(head, tail);//新的头尾
draw(body[head], HeadSymbol);//画新头
}
}
我们先看move(),看完move()才能更好地理解turn()。
首先定义了两个整形变量head和tail,并将此时(ready()之后)的头、尾坐标赋给他俩。
这样,我们就用head和tail表示了头、尾坐标在body数组中的位置。
可是头尾坐标不是固定的吗?为什么我们要另外标记呢?
继续看。
while(1),括号内是1,恒不等于0,因此while恒成立,这是一个死循环。
Sleep()讲过了,忘记的可以看前几期。
第一句draw(),在head处打印身体字符。要知道在ready()中我们已经在此坐标处打印了蛇头图案,当这一句执行后,我们的蛇变成了一个由五个身体图案组成的无头蛇。
第二句draw(),在尾部打印NoSymbol,NoSymbol是什么?翻到程序开始处的预定义:
#define NoSymbol ' '
NoSymbol就是空格,在尾部输出一个空格,也就是清除原有的身体图案。
好了,这两句draw()后我们的蛇不但没了头,还没了尾巴,只剩四格长。
turn(head, tail);//新的头尾
在最后一句draw()之前,调用了turn()函数用来转向,我们看一下turn()
//倾向
void turn(int& h, int& t) {//h表示需要存储蛇头坐标的元素的下标,t表示蛇尾的
body[t].X = body[h].X + 1;//将头部前面一格的位置保存在body[t]中
h = t;//将t的值赋给h,此时body[h]表示的是新头部
(t == LENGTH - 1) ? t = 0 : t++;//当t等于LENGTH时令t为0,否则t++
}
首先,turn()接受两个int&型参数。
int& h;
表示定义了一个int型的引用,引用是一个特殊的标识符,他没有自己独立的值,他可以指向一个已存在的同类型变量,如:
int& h=head;
这句语句定义了一个head的引用h,当我们改变h时,head的值也变了,因为h和head指向同一块内存,可以理解为h和head是一个变量的两个名字。即:当我们改变h的值时,改变的就是head的值。
我们在前面学习了,参数传递的实质是把实参的值赋给形参,形参的变化对实参不影响。可有些时候我们希望在被调者中改变调用者中变量的值,就可以将被调者的形参声明为引用,以达目的。
所以,在turn()中,形参h、t分别是move()中head和tail的引用,我们只需要操作h和t,就可以改变move()中的head和tail。
body[t].X = body[h].X + 1;//将头部前面一格的位置保存在body[t]中
为什么要把前进方向赋给body[t]呢?
h = t;//将t的值赋给h,此时body[h]表示的是新头部
原来,紧随其后我们将t赋给了h,也就是说,现在body[h]就是上一句里的body[t],新头部。
现在body[h]里保存了新的头部坐标,可body[t]也指向这里,我们还需要改变 t 的值。
(t == LENGTH - 1) ? t = 0 : t++;//当t等于LENGTH时令t为0,否则t++
表达式一?表达式二 : 表达式三,这个语句意为:先求表达式一的值,如果为真,则执行表达式二,并返回表达式二的结果;如果表达式一的值为假,则执行表达式三,并返回表达式三的结果。
也就是说:
(t == LENGTH - 1) ? t = 0 : t++;
等价于:
if(t==LENGTH-1) t=0;
else t++;
此条件表达式还有自身的值,就是说,当一为真,条件表达式的值为二,当一为假,条件表达式的值为三。
t=(t==LENGTH-1)?0:t+1;
上面这个语句意为:当 t 与LENGTH-1相等时,把0赋给t;不然,就把 t+1赋给 t。
我们想象body数组是一个头尾相连的5单位长的环,5各单位长的蛇在这个环上移动,那么蛇头和蛇尾相连,蛇头位置是移动前的蛇尾位置,蛇尾位置则是之前蛇尾位置前面的那个位置(t++),当蛇尾在LENGTH-1处(数组最后一位)时,下一次的位置就是0(因为我们设想数组成环。)
好吧,那我们为什么要这样做呢?
因为贪吃蛇移动一次,整个蛇的坐标有三个变化:
1:原来蛇头的位置成了蛇身。
2,原来蛇尾的位置成了空白
3,蛇头前进方向出现新的蛇头。
我们发现只需要舍弃之前的尾部坐标,再添加新的头部坐标,我们就可以仍用长度为5的body数组来表示蛇移动后的全部坐标。为达目的,我们只能将新头的坐标放在旧尾处,并且重新指定头尾(新头在旧尾,新尾往前移)。
好了,turn()结束,我们在turn()中将新的头部坐标放到了原来尾部的地方(被覆盖),并且让h(即head)指向他,更新了尾部的位置(第一次时的情况:从body[0]到body[1] )
现在turn()完毕,回到move()执行最后一句:
draw(body[head], HeadSymbol);//画新头
我们在新的head(原来的head的前方)处画了新头,完成了一格移动。
再看一次move()函数,我们发现整个移动逻辑都在一个死循环中:
while (1) {//while(1)恒成立,此为无限循环
Sleep(200);//休眠200毫秒
draw(body[head], BodySymbol);//变头为体
draw(body[tail], NoSymbol);//去尾
turn(head, tail);//新的头尾
draw(body[head], HeadSymbol);//画新头
}
此循环每次进入后Sleep(200),即休眠200毫秒,然后执行移动,此过程速度极快,可以忽略不计,因此我们可以认为:
贪吃蛇每200毫秒移动一格。
好了,就这样,我们用两期的时间完成且理解了贪吃蛇的移动逻辑:
每隔一定的时间(Sleep(time)),绘制(draw())新的头、尾、脖子(旧头处),改变头尾坐标(turn())。
最后再看一下运行结果:
这是一条徐徐前进的蛇,我们已经 “看透” 了它!