C语言实现贪吃蛇(一)----数组实现

本人是一个C语言新手,在网上看到有的人用C实现了贪吃蛇的游戏,想着为了巩固一下自己的C语言,然后就学着写了一个,当然基本上是Copy别人的代码,然后加上自己的理解。在这里做一下记录,也希望能够帮助那些跟我差不多的同学。

一、贪吃蛇实现原理:

  1. 贪吃蛇游戏在理论上是可以无限的进行下去的(除了撞墙和咬到自己),那么游戏主体就一定是个循环。
  2. 蛇是如何动起来的?在这里就是通过不断改变蛇的坐标,然后根据蛇的坐标不断刷新屏幕在视觉上形成蛇的移动效果。
  3. 食物出现在随机位置(当然不能出现在障碍物和蛇身上)。
  4. 蛇能吃到食物其实就是蛇头的坐标与食物的坐标重合时。
  5. 当蛇咬到自己或者撞到墙的时候游戏结束(坐标判断)

还有一点要注意的是:命令行的坐标轴长这样:

这里写图片描述

因为后面在处理坐标的时候可能会乱(我在看的时候就思维混乱过)。

二、代码实现:

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

int main(void){
    int dir = UP;   //初始方向默认向上,UP是我们定义的宏
    //按理论该游戏是可以无限继续下去的,因此是个循环
    while(1){
        print_game();   //打印游戏
        dir = get_dir(dir);     //获取方向(我们摁下的方向键)
        move_snake(dir);    //移动蛇身
        if(!isalive()){     //判断蛇的生命状态
            break;
        }
    }

    printf("Game Over!\n");
    return 0;
}

从主函数中我们看到,游戏的整体思路是比较简单的,仅仅只有那么几个函数。有了框架我们再来具体实现。

首先考虑使用的数据结构,正如博客名所示,这次我只用数组,不使用链表和结构体。在这里我们用一个二维数组 char map[17][17] 来表示地图,用一维数组 unsigned char snake[50] 来表示蛇的坐标。这里之所以用char是因为本着能省则省的原则,我们这是一个17*17大小的小游戏,char足够了。

上面的这些基本上是Copy原作者的话,但是还是有必要解释一下。用一维数组 unsigned char snake[50] 来表示蛇的坐标,这里假设了我们玩的蛇能达到的最大长度为 50(能玩到这个长度已经很牛逼了,满足吧!),也就是说我们用一个 unsigned char 类型的数字表示蛇的坐标。怎么表示?还是引用原作者的话:

只用数组做贪吃蛇的话,在蛇坐标与食物坐标的存储上会比较麻烦,毕竟我们的游戏坐标是二维的,这里我采用位运算的方法,一个unsigned char类型的变量八位,前四位记录x坐标,后四位记录y坐标,四位的话,最大值也就是15,所以我设置的是17*17的大小,(15+边界)*(15+边界)。要是觉得地图太小的话,用int(4位)或者long(4位或8位)也是可以的。

好了,屁话说了这么多,我们先来看看程序开始应该定义的一些变量和函数声明:

#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       //墙的坐标标识

//初始化地图 17*17
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 print_game(void);
//获取方向函数(注意当蛇身长度超过一节时不能回头)
int get_dir(int old_dir);
//移动蛇身函数(游戏大部分内容在其中)
void move_snake(int dir);
//生产食物的函数
unsigned char generate_food(void); 
//判断蛇死活的函数(判断了蛇是否撞到边界或者自食)
int isalive(void);

int main(void){
    int dir = UP;   //初始方向默认向上,UP是我们定义的宏
    //按道理该游戏是可以无限继续下去的,因此是个循环
    while(1){
        print_game();   //打印游戏
        dir = get_dir(dir);     //获取方向(我们摁下的方向)
        move_snake(dir);    //移动蛇身
        if(!isalive()){     //判断蛇的生命状态
            break;
        }
    }

    printf("Game Over!\n");
    return 0;
}

//存储坐标数字与x、y的转换函数
void tran(unsigned char num,unsigned char * x,unsigned char * y);

该函数实现了将 num 的前四位代表的值赋给 x,后四位赋给 y,得到对应的坐标(x,y)

void tran(unsigned char num,unsigned char * x,unsigned char * y){
    *x = num >> 4;
    *y = (unsigned char)(num << 4) >> 4;    //注意这里要做个强制类型转换
}

碰到被调用函数需要返回两个以上参数的时候,应该像上述函数一样,将要返回的值写成指针参数。

//打印游戏
void print_game(void);

