C语言 实现贪吃蛇 | 十分钟入门案例 | 初学者案例 | 附带设计思路 + 代码 + 图文分析

发布于:2023-01-09 ⋅ 阅读:(627) ⋅ 点赞:(0)

在这里插入图片描述

1. 贪吃蛇介绍


贪吃蛇游戏想必大家并不陌生,它的玩法很简单,通过上下左右控制贪吃蛇的移动,让它吃到地图上某个位置的食物,每次吃到食物,小蛇就会变长一段,看起来是不是很神奇呢!其实这个游戏的设计原理十分简单,通过本篇文章你可以学会使用C语言设计简单的贪吃蛇小游戏。在学习之前你只需要有以下知识的基础:

  1. C语言基础:基本数据类型、循环语句、switch 条件语句,宏定义,struct 结构体,函数的定义与使用
  2. DevCpp 工具的基本使用:编译 和运行 .c 文件

如果你有以上这些基础,我相信只需要10分钟你就能掌握贪吃蛇小游戏的编写技巧。

在这里插入图片描述

如上图所示,这个小游戏最主要的两个部分就是 画面操作
首先,小游戏绘制出了一个范围表示贪吃蛇允许的运动范围,以及不断运动着的小蛇。
其次,通过画面我们可以通过键盘来操作小蛇的方向,在这个画面中主要有 上、下、左、右 四种方向。

在写游戏代码前,我们有必要先了解一下如何用 C语言来实现小游戏的画面以及获取用户的操作。

2. 前置准备


2.1 C语言移动光标

windows.h 头文件支持许多与 Windows 系统相关的功能,这里我们主要是使用它里面当中可以获取运行的窗口,运行的坐标相关的方法。
参考:https://docs.microsoft.com/zh-cn/windows/console/setconsolecursorposition

#include <windows.h>

/*--------------------移动光标--------------------- */
void gotoxy(int x,int y) 
{
    HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);  // 获取当前运行程序的窗口 

    COORD coord;									  // 获取光标 
   
    coord.X = x;									  // 设置坐标					
    coord.Y = y;	
    
    SetConsoleCursorPosition(handle,coord);			  // 设置指定控制台屏幕缓冲区中的光标位置。 
}	

测试:

#include <stdio.h>

int main()
{
	gotoxy(2,2);
	printf("hello");
}

运行结果:
在这里插入图片描述

2.2 C语言读取键盘按键

读取用户的键盘按键则通过 conio.h 这个头文件,它的 _kbhit() 方法 判断用户是否按下某个键,它的最大特点是:如果用户没有按下任何按键,则会返回 false,若按下了按键则返回 true, 同时需配合 _getch() 函数来获取到用户之前按下的键对应的 ASCII码。

#include <conio.h>
#include <stdio.h>
/* ---获取用户按键--- */
int keyDown()
{	int key = -1;
    if(_kbhit())
    {
    	fflush(stdin);		// 刷新控制台输入的缓冲区
    	key=_getch();	    // 读取键盘的按键
    }
    return key;
}

测试:

int main(){
	int key = 0; 
	while(1){
		key = keyDown();	// 读取用户的键盘按键, 若没有则执行下一行内容
		if(key != -1)
			printf("按下的按键为: %c\n", char(key)); 
	}
}

运行结果:
在这里插入图片描述

2.3 C语言延迟生成随机数

在游戏过程中,我们发现食物的位置每次都是随机的,所以我们需要有生成随机数的函数。

#include <time.h>
#include <stdlib.h>
/*--------获取 [a, b) 范围的随机整数------*/
int randomIn(int a, int b){
    srand((unsigned int)time(NULL));
	return rand() % b + a;
}

测试:( 输出10次 [0, 10) 的随机整数 )

#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
/*--------获取 [a, b) 范围的随机整数------*/
int randomIn(int a, int b){
    srand((unsigned int)time(NULL));
	return rand() % b + a;
}

int main(){
	for(int i = 0; i < 10; i++){
		Sleep(1000);					// 延迟 1秒
    	printf("第 %d 个随机数 : %d\n" , i+1, randomIn(0, 10));
    }
}

运行效果:
在这里插入图片描述

2.4 C语言隐藏光标

为了防止游戏不断闪烁干扰实现,我们需要调用 API 来实现控制台窗口隐藏光标

/*-------------------- 隐藏光标 -------------------- */ 
void hideCursor()
{
	CONSOLE_CURSOR_INFO cursor;    
	cursor.bVisible = FALSE;    
	cursor.dwSize = sizeof(cursor);    
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);    
	SetConsoleCursorInfo(handle, &cursor);
}

3. 实现贪吃蛇小游戏

3.1 绘制游戏边界

