文章较长,看完大约需要一个小时,初学者能够通过这个项目学习很多课余知识是很棒的
ps:后续看到本文的读者,尽量用dev c++来运行本文代码,其它几个ide笔者尝试了,确实会出现一些未知的bug,可以先尝试用dev c++ 跑出代码,再根据自己喜欢用的ide调试

目标人群

已掌握while for do…while if 等基本控制语句及数组相关知识的初学者

前言

  1. 本文意在增加刚入门 c语言的朋友的兴趣,目标是让读者朋友能感受到编码的乐趣,最后能自己独立写出此代码,并自行不断优化、完善。

  2. 本文会涉及到较多对于初学者较陌生的知识,希望大家不要产生畏难心理,笔者为了文章的流畅性会简单介绍,想深入了解的朋友可以点击文中对应篮字。

       接下来让我们看看代码的全景(笔者ide用的是devC++5.11,用vscode的朋友在学习完本文,记得看一下评论)
    

代码

#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
#include<windows.h>
 
#define High 20   //游戏画面尺寸 以向下为x的正半轴,向右为Y的正半轴 
#define Width 30
 
//全局变量
int moveDirection;  //小蛇移动位置 ,上下左右分别用1,2,3,4表示 
int food_x,food_y;  //食物的位置 
int canvas[High][Width]={0};//二维数组存储游戏画布中对应元素 


//该函数是用于控制 控制台的光标的移动
void gotoxy(int x,int y)
{	
	HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);	//调用
	COORD pos;			//定义一个坐标
	pos.X=x;
	pos.Y=y;
	SetConsoleCursorPosition(handle,pos);
} 

//该函数是为了蛇的正常运动以及保证食物的持续出现
void moveSankeByDirection()
{
	int i,j;
	for(i=1;i<High-1;i++)
	{
		for(j=1;j<Width-1;j++)
		{
			if(canvas[i][j]>0)
				canvas[i][j]++;
		}
	}
	
	int oldTail_i,oldTail_j,oldHead_i,oldHead_j;
	int max=0;
	
	for(i=1;i<High-1;i++)
	{
		for(j=1;j<Width-1;j++)
		{
			if(canvas[i][j]>0)
			{
				if(max<canvas[i][j])
				{
					max=canvas[i][j];
					oldTail_i=i;
					oldTail_j=j;
				}
				if(canvas[i][j]==2)
				{
					oldHead_i=i;
					oldHead_j=j;
				}
			}
		}
	} 
	
	int newHead_i,newHead_j;
	
	if(moveDirection==1)			//向上移动 
	{
		newHead_i=oldHead_i-1;
		newHead_j=oldHead_j;
	}
	if(moveDirection==2)			//向下移动 
	{
		newHead_i=oldHead_i+1;
		newHead_j=oldHead_j;
	}
	if(moveDirection==3)			//向左移动 
	{
		newHead_i=oldHead_i;
		newHead_j=oldHead_j-1;
	}
	if(moveDirection==4)			//向右移动 
	{
		newHead_i=oldHead_i;
		newHead_j=oldHead_j+1;
	}
	
		//如果吃到食物
	if(canvas[newHead_i][newHead_j]==-2)
	{
		canvas[food_x][food_y]=0;
		//产生一个新的食物
		food_x=rand()%(High-5)+2;
		food_y=rand()%(Width-5)+2;
		canvas[food_x][food_y]=-2;
	
		//原来的旧蛇尾,长度自动增加1
	} 


	else//否则,为了保持长度不变,原来的旧蛇尾减掉
	{
		canvas[oldTail_i][oldTail_j]=0;
	}
	
	//小蛇是否撞墙或者和自身相撞,游戏失败
	if(canvas[newHead_i][newHead_j]>0||canvas[newHead_i][newHead_j]==-1)
	{
		printf("游戏失败!\n");
		Sleep(2000);		//Windows API函数为了不占过多cpu资源,让程序休眠
		system("pause"); //C语言标准库中的函数,让程序实现冻结屏幕,便于观察程序的执行结果
		exit(0);  			//c语言标准库中的函数,用于终止程序
	} 
	else
	{
		canvas[newHead_i][newHead_j]=1;  //为新的蛇头赋值
	}
} 

