用c语言实现简易贪吃蛇

       我们先分析游戏的功能需求:有一块区域,区域内有一条蛇和一个食物,蛇通过上下左右移动到达食物所在的点,蛇吃到食物,蛇长度增加,重新生成食物,分数增加,如果蛇头碰到自己的身体或者区域边界,游戏结束。否则,游戏继续。

我们用MVC的框架来实现它,即 Model——View——Controller:

       Model:定义和实现相关的结构体类型以及功能函数
       View:定义和实现所有输出相关的函数
       Controller:定义和实现游戏逻辑相关的函数,将Model和View中的函数联系起来

从Model中先开始看

       首先,我们需要知道在贪吃蛇的游戏中,我们会用到哪些结构体,通过这些结构体,我们基本可以构造出一个贪吃蛇的游戏。这里先附上我贪吃蛇的游戏界面,方便构思:
在这里插入图片描述
       通过这幅图,我们先来分析一波:在界面中出现的元素都有 食物 分数 操作提示 分数排行榜
       现在,我们就需要将他们一一表示出来:
       
              1.    蛇的属性可以简单的分解为:蛇头蛇尾蛇前进的方向蛇前进的速度
              2.    由于蛇是动态增长的,所以用链表的形式来保存,链表的每个节点都由一个数据域和一
              个指针域组成。因此需要一个节点类型的变量。然后将这些变量连接起来形成链表。当贪吃
              蛇吃食物变长时,为了减少时间复杂度,我们用链表的头当蛇的尾巴,链表的尾当蛇的头。
              当蛇前进时,增加蛇头就等于链表的尾插,删除蛇尾就等于链表的头删,因此它的时间复杂
              度都是为O(1)。

在这里插入图片描述

              3.    在这里数据域是为了表示坐标,因此我们需要一个新的类型来表示坐标,这个类型包含
              两个元素,即 XY
              4.    蛇前进的方向只有四种,即: 因此,我们用枚举的方式来定义。
       结合上面的四点,我们需要定义的结构体就有:

typedef enum Direction{               //方向
	UP, DOWN, LEFT, RIGHT
}Direction;

typedef struct Position{				//节点的数据域
	int x;
	int y;
}Position;

typedef struct Node{                     //节点
	Position pos;
	struct Node *next;
}Node;


typedef struct Snake{                 //蛇的结构体
	Node *head;
	Node *tail;
    int speed;					//我们通过Sleep函数来控制蛇的前进速度
	Direction direction;
}Snake;  

       食物 分数 分数排行榜
              1.     食物的属性只有一种,即 食物的坐标 ,而坐标实质即为之前定义的Position类型的一
               个变量。
              2.     分数,只需要用一个整型的变量记录当前分数,并在每一次移动后刷新输出即可。
              3.     分数排行榜,每次游戏结束后,将本次的分数与已存储的分数比较,将前十再次存储
               即可。
        操作提示
              1.    这两种元素,只需要给定一个坐标,将需要输出的内容输出即可
       我们对上面的所有元素进行一次封装,将它们封装在一个结构体内:

typedef struct Game {
	Snake snake;        //蛇
	Position food;      //食物坐标
	int width;          //宽
	int height;         //高
    int score;          //分数
}Game;

       到这里,贪吃蛇中用到的结构体基本就构建完成了,下面我们来分析执行的操作:
             1.     首先,我们需要对游戏进行初始化:即 :初始化蛇的各项属性 初始化食物的位置 初始化
              分数 初始化游戏界面
                     上面这些功能我们可以封装到一个函数内,即初始化游戏 ,在初始化蛇的属性时,我们
              需要给定蛇头的位置蛇前进的方向 蛇的长度 这样我们就能将几个连续的坐标连接成一条蛇
              最后还需要给定蛇的速度