#define MAP_HEIGHT 25					
#define MAP_WIDTH 60
#define WALL "■"
/*----------------绘制地图------------- */
void drawMap(){
    // 绘制左右边界
    for(int i=0;i<=MAP_HEIGHT;i++)		// 遍历指定的地图高度
    {
        gotoxy(0,i);					// 将光标移动到边界最左边的位置
        printf(WALL);
        gotoxy(MAP_WIDTH,i);			// 将光标移动到边界最右边的位置
        printf(WALL);
    }
	// 绘制上下边界
    for(int i=0;i<=MAP_WIDTH;i+=2) 		// 遍历指定的地图宽度
    {									// 由于 ■ 符号在水平方向上是占两个字符的, 所以地图也相应的隔两个位置
        gotoxy(i,0);					// 将光标移动到边界最上边的位置
        printf(WALL);
        gotoxy(i,MAP_HEIGHT);
        printf(WALL);					// 将光标移动到边界最下边的位置
    }
}

运行结果:
在这里插入图片描述

3.2 绘制小蛇

在绘制小蛇以前我们要知道它有哪些属性:

  • 蛇的长度
  • 蛇的移动速度
  • 蛇头和蛇身的位置(横坐标, 纵坐标)

我们可以用 struct 结构体来表示蛇的结构

#define SNAKE_SIZE 100  // 蛇头加上蛇身的最大节数
struct
{ 
	int x[SNAKE_SIZE];	// 蛇头和蛇身的横坐标
	int y[SNAKE_SIZE];  // 蛇头和蛇身的纵坐标
	int len; 			// 蛇长
	int speed; 			// 移动速度
    int direction;		// 移动方向
} snake;

接下来我们定义绘制小蛇的函数,和之前绘制边界的思路类似,先移动光标,然后再printf 打印

#define SNAKE "■"
/* ----------- 绘制小蛇 -------------*/
void drawSnake(){
	for(int i=0;i < snake.len; i++)
    {
        gotoxy(snake.x[i],snake.y[i]);
        printf(SNAKE);
    }
}

3.3 擦除小蛇尾部

根据之前移动小蛇的分析,我们知道在小蛇移动后,它的尾巴是需要抹去的,否则小蛇在移动的时候身子就会越来越长,这里为方便之后调用,定义可以擦除游戏画面的函数。

/*------------------ 擦除画面 --------------------*/
void clear(int x, int y)
{
    gotoxy(x, y);
    printf("  ");
}

3.4 绘制食物

和绘制小蛇类似,我们先定义一个表示食物的结构体:

struct
{
	int x;
	int y;
}food;

在本次小游戏的设计中,食物只有一个,所以就不需要以数组的形式存储了。

#define FOOD "●"
/*----------绘制食物----------*/
void drawFood(){
	gotoxy(food.x, food.y);
    printf(FOOD);
}

在绘制食物时,我们需要考虑绘制的位置,食物的位置是随机的,但是不能在小蛇的蛇头或者蛇身上。

/*------------ 判断坐标是否在蛇头或蛇身上--------------*/
bool isInSnake(int x,int y){
	for(int i = 0; i < snake.len; i++)
    	if(snake.x[i] == x && snake.y[i] == y)
            return true;
    return false;
}

/*------------ 随机生成食物的位置 --------*/
void randomFoodPosition(){
    do {
    	food.x = randomIn(2, MAP_WIDTH - 4);		// 食物在水平方向上必须在围墙内
        
        food.y = randomIn(1, MAP_HEIGHT - 2);		// 食物在竖直方向上必须在围墙内
    
    } while(isInSnake(food.x, food.y) || food.x % 2 != 0);		    // 当坐标不合理时, 则重新生成
}


3.5 移动小蛇

移动小蛇是有规律的,我们再次观察之前的动态图:
在这里插入图片描述

蛇头用于控制方向,如果用户没有按下任何键,那么小蛇会一直往那个方向前进,在移动过程中,蛇身的每一节总会往它靠近蛇头的那一节蛇身移动。

通过上面的分析,我们将 蛇头 和 蛇身 分开考虑:

  • 蛇头:根据移动方向移动,比如向右,那么就向右移动一格
  • 蛇身:当前这节蛇身朝着移动前靠近蛇头的那一节蛇身的位置移动,直接替换位置即可

除了绘制小蛇移动后的画面以外,我们还需要擦除它之前的尾巴,这个直接移动光标到之前尾巴的部分,printf 打印两个空格即可。在控制台中,光标的移动规律是如下图所示的,通过这个规律我们能总结出对于坐标 (x, y) ,它的上下左右四个坐标的特点:

  • 上 ( x, y -1 )
  • 下 ( x, y + 1)
  • 左 ( x - 1, y )
  • 右 ( x + 1, y )

