C语言实现贪吃蛇(二)----局部刷新

前言:

在上一篇博客《C语言实现贪吃蛇(一)—-数组实现》,我们使用数组来存储坐标,并且不断的通过全屏刷新的方式来实现蛇移动的动态效果。但是全屏刷新使得该游戏整个过程中的闪烁现象,究其原因,无非就是在于频繁的清空与打印。

但是想想看,整个游戏过程中并不需要重复打印整个界面,比如围墙,比如未被吃掉的食物。要实现蛇的移动,我们只要打印出新的蛇头,清除原来的蛇尾就好了。食物只有在被吃掉时才需要重新打印,边界更是只用打印一次。好了,既然我们看到了可提升的地方,就开始动手优化吧。

PS:以下内容都是基于《C语言实现贪吃蛇(一)—-数组实现》,大家可能得去看看。

准备工作:

为了避免全屏刷新,我们应直接定位到需要打印的地方进行打印操作,而不是像之前一样通过循环打印。

因此我们将需要一个可以自由移动光标的函数,这样我们才能做到在需要的地方打印。

TC上有一个叫gotoxy()的很方便的函数,该函数,顾名思义,就是将光标移动到(x,y)位置。然而现在估计很少有人还在使用TC。那么我们就着手自己编写一个gotoxy()函数。

void gotoxy(unsigned char x,unsigned char y){
    //COORD是Windows API中定义的一种结构,表示一个字符在控制台屏幕上的坐标
    COORD cor;

    //句柄 
    HANDLE hout;

    //设定我们要定位到的坐标 
    cor.X = x;
    cor.Y = y;

    //GetStdHandle函数获取一个指向特定标准设备的句柄,包括标准输入,标准输出和标准错误。
    //STD_OUTPUT_HANDLE正是代表标准输出(也就是显示屏)的宏 
    hout = GetStdHandle(STD_OUTPUT_HANDLE);

    //SetConsoleCursorPosition函数用于设置控制台光标的位置
    SetConsoleCursorPosition(hout, cor);
}

如果是没有接触过windows编程的同学看到这段代码可能会有些懵逼,不过没关系。整段代码的逻辑其实十分简单。

COORD是windows API定义的结构,其声明如下:

typedef struct _COORD {
  SHORT X;
  SHORT Y;
} COORD, *PCOORD;

正如其名字coordinate(坐标)一样,这是一个存储二维坐标的结构体。

HANDLE(句柄)在windows编程中是一个十分重要的概念。在window编程中,对于一个Object(对象)我们只能通过Handle来访问它。觉得不好理解的同学把句柄当作指针来看待就好了。

GetStdHandle函数获取一个指向特定标准设备的句柄,包括标准输入,标准输出和标准错误。STD_OUTPUT_HANDLE正是代表标准输出(也就是显示屏)的宏。

SetConsoleCursorPosition函数用于设置控制台光标的位置。

如果还不懂的同学还是 baidu 一下吧,毕竟我也是差不多的。。。。。

有了gotoxy函数,适当地修改上一篇的代码就可以解决闪屏的问题。

程序实现:

现在我们先来看看程序的最主要部分:主函数

int main(void){
    int dir = UP;   //初始方向默认向上,UP是我们定义的宏
    init_game();    //初始化游戏 
    while(1){
        dir = get_dir(dir);     //获取方向(我们摁下的方向)
        move_snake(dir);        //移动蛇身
        if(!isalive()){         //判断蛇的生命状态
            break;
        }
    }
    //清除屏幕 
    system("cls");
    printf("Game Over!\n");

    return 0;
}

与上一篇博客相比,我们的主函数中去掉 print_game 函数,加入 init_game 函数。

我们来看看程序修改后的一些变量和函数声明(基本上跟之前的差不多的)

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>

//72,80,75,77是方向键对应的键值
#define UP 72
#define DOWN 80
#define LEFT 75
#define RIGHT 77
#define SNAKE 1
#define FOOD 2
#define BAR 3

//初始化地图
char map[17][17] = {0};
//初始化蛇头坐标
unsigned char snake[50] = {77};
//初始化食物坐标
unsigned char food = 68;
//蛇长
char len = 1;

//存储坐标数字与x、y的转换函数
void tran(unsigned char num,unsigned char * x,unsigned char * y);
//初始化游戏
void init_game(void); 
//获取方向函数(注意当蛇身长度超过一节时不能回头)
int get_dir(int old_dir);
//移动蛇身函数(游戏大部分内容在其中)
void move_snake(int dir);
//其中有个生产食物的函数,generate_food(),它利用随机数生成函数生成食物坐标
unsigned char generate_food(void); 
//判断蛇死活的函数(判断了蛇是否撞到边界或者自食)
int isalive(void);
//将光标移动到命令行的 (x,y)位置
void gotoxy(unsigned char x,unsigned char y); 