#define DEFAULT_HEAD 8
void SnakeInitialize(Snake *pSnake)     
{
    int i = 0;
    assert(pSnake != NULL);
    pSnake->direction = LEFT;
	pSnake->speed = 300;
    pSnake->head = NULL;
	pSnake->tail = NULL;
			//链表		    	尾						头
     // 这里给出的蛇坐标为 : (8,8) <- (9, 8) <-(10, 8)
     		//pSnake           head 				   tail
     		//这里默认蛇的长度为3
    for(i = DEFAULT_HEAD; i < DEFAULT_HEAD + 3; i++) {
        int x = DEFAULT_HEAD + i;
        int y = DEFAULT_HEAD;
        Node *newnode = (Node *)malloc(sizeof(Node));
        assert(newnode != NULL);
        newnode->pos.x = x;
        newnode->pos.y = y;
        newnode->next = pSnake->tail;
        pSnake->tail = newnode;
		if (pSnake->head == NULL) {
			pSnake->head = pSnake->tail;
		}
    }
}

                     初始化食物的位置时,我们用随机生成的方式,在游戏的范围内生成即可,同时,食物
              也不能出现在蛇的身体上。

void GenerateFood(Game *pgame)
{
    int x = 0, y = 0;
    assert(pgame != NULL);
    do {
        x = rand()%(pgame->width);
        y = rand()%(pgame->height);
    }while(IsInSnake(x, y, &(pgame->snake)));
    //IsInSnake(x, y, &(pgame->snake) 用来判断是否在蛇的身体上。
    //如果在 返回 1  循环继续 如果不在 返回 0 跳出循环
    (pgame->food).x = x;
    (pgame->food).y = y;
}

                     最后我们将它们封装在 初始化游戏中:

void GameInitialize(Game *pgame)
{
    assert(pgame != NULL);
    pgame->height = 28;
    pgame->width = 28;
    SnakeInitialize(&(pgame->snake));
    GenerateFood(pgame);
    pgame->score = 0;
}

              2.     而当游戏进行时,我们需要执行的操作有:蛇前进 判断蛇是否吃到食物 判断游戏是否结
              束判断分数是否增加 。Model中我们只实现操作相关的函数,判断相关的函数在Controller中
              实现。
                      关于蛇前进的操作其实只有两步,即:① 将下一个到达的坐标置为蛇头 ② 将当前的蛇
              尾去掉。而因为我们的蛇头是链表的尾,所以只需要让当前的头的next 指向新的节点即
              可。删除蛇尾也就是进行一次链表的头删。

//蛇增加头     链表的尾插
void SnakeAddHead(Snake *pSnake, Position *nextpos)
{
    assert(pSnake != NULL);
    Node *newnode = (Node *)malloc (sizeof(Node));
    assert(newnode != NULL);
    newnode->pos.x = nextpos->x;
    newnode->pos.y = nextpos->y;
    newnode->next = NULL;
	PrintSnakeBlock(&(pSnake->head->pos), nextpos);
	//	PrintSnakeBlock在View 中实现
    pSnake->head->next = newnode;
    pSnake->head = newnode;
}

//蛇删除尾   链表的头删
void SnakeRemoveTail(Snake *pSnake)
{
    Node *del = NULL;
    assert(pSnake != NULL);
    del = pSnake->tail;
	pSnake->tail = pSnake->tail->next;
	CleanSnakeBlock(&(del->pos));
	//CleanSnakeBlock在View 中实现
	free(del);
    del = NULL;
}

              3.     最后游戏结束时,我们需要执行的操作有:刷新排行榜 销毁游戏
                      游戏开始时,我们先将排行吧中的数据取出来,游戏结束时,判断新的成绩能否加入排
               行榜,(判断的方式是对排行榜个数 + 1 个数进行排序,等到保存和输出的时候,只保存
               排行榜个数个数据。比如:排行榜保存前10名 ,当取出10个成绩后,将它们保存在一个大
               小为11的数组中,将本局分数存入最后一个,进行排序,前10个就为新的排行榜)并将排
               行榜数据再次保存。最后再将蛇的身体链表free掉即可。