//初始化时的函数
void startup()
{
	int i,j;
	//初始化边框
	for(i=0;i<High;i++)
	{
		canvas[i][0]=-1;
		canvas[i][Width-1]=-1;
	}
	for(j=0;j<Width;j++)
	{
		canvas[0][j]=-1;
		canvas[High-1][j]=-1;
	} 
	
	//初始化蛇头
	canvas[High/2][Width/2]=1;
	//初始化蛇身,画布元素分别为2,3,4,5
	for(i=1;i<=4;i++)
		canvas[High/2][Width/2-i]=i+1; 
	
	//初始小蛇向右移动
	moveDirection=4;
	
	food_x=rand()%(High-5)+2;  //rand()函数是c语言标准库中的函数,用于产生随机数
	food_y=rand()%(Width-5)+2;
	canvas[food_x][food_y]=-2;
}

//一个根据咱们定义的画布上的数据而分别为用户呈现不同视觉效果的函数 
void show()
{
	gotoxy(0,0);
	int i,j;
	for(i=0;i<High;i++)
	{
		for(j=0;j<Width;j++)
		{
			if(canvas[i][j]==0) //0为空格,-1为边框#,-2为食物F,1为蛇头@,大于1为蛇身* 
				printf(" ");
			else if(canvas[i][j]==-1)
				printf("#");
			else if(canvas[i][j]==1)
				printf("@");
			else if(canvas[i][j]>1)
				printf("*");
			else if(canvas[i][j]==-2)
				printf("F");
		}
		printf("\n");
	}
	Sleep(100);
}

//与用户无关的更新的函数 
void updateWithoutInput()		 
{
	moveSankeByDirection();
}

//与用户有关的更新的函数 ,判断用户的输入 
void updateWithInput()			
{
	char input;
	if(kbhit())					//kbhit是conio库中的函数,判断是否有输入 
	{
		input=getch();	//根据用户不同的输入来移动,不必输入回车 
		if(input=='a')  	//input、getch是conio库中的函数,用于输入和输出字符
		{
			moveDirection=3;	//位置左移 
			moveSankeByDirection();
		}
				if(input=='d')
		{
			moveDirection=4;	//位置右移 
			moveSankeByDirection();
		}
				if(input=='w')
		{
			moveDirection=1;	//位置上移 
			moveSankeByDirection();
		}
				if(input=='s')
		{
			moveDirection=2;	//位置下移 
			moveSankeByDirection();
		}
	}
}
 
int main()
{
	startup();					//数据初始化
	while(1)					//游戏循环执行 
	{
		show();					//显示画面 
		updateWithoutInput();
		updateWithInput();
	}
	return 0; 
}

思路

  1. 大多数读者朋友应该都玩过贪吃蛇这个小游戏,想要写出一个贪吃蛇的游戏,我们需要设置哪些元素呢?首先为了游戏的可玩性,不浪费过多时间在蛇的移动上,可能需要一个地方,让蛇局限在某块区域移动;其次要设计一下蛇的样子以及考虑如何让蛇的移动,按‘w’键让蛇向上移动还是按其他什么键和不按的状态下,如何保持蛇的移动;设计一下,让食物随机出现;最后就是制定游戏的规则,什么情况下判定游戏失败。

—————————————————————————————————

程序分析

让我们来看看代码。首先我们要明确一个概念,c语言程序由一个一个的函数构成,而此游戏的程序由六个子函数和一个主函数(main)构成。笔者将按照程序中编写的函数的顺序逐一讲解。


预处理指令

	#include<stdio.h>  
	#include<stdlib.h> //c语言标准库头文件
	#include<conio.h>// 控制台输入输出库头文件
	#include<windows.h> //Windows 操作系统库头文件

	#define High 20   //宏定义 
	#define Width 30 //游戏画面尺寸 以向下为x的正半轴,向右为Y的正半轴 

#define此处是定义一个标识符用来表示一个常量,其方便程序的修改


全局变量

int moveDirection;  //小蛇移动位置 ,上下左右分别用1,2,3,4表示 
int food_x,food_y;  //食物的位置 
int canvas[High][Width]={0};//二维数组存储游戏画布中对应元素 

  1. 以上这几个变量任何一个子函数都可以调用,不过若是子函数中定义的变量全局变量重名会使程序冲突,这种情况不被允许。但读者朋友要注意各子函数中定义的变量是允许重名的,相互不影响。
  2. 需重点关注此处定义的数组,此数组我们取名为canvas(画布),其作用是保存程序中不同元素的数值,方便子函数show根据不同的数值在控制台中呈现不同的效果。