void print_game(void){
    int i,j;
    //根据地图上每点的情况绘制游戏( i 表示 x 轴,j 表示 y 轴),按行打印
    for(j = 0;j < 17;j ++){
        for(i = 0;i < 17;i ++){
            //空白地方
            if(map[i][j] == 0){
                putchar(' ');
            }
            //蛇身
            else if(map[i][j] == SNAKE){
                putchar('*');
            }
            //围栏
            else if(map[i][j] == BAR){
                putchar('#');
            }
            //食物
            else if(map[i][j] == FOOD){
                putchar('$');
            }
        }
        putchar('\n');
    }
    Sleep(500);     //休眠函数 将进程挂起500ms,包含在window.h(在linux下用 sleep(),#include <unistd.h>) 
    system("cls");  //清屏函数 配合下一次 print_game() 起到刷新作用,包含在stdlib.h中
}

//获取方向函数(注意当蛇身长度超过一节时不能回头)
int get_dir(int old_dir);

这里有一个注意的点:当蛇身长度超过一节的时候,我们就不能往回走了。

在这里我们是用 kbhit() 与 getch() 组合实现键盘响应

//返回新的方向
int get_dir(int old_dir){

    int new_dir = old_dir;

    //用kbhit()与getch()组合实现键盘响应
    //kbhit() 检查当前是否有键盘输入,若有则返回一个非0值,否则返回0
    //getch() 用ch=_getch();会等待你按下任意键之后,把该键字符所对应的ASCII码赋给ch,再执行下面的语句。 
    if(_kbhit()){
        _getch();
        new_dir = _getch();     //getch()函数要使用两次,原因是因为第一次返回的值指示该键扩展的字符,第二次调用才返回实际的键代码

        //如果蛇身长度大于1,则不能回头,如果摁回头方向,则按原来方向走 
        //abs(new_dir - old_dir) == 2 表示 |LEFT-RIGHT|
        //abs(new_dir - old_dir) == 8 表示 |UP-DOWN|
        if(len > 1 && (abs(new_dir - old_dir) == 2 || abs(new_dir - old_dir) == 8)){
            new_dir = old_dir;
        }
    }
    return new_dir;
}

原作者提示:getch()和kbhit()前面的下划线是因为在vs环境下一些函数需要使用加下划线的版本,更安全。

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

void move_snake(int dir){
    int last = snake[0],current;    //last与current用于之后蛇坐标的更新
    int i,j;
    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

    //如果蛇边长了
    if (grow) {
        snake[len] = last;
        len++;
    }

    for (j = 0; j < 17; j ++){      //将边界与食物加到地图里去(i,j 对应 x轴和y轴)
        for (i = 0; i < 17; i ++){ 
            if (i == 0 || i == 16 || j == 0 || j == 16){ 
                map[i][j] = BAR;
            } 
            else if (i == fx&&j == fy){ 
                map[i][j] = FOOD;
            } 
            else{ 
                map[i][j] = 0;
            } 
        } 

    for (i = 0; i < len; i++) {     //将蛇加到地图里去
        tran(snake[i], &x, &y);
        if (snake[i] > 0){
            map[x][y] = SNAKE;
        }
    }
}

//生产食物的函数
unsigned char generate_food(void);

随机数生成函数,一般由srand()和rand()函数组成,前者以当前时间为参数提供种子供rand()函数生成更为随机的数。这里用do while语句做了一个筛选,最后产生在边界内且不再蛇身上的食物。

unsigned char generate_food(void)
{
    unsigned char food_,fx,fy;
    int in_snake=0,i;
    //以当前时间为参数提供种子供rand()函数生成更为随机的数
    srand((unsigned int)time(NULL));
    //循环产生在边界内且不在蛇身上的食物 
    do {
        food_ = rand() % 255;
        tran(food_, &fx, &fy);
        for (i = 0; i < len; i++){ 
            if (food_ == snake[i]){ 
            //在不在蛇身上
                in_snake = 1;
            }
        } 
    } while (fx == 0 || fx == 16 || fy == 0 || fy == 16 || in_snake);
    return food_;
}

//判断蛇死活的函数(判断了蛇是否撞到边界或者自食)
int isalive(void);

int isalive(void)
{    
    int self_eat = 0, i;
    unsigned char x, y;
    tran(snake[0], &x, &y);
    for (i = 1; i < len; i++){
        if (snake[0] == snake[i]){
            self_eat = 1;
        }
    }
    //蛇头撞边界或者吃到自己 ,则死掉 
    return (x == 0 || x == 16 || y == 0 || y >= 16 || self_eat) ? 0 : 1;
}

终于好了,我们看看效果图:

这里写图片描述

虽然相当得简陋,由于用的是全屏刷新的方法,闪屏也比较严重,但也算是麻雀虽小五脏俱全,该有的功能也都有了。在接下来我还会跟着原作者的步伐对这个小游戏进行改进,直到比较”顺眼“为止。也希望大家在看的时候有啥疑问或不对的地方,请提出来。大神路过的话,就不要喷我这种小白了。

本博客有相当一部分(0.9+)是参考自《C语言实现贪吃蛇之全靠数组篇 》

  • 30
    点赞
  • 147
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值