//读取排行榜
void SnakeRankRead(int size, int *prank)
{
	int i = 0, k = 0;
	assert(prank != NULL);
	FILE *pf = NULL;
	pf = fopen("Rank.dat", "a");
	pf = fopen("Rank.dat", "r");
	assert(pf != NULL);
	while ((i < size) && ((k = fgetc(pf)) != EOF)) {
		(*(prank + i++)) = k;
	}
}
//保存排行榜
void SnakeRankSave(int size, int *prank)
{
	int i = 0;
	FILE *pf = NULL;
	assert(prank != NULL);
	pf = fopen("Rank.dat", "w");
	assert(pf != NULL);
	for (i = 0; i < size; i++) {
		fputc(*(prank + i), pf);
	}
}
//销毁蛇身体
void SnakeDestroy(Snake *pSnake)
{
	Node *del, *cur = NULL;
	assert(pSnake != NULL);
	for (cur = pSnake->tail; cur != NULL;) {
		del = cur;
		cur = cur->next;
		free(del);
	}
	pSnake->head = NULL;
	pSnake->tail = NULL;
	pSnake->speed = 0;

}
再看View

       我们说过View 中实现的是与输出相关的,游戏中的输出则共有下面几种:输出蛇 输出食物 输出边界 输出提示 输出分数 输出排行榜 开始界面
       View中的输出我们而已采用移动光标的方式来做,这里用SetConsoleCursorPosition函数来实现。

//X 为横 Y 为纵
static void SetCurPos(int X, int Y)
{
	HANDLE hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	COORD coord = { X, Y };
	SetConsoleCursorPosition(hStdOutput, coord);
}

       因为我们在前面定义的边界为28*28,我们是从左上角,也就是(0,0)的位置开始输出,又因为中文字符占两个坐标位置,因此,在输出时需要对输出的界面进行转换。

static void NewCurPos(int X, int Y)
{
	X = 2 * (X + 1);
	Y = Y + 1;
	SetCurPos(X, Y);
}

       为了界面的美化,我们还需要一个隐藏光标的函数:

void ViewInit(int width, int height)
{
	HANDLE hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO info;
	GetConsoleCursorInfo(hStdOutput, &info);
	info.bVisible = 0;
	SetConsoleCursorInfo(hStdOutput, &info);
}

输出蛇 :     输出蛇分为两种:一种是最开始的时候我们需要将整个蛇输出。另外一种是蛇在前进过
              程中的输出。在这里我们采用局部刷新的方法,即:当蛇前进一步时,我们在新的蛇头的位
              置输出一块蛇的身体,在原来蛇尾的部分输出两个空格(蛇的身体为中文字符,占两个空格
              )。这样我们就可以根据光标的移动输出,来显示蛇的前进。

//输出蛇
void PrintSnake(const Snake *pSnake)
{
	Node *cur = NULL;
	assert(pSnake != NULL);
	for (cur = pSnake->tail; cur != NULL; cur = cur->next)
	{
		NewCurPos((cur->pos).x, (cur->pos).y);
		if (cur == pSnake->head) {
			printf("⊙");
		}
		else {
			printf("※");
		}
	}
	SetCurPos(65, 16);

}
//新输出的蛇头
void PrintSnakeBlock(const Position *prevpos, const Position *curpos)
{
	NewCurPos(prevpos->x, prevpos->y);
	printf("※");
	NewCurPos(curpos->x, curpos->y);
	printf("⊙");
}
//清除掉的蛇尾
void CleanSnakeBlock(const Position *curpos)
{
	NewCurPos(curpos->x, curpos->y);
	printf("  ");
}

输出食物 输出边界 输出提示 输出分数 输出排行榜 开始界面
                   只需要将光标移动到指定位置输出即可:

//输出界面
void PrintStart0(int width, int height)
{
	ViewInit(width, height);
	NewCurPos(width / 4, height / 2);
	printf("PRESS SPACE TO START GAME !");
}
void PrintStart1(int width, int height)
{
	NewCurPos(width / 4, height / 2);
	printf("                            ");
}

void PrintEnd(int width, int height)
{
	NewCurPos(width / 3, height / 2);
	printf("GAME OVER!");
	NewCurPos(width + 2, height);
}