控制控制台光标的子函数

void gotoxy(int x,int y)
{	
	HANDLE handle=GetStdHandle(STD_OUTPUT_HANDLE);//
	COORD pos;
	pos.X=x;
	pos.Y=y;
	SetConsoleCursorPosition(handle,pos);
} 
  1. 该函数会被另一个子函数(show)调用,其作用是从Windows API(控制台API函数(此博客很棒))中调用关于控制台上的光标的函数
    而调用的函数有HANDLE 、COORD、GetStdHandle、SetConsoleCursorPosition.

  2. HANDLE 是一种无类型指针,其定义的变量叫句柄
    COORD是Windows API中定义的一种结构,表示一个字符在控制台屏幕上的坐标

  3. GetStdHandle此函数用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值)
    SetConsoleCursorPosition 此函数是用于光标的位置控制


蛇的正常运动以及保证食物的持续出现的函数

//该函数是为了蛇的正常运动以及保证食物的持续出现
void moveSankeByDirection()
{
	int i,j;
	for(i=1;i<High-1;i++)
	{
		for(j=1;j<Width-1;j++)
		{
			if(canvas[i][j]>0)
				canvas[i][j]++;
		}
	}
	
	int oldTail_i,oldTail_j,oldHead_i,oldHead_j;
	int max=0;
	
	for(i=1;i<High-1;i++)
	{
		for(j=1;j<Width-1;j++)
		{
			if(canvas[i][j]>0)
			{
				if(max<canvas[i][j])
				{
					max=canvas[i][j];  //此处很精妙,可细细体会
					oldTail_i=i;
					oldTail_j=j;
				}
				if(canvas[i][j]==2)
				{
					oldHead_i=i;
					oldHead_j=j;
				}
			}
		}
	} 
	
	int newHead_i,newHead_j;
	
	if(moveDirection==1)			//向上移动 
	{
		newHead_i=oldHead_i-1;
		newHead_j=oldHead_j;
	}
	if(moveDirection==2)			//向下移动 
	{
		newHead_i=oldHead_i+1;
		newHead_j=oldHead_j;
	}
	if(moveDirection==3)			//向左移动 
	{
		newHead_i=oldHead_i;
		newHead_j=oldHead_j-1;
	}
	if(moveDirection==4)			//向右移动 
	{
		newHead_i=oldHead_i;
		newHead_j=oldHead_j+1;
	}
	
		//如果吃到食物
	if(canvas[newHead_i][newHead_j]==-2)
	{
		canvas[food_x][food_y]=0;
		//产生一个新的食物
		food_x=rand()%(High-5)+2;
		food_y=rand()%(Width-5)+2;
		canvas[food_x][food_y]=-2;
	
		//原来的旧蛇尾,长度自动增加1
	} 


	else//否则,为了保持长度不变,原来的旧蛇尾减掉
	{
		canvas[oldTail_i][oldTail_j]=0;
	}
	
	//小蛇是否撞墙或者和自身相撞,游戏失败
	if(canvas[newHead_i][newHead_j]>0||canvas[newHead_i][newHead_j]==-1)
	{
		printf("游戏失败!\n");
		Sleep(2000);		//Windows API函数为了不占过多cpu资源,让程序休眠
		system("pause"); //C语言标准库中的函数,让程序实现冻结屏幕,便于观察程序的执行结果
		exit(0);  			//c语言标准库中的函数,用于终止程序
	} 
	else
	{
		canvas[newHead_i][newHead_j]=1;  //为新的蛇头赋值
	}
} 
预备知识
  1. Sleep为了更好理解可以点这)是Windows API库中的函数,作用睡觉是为了不占过多cpu资源,让程序休眠。
  2. system(“pause”)C语言标准库中的函数,让程序实现冻结屏幕,便于观察程序的执行结果。
  3. exit(0)c语言标准库中的函数,用于终止程序
  4. rand()函数用来产生随机数,其范围公式:int num = rand() % n +a;
    其中的a是起始值,n-1+a是终止值,n是最大值。
