贪吃蛇小游戏
main.o+game.o->可执行程序,所以在这一步链接器发现:同一个符号(struct SNAKE)被定义了两次,这是错误的,所以在做文件包含的项目时,不要忘记该结构。光标位置有阻塞等待,即正常输入一个字符,它会在那里闪,等待你输入下一个字符,而贪吃蛇游戏,我们不能让用户输入一个方向蛇走一下,而应该让蛇一直走,所以应该把阻塞等待删掉。蛇的移动控制分析://接收用户的按键操作,进行移动控制//四
snake.h
#ifndef __SNAKE_H__//防止头文件重复包含的标准写法,即头文件保护。
#define __SNAKE_H__
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
#define WIDE 60//宽度
#define HIGH 20//高度
//定义一个身体对象
struct BODY{//BODY指的是蛇身体,即蛇的位置坐标,所以用二维坐标表示位置,这是一个蛇身的坐标
int x;
int y;
};
//定义蛇对象
struct SNAKE{//蛇身就是由很多位置的坐标组成的,所以有很多坐标,所以在此定义了上述BODY数组
struct BODY body[WIDE*HIGH]; //蛇身铺满整个范围,即为蛇身的最大值。为范围的长和宽
int size;//蛇的当前大小
}snake;//一个蛇对象
//定义食物对象
struct FOOD{
int x;
int y;
}food;//一个食物对象
//定义分数
int score=0;
extern int kx;//最好是在头文件声明,在源文件定义和初始化。
extern int ky;
extern int lastx;//描述蛇尾的坐标
extern int lasty;
int sleepSecond=400;//设置初始延迟为400ms
//明确头在左边还是右边,在于身子和头的坐标怎么写
//声明函数
void initSnake();//定义全局变量,如果被调用的函数放在主函数后面时
void initFood();
void initUI();
#endif
#ifndef __SNAKE_H__ 如果宏__SNAKE_H__还没有被定义,用于检查这个标识符是否被定义过
#define __SNAKE_H__如果上面的#ifndef条件为真,即没有定义过这个宏,就执行这一行,定义这个宏。
#ifndef __SNAKE_H__ // 第一次包含:__SNAKE_H__未定义 → 条件为真
#define __SNAKE_H__ // 定义__SNAKE_H__宏
// 头文件的所有内容(函数声明、结构体定义等)
#endif // 结束条件编译
由上述得,假设多个源文件都包含了snake.h:
第一次包含时:
· __SNAKE_H__ 未定义 → #ifndef 条件为真
· 执行 #define __SNAKE_H__ 定义宏
· 编译头文件内容
后续再包含时:
· __SNAKE_H__ 已经在上次定义了 → #ifndef 条件为假
· 跳过整个头文件内容,直接到 #endif
· 避免重复编译相同的内容
好处:
1. 防止重复定义错误:避免结构体、函数等被重复定义
2. 提高编译速度:避免重复编译相同代码
3. 避免循环包含问题:当多个头文件相互包含时特别重要
注意:
· 通常宏名使用头文件名的大写形式,如 SNAKE_H
· 加上双下划线是为了减少命名冲突的可能性
· 最后一定要有 #endif 来结束条件编译
如果没有用上述两行,会造成编译错误:
// 情况1:同一个源文件多次包含
#include "snake.h"
#include "snake.h" // 如果没有保护,这里会重复定义所有内容!
// 情况2:多个文件包含同一个头文件
// main.c
#include "snake.h" // 定义了struct SNAKE
// game.c
#include "snake.h" // 又定义了struct SNAKE → 链接错误!
上述情况2的意思是:
// snake.h(头文件)
struct SNAKE { // 结构体定义
int size;
// ... 其他成员
};
// main.c(源文件1)
#include "snake.h" // 包含头文件
// main.c的其他代码...
// game.c(源文件2)
#include "snake.h" // 再次包含同一个头文件
// game.c的其他代码...
编译阶段,每个.c文件单独编译
// 编译器编译 main.c 时:
#include "snake.h" → 将snake.h的内容复制到main.c中
// 结果:main.o 目标文件包含了 struct SNAKE 的定义
// 编译器编译 game.c 时:
#include "snake.h" → 再次将snake.h的内容复制到game.c中
// 结果:game.o 目标文件也包含了 struct SNAKE 的定义
上述编译看似没什么问题,因为没有重复编译,但是在链接阶段,即将所有.o文件合并成可执行文件。main.o+game.o->可执行程序,所以在这一步链接器发现:同一个符号(struct SNAKE)被定义了两次,这是错误的,所以在做文件包含的项目时,不要忘记该结构。
main.c
#define _CRT_SECURE_NO_WARNINGS//因为对于有些C语言标准库函数微软编辑器会发出安全警告
#include"snake.h"//引入自定义头文件
#include<windows.h>
#include<conio.h>
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int lastx;
int lasty;
int kx;
int ky;
void move(){//蛇的移动,即位置改变
int i;
for(i=snake.size-1;i>0;i--){
snake.body[i].x=snake.body[i-1].x;
snake.body[i].y=snake.body[i-1].y;
}
snake.body[0].x+=kx;
snake.body[0].y+=ky;
return;
}
void playGame()
{
int i;
char key='d';
//判断蛇撞墙
while(snake.body[0].x>=0&&snake.body[0].x<WIDE
&&snake.body[0].y>=0&&snake.body[0].y<HIGH){
initUI();
//判断用户是否按键
if(_kbhit()){
//接收用户按键输入
key=_getch();
}
switch(key){
case 'w': kx=0;ky=-1;break;
case 's': kx=0;ky=1;break;
case 'd': kx=1;ky=0;break;
case 'a': kx=-1;ky=0;break;
default:
break;
}
//蛇撞身体
for(i=1;i<snake.size;i++){
if(snake.body[0].x==snake.body[i].x
&&snake.body[0].y==snake.body[i].y){
return;//游戏结束
}
}
//蛇头撞食物
if(snake.body[0].x==food.x
&&snake.body[0].y==food.y){
//食物消失,再生成随机数
initFood();
//身体增zhang
snake.size++;
//加分
score+=10;
//加速
sleepSecond-=100;
}
//存储蛇尾坐标
lastx=snake.body[snake.size-1].x;
lasty=snake.body[snake.size-1].y;
move();
Sleep(sleepSecond);
}
return;
}
void initWall()
{
size_t i,j;
for(i=0;i<=HIGH;i++){
for(j=0;j<=WIDE;j++){
if(j==WIDE){
printf("|");
}
else if(i==HIGH){
printf("_");
}
else{
printf(" ");
}
}
printf("\n");
}
}
//想要把蛇放到哪个位置,实际上需要修改控制台光标位置。因为光标在哪,我们从键盘上输入的位置就在哪
void showScore(){
//将光标默认位置移动至不干扰游戏的任意位置
COORD coord;
coord.X=0;
coord.Y=HIGH+2;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);
printf("Game Over!!!\n");
printf("成绩为:%d\n\n\n",score);
return;
}
int main()
{
//去除光标
CONSOLE_CURSOR_INFO cci;
cci.dwSize=sizeof(cci);
cci.bVisible=FALSE;
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cci);
srand(time(NULL));//播种随机数的种子
//注意:srand和rand来自<stdlib.h>,time来自<time.h>
initSnake();//初始化蛇
initFood();//初始化食物
initWall(); //画墙
initUI(); //画蛇和食物
playGame();//启动游戏
//打印分数
//printf("snake:头:x=%d,y=%d\n",snake.body[0].x,snake.body[0].y);
//printf("snake 身:x=%d,y=%d\n",snake.body[1].x,snake.body[1].y);
//printf("food:x=%d,y=%d\n",food.x,food.y);
//打印分数
showScore();
system("pause");
return EXIT_SUCCESS;
}
//画出蛇和食物在屏幕上
//初始化界面控件
void initUI()
{
COORD coord={0};//定义结构体
//coord.X=snake.body[0].x;
//coord.Y=snake.body[0].y;
//画蛇
size_t i;
for(i=0;i<snake.size;i++){//蛇头和蛇身是有关系的,只需要改变【】内的值即可,所以可以用循环
coord.X=snake.body[i].x;//将坐标都赋给这个结构体,然后利用后面的一行将光标的位置移动到定义好的蛇头和蛇身的位置。
coord.Y=snake.body[i].y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);//将光标的位置定位到初始化好的蛇头位置 。
if(i==0){
putchar('@');
}else{
putchar('*');
}
}
//去除蛇尾。否则一开始蛇尾的位置不变,蛇身一直在增大
coord.X=lastx;//还没有重画蛇之前,先将现在的蛇尾坐标记录下来。就将现在的蛇尾去除,以便之后重画的蛇就是去掉蛇尾后正常的蛇
coord.Y=lasty;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);//改变光标的位置到蛇尾
putchar(' ');//蛇尾换成空格就是去掉蛇尾
//解决运行结果看起来被请按任意键输出覆盖的问题
//coord.X=0;
//coord.Y=0;
//SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);
//把光标设置到屏幕中间
//SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);
//putchar('@');
//画食物
coord.X=food.x;
coord.Y=food.y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);
putchar('#');
}
//初始化蛇:封装一个函数,完成初始化
void initSnake()
{
snake.size=2;//初始化蛇的大小
snake.body[0].x=WIDE/2;//初始化蛇头
snake.body[0].y=HIGH/2;
snake.body[1].x=WIDE/2-1;//说明蛇头朝右,只定义一节蛇身
snake.body[1].y=HIGH/2;
return;
}
//初始化食物:
void initFood(){
food.x=rand()%WIDE;//伪随机函数,真正随机应当播种随机数的种子
food.y=rand()%HIGH;//随机数有范围。必须小于边界
return;
}
上述第一行代码,如果没有会出现以下安全警告:
char buffer[10];
scanf("%s", buffer); // VS会警告:C4996 错误
// 错误信息:'scanf': This function or variable may be unsafe...
详细解释设计随机数食物:
srand(time(NULL));此行代码是设置随机数种子。
1.time(NULL)来自<time.h>。NULL表示我们不关心时间的详细结构,只获取秒数;每秒变化一次,所以每次程序运行时值都不同
2.srand()播种随机数来自<stdlib.h>。设置随机数生成器的种子值,格式srand(种子值)
3.整体,意思是用当前时间作为种子来初始化随机数生成器
SetConsoleCursorPosition函数:包含头文件:include<conio.h><windows.h>
为什么要播种?
// 不播种的情况:
rand(); // 总是生成相同的"随机数"序列
// 每次运行程序:42, 17, 93, 65...(完全一样)每次开头都是一样的,数字序列不变
// 播种后的情况:
srand(time(NULL)); // 用当前时间播种
rand(); // 生成不同的"随机数"序列
// 第一次运行:42, 17, 93, 65...//每次启动程序时间不同,所以起始位置不同,得到的数字序列不同。
// 第二次运行:88, 31, 75, 22...(不同了!)
在贪吃蛇游戏中的用途:
// 生成随机食物位置
food.x = rand() % WIDTH; // 随机X坐标
food.y = rand() % HEIGHT; // 随机Y坐标
// 没有播种:食物每次都出现在相同位置
// 播种后:食物每次出现在不同随机位置
注意:一个程序只需写一次即可。
补充:rand()的使用
srand(time(NULL));
// 生成0-99的随机数
int num = rand() % 100;
// 生成1-6的随机数(骰子)
int dice = rand() % 6 + 1;
// 生成a到b之间的随机数
int randomInRange = rand() % (b - a + 1) + a;
在上述头文件中有结构体:用来移动光标位置
typedef struct _COORD{
SHORT X;//X坐标
SHORT Y;//Y坐标
}COORD;
蛇的移动控制分析://接收用户的按键操作,进行移动控制//四种按键影响的都是蛇头,所以下述按键引起的位置变化都影响蛇头的位置,所以加在蛇头上。
W:(4,6)->(4,5) (0,-1)
O:(4,6)->(5,6) (1,0)
S:(4,6)->(4,7) (0,1)
A:(4,6)->(3,6) (-1,0)
从键盘上输入h,这个字符实际上被终端读走了,但我们仍然可以在终端看到h这个字符,这叫做回显。目的只是为了体验感好一点,因为我们可以看到我们输入了什么。而在贪吃蛇这个游戏中,我们不应该让用户输入有回显。
不回显函数:getchar()接收输入会回显;getch()接收输入不回显;
应用类似getchar(),char ch=getch();
光标位置有阻塞等待,即正常输入一个字符,它会在那里闪,等待你输入下一个字符,而贪吃蛇游戏,我们不能让用户输入一个方向蛇走一下,而应该让蛇一直走,所以应该把阻塞等待删掉。
不阻塞函数:kbhit()函数不阻塞接收用户输入
kbhit()函数:
包含头文件:include<conio.h>
功能:以非阻塞方式,检查当前是否有键盘输入
用法:if(kbhit()){}
返回值:若有输入则返回1,否则返回0;
getch()函数:
包含头文件:include<conio.h>
功能:从控制台无回显的取一个字符。
开始游戏:
void playGame()
{
while(判断是否撞墙(一定是在用户开始移动之后,因为我们一开始设的食物和蛇并不相碰){
//重画蛇身initUI() //移动之后的蛇身。现在的蛇头和蛇身坐标都已经计算好了,就是还没有画到屏幕上。
//判断用户是否输入
//接收用户输入,然后才能开始下面的循环
在全局添加kx,ky->根据按键得不同坐标,影响蛇头坐标
//蛇头和身体的碰撞
//蛇与食物的碰撞
//蛇身体移动:前一节给后一节赋值,蛇头受kx,ky的影响
}
}
蛇头和墙壁碰撞:
snake.body[0].x>0&&snake.body[0].x<WIDE
&&snake.body[0].y>0&&snake.body[0].y<HIGH
蛇头和身体碰撞:蛇头的坐标和任意一节身体的坐标完全一致
for(i=1;i<snake.size;i++)
{
if(snake.body[0].x==snake.body[i].x&&snake.body[0].y==snake.body[i].y)
{ 终止游戏;
return;
}
}
蛇头和食物碰撞:
if(snake.body[0].x==snake.food.x&&snake.body[0].y==snake.food.y)
{
蛇身增长:snake.size++;
食物消失:(产生一个新食物,即再产生一个随机数,所以再调用一次产生随机数函数)
initFood()
加分:score+=10;
加速;
}
蛇的移动:
前一节身体给后一节身体赋值(实际上就是把蛇头和身子的下标值赋好)。蛇头按照aswd换算的坐标值进行变换。
snake.body[0].x+=kx; //蛇头坐标根据用户输入修改
snake.body[0].y+=ky;
画墙:
去除蛇尾:
再蛇移动之前,保存蛇尾坐标
在initUI中将蛇尾替换为“ ”
设置光标不可见:
SetConsoleCursorInfo函数:
CONSOLE_CURSOR_INFO结构体,描述终端光标信息,包含两个成员变量
typedef struct _CONSOLE_CURSOR_INFO{
DWORD dwSize;//大小
BOOL bVisible;//是否可见
}CONSOLE_CURSOR_INFO;
CONSOLE_CURSOR_INFO cci;
cci.dwSize=sizeof(cci);
cci.bVisible=FALSE;//假值表示不可见
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cci);
显示分数:playGame结束后,打印全局score值
加速: 全局变量定义:sleepSecond=400;
· 初始值设为400,表示每次移动后等待400毫秒
· 数值越大,蛇移动越慢;数值越小,蛇移动越快
Sleep(sleepSecond);//程序暂停300ms,等待300ms后在进入下一帧。
成功吃食物后: sleepScond-=40;
更多推荐


所有评论(0)