void PrintWall(int width, int height)
{
	int i = 0;


	//上
	SetCurPos(0, 0);
	for (i = 0; i < width + 2; i++) {
		printf("█");
	}


	//左
	for (i = 0; i < height + 2; i++) {
		SetCurPos(0, i);
		printf("█");
	}

	//下
	SetCurPos(0, height + 1);
	for (i = 0; i < width + 2; i++) {
		printf("█");
	}

	//右
	for (i = 0; i < height + 2; i++) {
		SetCurPos(2 * (width + 1), i);
		printf("█");
	}
}
//输出食物
void PrintFood(const Position *pFood)
{
	NewCurPos(pFood->x, pFood->y);
	printf("●");
}
//输出分数
void PrintScore(int width, int height, int score)
{
	NewCurPos(width + 2, height / 3);
	printf("当前分数:%d", score);
}
//输出排行榜
void PrintRank(int width, int height, int size, const int *pscore)
{
	int i = 0;
	assert(pscore != NULL);
	NewCurPos(width + 15, 0);
	printf("Rank:");
	for (i = 1; i <= size; i++) {
		NewCurPos(width + 15, i);
		printf("第%2d名:%5d分", i, *(pscore + i - 1));
	}
}
//输出提示
void PrintFunction(int width, int height)
{
	NewCurPos(width + 2, height / 2);
	printf("空格键暂停");
	NewCurPos(width + 2, (height / 2) + 5);
	printf("长按方向键加速");
}

       因为上面这些函数在游戏的一开始就需要整体进行一次输出,因此我们将它们封装在一个函数内部,方便调用,在需要单独输出时,再单独调用即可。

void PrintGame(const Game *pGame)
{
	PrintWall(pGame->width, pGame->height);
	PrintSnake(&(pGame->snake));
	PrintScore(pGame->width, pGame->height, pGame->score);
	PrintFunction(pGame->width, pGame->height);
}
最后是Controller的部分

       在这里我们将Model中的函数以及View中的函数联系起来,最后组成我们的贪吃蛇。

  1. 游戏开始:
    a. 进行界面输出 PrintGame
    b. 游戏的初始化 GameInitialize
    c. 从排行榜文件中读取数据
               将以前的排行榜中的数据取出来,存放在一个比排行榜容量大1的数组中。