#define D_UP 'w'
#define D_RIGHT 'd' 
#define D_DOWN 's'
#define D_LEFT 'a'

/*----------移动小蛇 --------*/
void move(){
	clear(snake.x[snake.len - 1], snake.y[snake.len -1]);
    // 先移动蛇身
    for(int i = snake.len - 1; i > 0 ; i--){
    	snake.x[i] = snake.x[i-1] + 2;
        snake.y[i] = snake.y[i-1];
    }
    // 控制蛇头
	switch (snake.direction){
        case UP:
            snake.y[0]--;
            break;
        case DOWN:
            snake.y[0]++;
            break;
        case LEFT:
            snake.x[0] -= 2;
            break;
        case RIGHT:
            snake.x[0] += 2;
            break;   
    }
    drawSnake();
}

3.6 小蛇吃到食物

当蛇头遇到食物时,小蛇吃到了食物,此时小蛇的身体会长一节,这里的实现比较简单,我们只要将小蛇的身体整体往后移动一节即可。

/*--------------  判断是否吃到食物 --------------*/
bool isEating(){
	return snake.x[0] == food.x && snake.y[0] == food.y;
}
/*---------------  小蛇吃到食物 ----------------*/
bool snakeEatenFood(){
    if(isEating()){
        // 蛇的身体 + 1
        snake.len ++;
        if(snake.len < SNAKE_SIZE) {		// 若蛇还未达到最大长度
			for(int i = snake.len - 1; i > 0; i--){
            	snake.x[i] = snake.x[i-1];
                snake.y[i] = snake.y[i-1];
            }
            // 将食物作为蛇头
            snake.x[0] = food.x;
            snake.y[0] = food.y;
            randomFoodPosition();
            drawFood();
            return true;
        }
    }
    return false;
}

3.7 判断游戏结束

当蛇头碰到边界或自己的蛇身游戏则结束,因为蛇身是跟着蛇头走的,所以我们只需要判断蛇头就行了。


bool gameover(){
    for(int i = 1; i < snake.len; i++)			// 蛇头碰到蛇身
    	if(snake.x[0] == snake.x[i] && snake.y[0] == snake.y[i])
                return true;
    if(snake.x[0] < 2 || snake.x[0] > MAP_WIDTH - 2) 	// 蛇头超出了左/右边界
        return true;
    if(snake.y[0] < 0 || snake.y[0] > MAP_HEIGHT - 1)	// 蛇头超出了上/下边界
        return true;
    return false;
}

4. 整合所有部分


将上面所有的部分整合起来,我们在 main 主函数里写游戏的逻辑:
代码关键部分:

/*---------- 初始化游戏信息 ------------*/
void init(){
	hideCursor();				// 隐藏光标 
	snake.len = 1;				// 默认只有一个蛇头 
    snake.x[0] = MAP_WIDTH / 2;
    snake.y[0] = MAP_HEIGHT / 2;
    snake.speed = 1;			
    snake.direction = D_RIGHT;	// 初始化贪吃蛇方向 向右 
    randomFoodPosition();		// 产生随机食物 
    					 
    drawMap();					// 绘制边界
    
    drawFood();					// 绘制食物
     
    drawSnake();				// 绘制蛇
    
}
int main(){
    init();
	while (!gameover()){
		gotoxy(MAP_WIDTH +5, 0);			// 显示得分 
		printf("得分: %d", snake.len - 1);
        // 判断按键
        int keydown = keyDown();
        if(keydown != -1)
        {
        	snake.direction = char(keydown);
        }
        snakeEatenFood();
		move();
        Sleep(100 / snake.speed);				// 延迟
    }
}

全部代码请参考 Gitee 网址:C语言实现贪吃蛇

最终运行效果:

在这里插入图片描述

5. 总结


相信通过本次的实践,能提升你对C语言编程的熟练度,关于这个小游戏其实还有许多可拓展的地方,比如设计一个游戏初始界面,用户可以选择"开始游戏"、“游戏说明”、“游戏设置” 等等,在游戏界面上我使用了最普通的光标移动 加 printf 打印的方式,如果你想要更好看的游戏界面,比如添加一些图片,可以了解一下 EasyX 这个图形库。

EasyX Graphics Library 是针对 Visual C++ 的免费绘图库,支持 VC6.0 ~ VC2022,简单易用,学习成本极低,应用领域广泛。目前已有许多大学将 EasyX 应用在教学当中。

不过 EasyX 是基于 C++ 语言的拓展图形库,如果你的学校是要求使用 C语言进行课程设计的话,需要考虑编程语言不同的问题哟~


网站公告

今日签到

点亮在社区的每一天
去签到