0基础学习C++做贪吃蛇, 边玩儿边学习!(七)数组和移动逻辑——徐徐移动的蛇

点击链接回顾前几篇:
(一)标准输出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())。

最后再看一下运行结果:
请添加图片描述
这是一条徐徐前进的蛇,我们已经 “看透” 了它!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值