#define DEFAULT_RANKNUM 10
int *prank = NULL;
prank = (int *)malloc(sizeof(int) * (DEFAULT_RANKNUM + 1));
SnakeRankRead(DEFAULT_RANKNUM, prank);
  1. 游戏进行:
    a. 输出食物 PrintFood
    b. 输出当前分数 PrintScore
    c. 进行按键操作
           这里我们用到了GetAsyncKeyState函数来检测我们按下的键,当我们按下一个键,并且目前蛇的方向与我们按下键的方向不是反方向时,蛇的方向更改为我们按下的键的方向。如果本次按下的键的方向与上次的相同,那么蛇的前进速度增加。也就是speed的值减小,在这里我使用了一个固定值50,这并不会改变蛇自身的速度的值,只是通过isspeed这个标记的值来决定,使用当前的速度,还是固定值50,作为本次循环中Sleep 函数的值。如果我们按下了空格,那么游戏会进入暂停状态,直下一次按下空格。
		if (GetAsyncKeyState(VK_UP) && game.snake.direction != DOWN) {
			if (game.snake.direction == UP) {
				isspeed = 1;
			}
			game.snake.direction = UP;
		}
		else if (GetAsyncKeyState(VK_DOWN) && game.snake.direction != UP) {
			if (game.snake.direction == DOWN) {
				isspeed = 1;
			}
			game.snake.direction = DOWN;
		}
		else if (GetAsyncKeyState(VK_LEFT) && game.snake.direction != RIGHT) {
			if (game.snake.direction == LEFT) {
				isspeed = 1;
			}
			game.snake.direction = LEFT;
		}
		else if (GetAsyncKeyState(VK_RIGHT) && game.snake.direction != LEFT) {
			if (game.snake.direction == RIGHT) {
				isspeed = 1;
			}
			game.snake.direction = RIGHT;
		}
		else if (GetAsyncKeyState(VK_SPACE)) {
			Pause();
		}

    d. 判断食物是否被吃。
           只需要判断蛇头的位置是否在食物的坐标处即可。当走到这一步的时候,已经检测过一次按键
    了,所以蛇头已经变为了新的方向上的蛇头,因此在这一步需要获取下一个坐标,GetNextPos
    就是新的蛇头的位置。用新的蛇头的位置和食物的位置做比较。
           i. 如果被吃:增加蛇头,更新分数,生成新的食物。
           ii. 如果没有被吃:增加蛇头,删除蛇尾。

		//获取新的蛇头的坐标
		static Position GetNextPos(const Snake *pSnake)					
		{
				assert(pSnake != NULL);
				Position nextpos = pSnake->head->pos;
				switch(pSnake->direction) {
				case UP:
					nextpos.x = (pSnake->head->pos).x;
					nextpos.y = (pSnake->head->pos).y - 1;
					break;
				case DOWN:
					nextpos.x = (pSnake->head->pos).x;
					nextpos.y = (pSnake->head->pos).y + 1;
					break;
				case LEFT:
					nextpos.x = (pSnake->head->pos).x - 1;
					nextpos.y = (pSnake->head->pos).y;
					break;
				case RIGHT:
					nextpos.x = (pSnake->head->pos).x + 1;
					nextpos.y = (pSnake->head->pos).y;
					break;
			}	
			return nextpos;
		}
		//判断食物是否被吃
		static int IsEat(const Position *pFood, const Position *nextpos)
		{
				return (pFood->x == nextpos->x) && (pFood->y == nextpos->y);
		}
		//判断按完之后的情况
		Position nextpos = GetNextPos(&(game.snake));
		//走的这里的时候,蛇的头部已经到达了下一个坐标处,只差赋值
		// 吃到食物   没吃食物
		if (IsEat(&(game.food), &nextpos)) {
			//吃到食物 尾巴不变 头变长一个  重新生成食物  分数增加
			GenerateFood(&game);
			if (game.snake.speed > 80) {
				game.snake.speed -= 5;
			}
			game.score += 10;
		}

    e. 判断游戏是否结束。
                判断游戏是否结束,只需要判断蛇头与边界是否重合、蛇头与自身除蛇头以外的位置是否重合即可。

static int GameOver(const Game *pGame)
{
	Node *cur = NULL;
	assert(pGame != NULL);


	//左边
	if (pGame->snake.head->pos.x < 0) {
		return 1;
	}
	//右边
	if (pGame->snake.head->pos.x >= pGame->width) {
		return 1;
	}
	//上边
	if (pGame->snake.head->pos.y < 0) {
		return 1;
	}
	//下边
	if (pGame->snake.head->pos.y >= pGame->height) {
		return 1;
	}
	//自己
	for (cur = pGame->snake.tail; cur != pGame->snake.head; cur = cur->next) {
		if (cur->pos.x == pGame->snake.head->pos.x && cur->pos.y == pGame->snake.head->pos.y) {
			return 1;
		}
	}

	return 0;
}

           i. 如果结束,跳出循环。
           ii. 如果没有结束,继续重复游戏进行的动作。
3. 游戏结束:
    a. 输出结束界面PrintEnd
    b. 更新排行榜
           将本局的分数存入存放排行榜分数数组的最后一位,从最后一位往前排序,将排行榜个数的数
    据从前到后输出,并保存。

	(*(prank + DEFAULT_RANKNUM)) = game.score;
	SnakeScoreSort(DEFAULT_RANKNUM + 1, prank);
	SnakeRankSave(DEFAULT_RANKNUM, prank);
	PrintRank(game.width, game.height, DEFAULT_RANKNUM, prank);

    c. 游戏销毁 GameDestroy

上面只是讲了实现的思路和框架,代码并不是很全,所以在最后附上代码地址:

Mmmmmmmi

以上即为本篇所有内容,不足之处还望指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值