int main(void){
    int dir = UP;   //初始方向默认向上,UP是我们定义的宏
    init_game();    //初始化游戏 
    while(1){
        dir = get_dir(dir);     //获取方向(我们摁下的方向)
        move_snake(dir);        //移动蛇身
        if(!isalive()){         //判断蛇的生命状态
            break;
        }
    }
    //清除屏幕 
    system("cls");
    printf("Game Over!\n");
    return 0;
}

与前一篇博客对比,这里的一下变化:

  1. 修改了 main() 主函数
  2. 用 init_game() 替换 print_game()
  3. 修改了 move_snake() 函数
  4. 增加了 gotoxy() 光标坐标定位函数

main() 函数和 gotoxy() 函数我们已经介绍过了,现在我们看看 init_game() 函数:

//初始化游戏
void init_game(void);

//初始化游戏 (打印初始状态) 
void init_game(void)
{
    int i, j;
    unsigned char x, y, fx, fy;
    tran(snake[0], &x, &y);
    tran(food, &fx, &fy);
    for (j = 0; j<17; j++) {
        for (i = 0; i<17; i++) {
            //打印围墙 
            if (i == 0 || i == 16 || j == 0 || j == 16){
                putchar('#');
            }
            //打印蛇头 
            else if (i == x&&j == y){
                putchar('*');
            }
            //打印食物 
            else if (i == fy&&j == fx){ 
                putchar('$');
            }
            //空白地方 
            else{
                putchar(' ');
            }
        }
        putchar('\n');
    }
}

从 init_game() 函数中和在 main() 调用可以看出,该函数的作用仅仅是初始化作用(将一成不变的围墙画好,打印食物和蛇头的初始位置)。后面就没它什么事了。

再来看看 move_snake() 函数:

//移动蛇身函数(游戏大部分内容在其中)
void move_snake(int dir);

void move_snake(int dir){
    int last = snake[0],current;    //last与current用于之后蛇坐标的更新
    int i;
    int grow=0;     //判断是否要长身体
    unsigned char x, y, fx, fy;     //蛇坐标与食物坐标
    tran(food, &fx, &fy);   //食物坐标 
    tran(snake[0], &x, &y); //蛇头坐标 
    switch (dir){           //更新蛇头坐标(坐标原点是左上角) 
        case UP:
            y--;
            break;
        case DOWN:
            y++;
            break;
        case LEFT:
            x--;
            break;
        case RIGHT:
            x++;
            break;
    }
    //按位抑或(妙!) 
    snake[0] = ((x ^ 0) << 4) ^ y;      //将x,y换回一个数

    //蛇吃到了食物 
    if (snake[0] == food) {
        grow = 1;
        food = generate_food();     //产生新食物
    }

    for (i = 0; i<len; i++) {       //蛇移动的关键,通过将蛇头原来的坐标赋给第二节,原来的第二节赋给第三节,依次下去,完成蛇坐标的更新
        if (i == 0)     //如果只有头,跳过,因为前面已更新蛇头坐标
            continue;
        current = snake[i];     //将当前操作的蛇节坐标存储到current里
        snake[i] = last;        //完成当前操作蛇节坐标的更新
        last = current;     //last记录的是上一次操作蛇节的坐标,这次操作已经结束,故把current赋给last
    }

    gotoxy(x, y);       //将光标移动到指定位置 
    putchar('*');       //打印新的蛇头

    if (grow) {         //如果要长节的话就不去除旧的蛇尾
        snake[len] = last;
        len++;
        tran(food, &fx, &fy);   // 打印食物(上面已经产生新的食物坐标)
        gotoxy(fx,fy);
        putchar('$');
    }else {
        //这是为了避免当你把蛇绕成一个圈的时候(蛇头紧跟蛇尾,没咬到),清除蛇尾顺便也把蛇头清除掉了 
        if(snake[0] != last){
            tran(last, &x, &y);
            gotoxy(x, y);
            putchar(' ');   //去除旧的蛇尾
        }
    }
    //避免光标一直跟着蛇尾(或食物) 
    gotoxy(0,17);

    Sleep(500);
}

从该函数中可以看出,我们利用 gotoxy() 函数,实现了局部刷新的功能,从而避免了游戏的闪烁现象。

本博客参考自《C语言实现贪吃蛇之局部刷新篇 》,在后续的博客中,我将跟随着原作者的脚步继续折腾这个游戏,感兴趣的同学可以看看后续的文章。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值