此子函数 解析
  1. 第一个for循环及其嵌套的for循环:让蛇头和蛇身的数字都加一,加一后蛇尾的数值会变成整个画布上最大的值,其意在为蛇的移动时,数据的迭代,也就是方便控制蛇尾和蛇头(此时实现蛇移动的关键)。
  2. 第二个for循环及其嵌套的for循环:为了找到蛇尾的变量和蛇头的变量
    后面的if循环:通过判断用户输入的值,进行移动和改变蛇的长度以及判断是否结束游戏。

用于初始化时的子函数

//初始化时的函数
void startup()
{
	int i,j;
	//初始化边框
	for(i=0;i<High;i++)
	{
		canvas[i][0]=-1;
		canvas[i][Width-1]=-1;
	}
	for(j=0;j<Width;j++)
	{
		canvas[0][j]=-1;
		canvas[High-1][j]=-1;
	} 
	
	//初始化蛇头
	canvas[High/2][Width/2]=1;
	//初始化蛇身,画布元素分别为2,3,4,5
	for(i=1;i<=4;i++)
		canvas[High/2][Width/2-i]=i+1; 
	
	//初始小蛇向右移动
	moveDirection=4;
	
	food_x=rand()%(High-5)+2;  //rand()函数是c语言标准库中的函数,用于产生随机数
	food_y=rand()%(Width-5)+2;
	canvas[food_x][food_y]=-2;
}

此处是分别为游戏边框、蛇的身体以及第一次出现的食物赋值。


根据咱们定义的画布上的数据而分别为用户呈现不同视觉效果的子函数

//一个根据咱们定义的画布上的数据而分别为用户呈现不同视觉效果的函数 
void show()
{
	gotoxy(0,0);
	int i,j;
	for(i=0;i<High;i++)
	{
		for(j=0;j<Width;j++)
		{
			if(canvas[i][j]==0) //0为空格,-1为边框#,-2为食物F,1为蛇头@,大于1为蛇身* 
				printf(" ");
			else if(canvas[i][j]==-1)
				printf("#");
			else if(canvas[i][j]==1)
				printf("@");
			else if(canvas[i][j]>1)
				printf("*");
			else if(canvas[i][j]==-2)
				printf("F");
		}
		printf("\n");
	}
	Sleep(100);
}

根据咱们定义的画布上的数据在控制台中输出不同的符号。


与用户无关的更新的子函数

//与用户无关的更新的函数 
void updateWithoutInput()		 
{
	moveSankeByDirection();
}

对于为什么不直接在主函数中调用moveSankeByDirection函数,主要是与updateWithInput函数形成对比,让代码更有可读性。


判断用户输出了什么键的子函数

//与用户有关的更新的函数 ,判断用户的输入 
void updateWithInput()			
{
	char input;
	if(kbhit())					//kbhit是conio库中的函数,判断是否有输入 
	{
		input=getch();	//根据用户不同的输入来移动,不必输入回车 
		if(input=='a')  	//input、getch是conio库中的函数,用于输入和输出字符
		{
			moveDirection=3;	//位置左移 
			moveSankeByDirection();
		}
				if(input=='d')
		{
			moveDirection=4;	//位置右移 
			moveSankeByDirection();
		}
				if(input=='w')
		{
			moveDirection=1;	//位置上移 
			moveSankeByDirection();
		}
				if(input=='s')
		{
			moveDirection=2;	//位置下移 
			moveSankeByDirection();
		}
	}
}
 

预备知识

  1. kbhit()是conio库中的函数,判断是否有输入
  2. input和getch()分别是输出字符和输入字符的意思

解析

根据判断用户输入了何键,对moveDirection赋值,最后再调用moveSankeByDirection子函数对蛇的身体的数值进行迭代,以实现蛇的移动和长度变化。


主函数

int main()
{
	startup();					//数据初始化
	while(1)					//游戏循环执行 
	{
		show();					//显示画面 
		updateWithoutInput();
		updateWithInput();
	}
	return 0; 
  1. 程序一般都是从主函数(main)开始的运行的,所以其实这才是整个程序的开头。
  2. 程序在定义完全局变量后,先对carvans赋值,然后根据数值来输出符号,最后就是不断地对蛇移动后身体的数值以及食物的数值迭代,判断游戏是否结束。

总结

希望读者朋友细细品味此游戏的代码,并在最后通过自己的努力写出来后喜笑颜开,欣喜若狂。
(作者水平有限,若有疏漏以及言辞不当处,乞请读者朋友们斧正,若是对本文章的排版、文案有建议的话,也请朋友们不吝赐教!!)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