花了点时间,用C语言实现了一个贪吃蛇小游戏,开发工具是VS2010。
以下是代码代码链接,我放在了github上,需要的可自行下载:
https://github.com/zhengzebin525/basic_algorithm
本文会着重于思路讲解,最后会放上完整代码。
先来看看运行后的效果图:
总体来说,贪吃蛇小游戏大体需要实现以下几个最主要的函数功能:
1、游戏区域展示;
2、运用链表操作初始化小蛇
3、在游戏区域内随机出现方块食物
4、利用API函数获取用户的按键状态
5、根据按键状态控制小蛇的不同移动方向,小蛇移动中一旦吃到食物,分数增加;
6、游戏的错误函数,比如小蛇吃到自己,撞墙等;
那么接下来,就是对以上六大部分进行详细说明。
//游戏区域展示
void Area_Show()
{
int num;
for(num=0;num<70;num+=2) //如果改成num++的话,上下 边框会出现错误
{
Goto_Coord(num,0);
printf("■");
Goto_Coord(num,26);
printf("■");
}
for(num=0;num<=26;num++)
{
Goto_Coord(0,num);
printf("■");
Goto_Coord(70,num);
printf("■");
}
}
这是个比较简单的功能,利用方块“■”来划定游戏的区域,而Goto_Coord(num,0)用于配置文本显示的坐标,这是个自定义函数,用到了GetStdHandle()和SetConsoleCursorPosition()这两个window自带的API函数,前一个用来获取标准输入/标准输出/标准错误中句柄,后一个用来定位要显示的坐标。
句柄分别有:标准输入:STD_INPUT_HANDLE、标准输出:STD_OUTPUT_HANDLE、标准错误:STD_ERROR_HANDLE。
很明显,因为是要在屏幕上显示,所以是获取标准输出STD_OUTPUT_HANDLE的句柄。
//定位输出坐标 void Goto_Coord(int x,int y) { COORD pos; pos.X = x; pos.Y = y; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle,pos); } |
void Snake_Init()
{
snake *newnode = (snake *)malloc(sizeof(struct node));
newnode->x = 10; //赋予初始坐标
newnode->y = 10; //赋予初始坐标
newnode->next = NULL;
for(int i=1;ix = 10 + 2*i;
head->y = 10;
head->next = newnode;
newnode = head;
}
while(newnode!= NULL)
{
Goto_Coord(newnode->x,newnode->y);
printf("■");
newnode = newnode->next;
}
}
初始化小蛇是用若干个方块组成的,所以需要存储每个方块的数据比如内存信息,显示坐标,并用链表将方块联系起来,最后一并打印出来,从而呈现方块小蛇的样子。
为了跟游戏区域对应,因此横坐标是偶数单位增加,纵坐标是奇数单位增加。
因此肯定会有一个结构体,来存储小蛇每个方块的数据信息:
typedef struct node { int x; //横坐标 int y; //纵坐标 int color; //方块颜色 struct node *next; }snake; |
int Food_Show()
{
int color_num;
snake *temp1 = NULL,*temp2 = NULL;
food1 = (snake *)malloc(sizeof(struct node));
food2 = (snake *)malloc(sizeof(struct node));
if(food1 == NULL || food2 == NULL)
{
return FALSE;
}
srand((unsigned)time(NULL)); //产生随机数必须要的种子
//产生第一个食物
while((food1->x%2)!= 0 || food1->x == 70) //食物x坐标不是偶数,跟边界没有对齐
{
food1->x = rand()%70 + 2;
}
do
{
food1->y = rand()%26 + 1;
}
while(food1->y == 26);
temp1 = head;
while(temp1 != NULL)
{
if(food1->x == temp1->x && food1->y == temp1->y)
{
free(food1);
Food_Show();
}
temp1 = temp1->next;
}
Goto_Coord(food1->x,food1->y);
color_num = rand()%6;
food1->color = color_table[color_num];
color(food1->color);
printf("■");
//产生第二个食物
while((food2->x%2)!= 0 || food2->x == 70) //食物x坐标不是偶数,跟边界没有对齐
{
food2->x = rand()%70 + 2;
}
do
{
food2->y = rand()%26 + 1;
}
while(food2->y == 26);
temp2 = head;
while(temp2 != NULL)
{
if(food2->x == temp2->x && food2->y == temp2->y)
{
free(food2);
Food_Show();
}
temp2 = temp2->next;
}
Goto_Coord(food2->x,food2->y);
color_num = rand()%6;
food2->color = color_table[color_num];
color(food2->color);
printf("■");
}
每个食物的出现,都是一小块内存,因此malloc()申请内存是必须的步骤,此处需要注意的是随机的坐标必须位于游戏区域坐标内,并且不能跟区域边界方块重叠,也不能跟小蛇身上的每一块“蛇身”方块重叠,而且我给每个食物随机分配了不同的颜色,使得小蛇吃到不同的颜色就会有不一样的分数加成。
//游戏控制
void Game_Control()
{
Goto_Coord(80,18);
color(0x07);
printf("当前获得分数:%d\n",total_score);
//判断按下了哪一个按键,并且避免上一次的按键状态与其方向相反
if(GetAsyncKeyState(VK_UP) && Key_Status != Key_Down)
Key_Status = Key_Up;
else if(GetAsyncKeyState(VK_DOWN) && Key_Status != Key_Up)
Key_Status = Key_Down;
else if(GetAsyncKeyState(VK_LEFT) && Key_Status != Key_Right)
Key_Status = Key_Left;
else if(GetAsyncKeyState(VK_RIGHT) && Key_Status != Key_Left)
Key_Status = Key_Right;
else if(GetAsyncKeyState(VK_SPACE))
{
Save_Status = Key_Status; //当按下了暂停健的时候,暂停前的运动方向状态需要保存起来
Key_Status = Key_Space;
}
else if(GetAsyncKeyState(VK_ESCAPE ))
Key_Status = Key_Esc;
}
这一部分点主要在GetAsyncKeyState(int vkey)这个window的API函数,用于判断某个物理按键是处于按下状态(非0)还是没按下状态(0),而vkey 是256个虚拟按键中的一个,返回值是一个非0的数值。
常用的大概是以下几个:
VK_SHIFT Shift键 VK_LSHIFT 左Shift键 VK_RSHIFT 右Shift键 VK_CONTROL Ctrl键 VK_LCONTROL 左Ctrl键 VK_RCONTROL 右Ctril键 VK_MENU Alt键 VK_LMENU 左Alt键 VK_RMENU 右Alt键 VK_LBUTTON 鼠标左键 VK_RBUTTON 鼠标右键 VK_ESCAPE ESC键 VK_RETURN回车键 VK_TABTAB键 VK_SPACE空格键 VK_UP↑键 VK_DOWN↓键 VK_LEFT←键 VK_RIGHT→键 |
比如if(GetAsyncKeyState(VK_UP) && Key_Status != Key_Down) ,前一个条件判断用户是否按下了↑键,而后一个条件则判断上一次的按键状态是不是↓键,这么做是为了避免小蛇由向上移动突变为向下移动的情况出现。
//蛇的移动
void Snake_Move()
{
snake *nextnode = (snake *)malloc(sizeof(struct node));
snake *temp = NULL;
if(Key_Status == Key_Up)
{
nextnode->x = head->x;
nextnode->y = (head->y) - 1;
//Goto_Coord(80,28);
//printf("蛇头head的坐标 %d,%d",head->x,head->y);
if((nextnode->x == food1->x && nextnode->y == food1->y) ||
(nextnode->x == food2->x && nextnode->y == food2->y)) //移动过程中吃到了食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Food_Score(food1->color);
free(food1);
Food_Show();
}
else //移动过程没有遇到食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp->next->next != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Goto_Coord(temp->next->x,temp->next->y);
printf(" ");
free(temp->next);
temp->next = NULL;
}
}
此处只截取了小蛇向上移动的状态代码,小蛇一旦运动起来,第一步就需要配置下一步要显示的方块的坐标,然后判断此方块有没有与食物的坐标重叠,重叠了就说明吃到了食物,获得分数加成;没重叠的话,就把新显示的方块作为新的蛇头添加进小蛇链表,蛇尾最后的方块从链表上释放消除掉,然后重新打印小蛇链表,这样就出现了小蛇向上移动一步的现象。
利用Food_Score(food1->color)函数进行分数加成,这是个自定义函数,根据吃到的食物方块颜色进行判断,不同的颜色获取不同的分数加成。
void Food_Score(int food_color) { switch (food_color) { case red: total_score += 50;break; case orange: total_score += 40;break; case green: total_score += 30;break; case blue: total_score += 20;break; case purple: total_score += 10;break; } } |
//穿墙错误函数
void Through_Walls()
{
if(head->x == 0 || head->x == 70 ||
head->y == 0 || head->y == 26)
{
system("cls");
Goto_Coord(30,10);
printf("抱歉,事故判断,小蛇死亡撞墙\n");
Goto_Coord(30,12);
color(0x07);
printf("当前获得分数:%d\n",total_score);
exit(0);
}
}
只要判断蛇头方块的坐标与游戏区域边界方块坐标重叠,那就是撞墙的,撞得死死的。
//咬到自己错误函数
void Bit_Oneself()
{
snake *temp;
temp = head->next;
while(temp!=NULL)
{
if(head->x == temp->x && head->y == temp->y)
{
system("cls");
Goto_Coord(30,10);
printf("抱歉,事故判断,小蛇咬死了自己\n");
Goto_Coord(30,12);
color(0x07);
printf("当前获得分数:%d\n",total_score);
exit(0);
}
temp = temp->next;
}
}
只要判断蛇头方块的坐标与蛇身任何一个方块坐标重叠,那小蛇就是咬到自己了,咬得死死的。
以下是完整代码,直接copy能用的。
#ifndef __GLUTTONOUS_SNAKE_H
#define __GLUTTONOUS_SNAKE_H
#include
#include
#include
#include
#include
#define FALSE 0
#define TRUE 1
#define snake_len_init 3 //蛇的初始化长度为3
#define Key_Up 1 //↑按键状态
#define Key_Down 2 //↓按键状态
#define Key_Left 3 //←按键状态
#define Key_Right 4 //→按键状态
#define Key_Space 5
#define Key_Esc 6
#define red FOREGROUND_RED
#define orange FOREGROUND_RED|FOREGROUND_RED|FOREGROUND_GREEN
#define green FOREGROUND_GREEN
#define blue FOREGROUND_BLUE
#define purple FOREGROUND_RED|FOREGROUND_BLUE
typedef struct node
{
int x;
int y;
int color;
struct node *next;
}snake;
void Goto_Coord(int x,int y);
void color(int color);
void Area_Show();
void Side_Show();
void Snake_Init();
int Food_Show();
void Game_Init();
void Food_Score(int food_color);
void Game_Control();
void Snake_Move();
void Pause();
void Through_Walls();
void Bit_Oneself();
#endif
#include "Gluttonous_Snake.h"
int snake_x,snake_y;
int food_score = 10; //每个食物的分数
int total_score = 0; //获得的总分数
int highest_score = 0; //最高分
int Key_Status = Key_Right; //蛇一开始是向右行进
int Save_Status;
snake *head;
snake *newnode; //中间变量,放在while(1)中会内存溢出
snake *food1,*food2;
//颜色表
int color_table[6] = {
red, //红
orange, //橙
green, //绿
blue, //蓝
purple //紫
};
//定位输出坐标
void Goto_Coord(int x,int y)
{
COORD pos;
pos.X = x;
pos.Y = y;
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle,pos);
}
//设置文本颜色
void color(int color)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(handle,color);
}
//游戏区域展示
void Area_Show()
{
int num;
for(num=0;num<70;num+=2) //如果改成num++的话,上下 边框会出现错误
{
Goto_Coord(num,0);
printf("■");
Goto_Coord(num,26);
printf("■");
}
for(num=0;num<=26;num++)
{
Goto_Coord(0,num);
printf("■");
Goto_Coord(70,num);
printf("■");
}
}
//侧面说明展示
void Side_Show()
{
Goto_Coord(80,5);
printf("欢迎来到贪吃蛇游戏\n");
printf("\n");
Goto_Coord(80,7);
printf("↑ ↓ ← → 是上下左右移动\n");
printf("\n");
Goto_Coord(80,9);
printf("规则1:不能穿墙,不能吃自己\n");
printf("\n");
Goto_Coord(80,11);
printf("规则2:ESC退出,SPACE是暂停\n");
printf("\n");
Goto_Coord(80,13);
printf("规则2:吃的食物越多,分数越高\n");
printf("\n");
Goto_Coord(80,15);
printf("规则3:其中会出现吃到了也没分数的假食物\n");
printf("\n");
}
void Snake_Init()
{
snake *newnode = (snake *)malloc(sizeof(struct node));
newnode->x = 10; //赋予初始坐标
newnode->y = 10; //赋予初始坐标
newnode->next = NULL;
for(int i=1;ix = 10 + 2*i;
head->y = 10;
head->next = newnode;
newnode = head;
}
while(newnode!= NULL)
{
Goto_Coord(newnode->x,newnode->y);
printf("■");
newnode = newnode->next;
}
}
int Food_Show()
{
int color_num;
snake *temp1 = NULL,*temp2 = NULL;
food1 = (snake *)malloc(sizeof(struct node));
food2 = (snake *)malloc(sizeof(struct node));
if(food1 == NULL || food2 == NULL)
{
return FALSE;
}
srand((unsigned)time(NULL)); //产生随机数必须要的种子
//产生第一个食物
while((food1->x%2)!= 0 || food1->x == 70) //食物x坐标不是偶数,跟边界没有对齐
{
food1->x = rand()%70 + 2;
}
do
{
food1->y = rand()%26 + 1;
}
while(food1->y == 26);
temp1 = head;
while(temp1 != NULL)
{
if(food1->x == temp1->x && food1->y == temp1->y)
{
free(food1);
Food_Show();
}
temp1 = temp1->next;
}
Goto_Coord(food1->x,food1->y);
color_num = rand()%6;
food1->color = color_table[color_num];
color(food1->color);
printf("■");
//Goto_Coord(80,21);
//printf("食物1坐标 %d,%d",food1->x,food1->y);
//产生第二个食物
while((food2->x%2)!= 0 || food2->x == 70) //食物x坐标不是偶数,跟边界没有对齐
{
food2->x = rand()%70 + 2;
}
do
{
food2->y = rand()%26 + 1;
}
while(food2->y == 26);
temp2 = head;
while(temp2 != NULL)
{
if(food2->x == temp2->x && food2->y == temp2->y)
{
free(food2);
Food_Show();
}
temp2 = temp2->next;
}
Goto_Coord(food2->x,food2->y);
color_num = rand()%6;
food2->color = color_table[color_num];
color(food2->color);
printf("■");
//Goto_Coord(80,23);
//printf("食物2坐标 %d,%d",food2->x,food2->y);
}
//游戏界面初始化
void Game_Init()
{
system("title 贪吃蛇");
Goto_Coord(40,10);
printf("欢迎来到贪吃蛇小游戏\n");
system("pause");
system("cls");
Goto_Coord(40,10);
printf("希望能带给你良好的游戏体验\n");
system("pause");
system("cls");
Area_Show(); //游戏区域初始化
Side_Show(); //侧面说明初始化
Snake_Init(); //蛇初始化
Food_Show(); //产生方块食物
}
void Food_Score(int food_color)
{
switch (food_color)
{
case red:
total_score += 50;break;
case orange:
total_score += 40;break;
case green:
total_score += 30;break;
case blue:
total_score += 20;break;
case purple:
total_score += 10;break;
}
}
//游戏控制
void Game_Control()
{
Goto_Coord(80,18);
color(0x07);
printf("当前获得分数:%d\n",total_score);
//判断按下了哪一个按键,并且避免上一次的按键状态与其方向相反
if(GetAsyncKeyState(VK_UP) && Key_Status != Key_Down)
Key_Status = Key_Up;
else if(GetAsyncKeyState(VK_DOWN) && Key_Status != Key_Up)
Key_Status = Key_Down;
else if(GetAsyncKeyState(VK_LEFT) && Key_Status != Key_Right)
Key_Status = Key_Left;
else if(GetAsyncKeyState(VK_RIGHT) && Key_Status != Key_Left)
Key_Status = Key_Right;
else if(GetAsyncKeyState(VK_SPACE))
{
Save_Status = Key_Status; //当按下了暂停健的时候,暂停前的运动方向状态需要保存起来
Key_Status = Key_Space;
}
else if(GetAsyncKeyState(VK_ESCAPE ))
Key_Status = Key_Esc;
}
//蛇的移动
void Snake_Move()
{
snake *nextnode = (snake *)malloc(sizeof(struct node));
snake *temp = NULL;
if(Key_Status == Key_Up)
{
nextnode->x = head->x;
nextnode->y = (head->y) - 1;
//Goto_Coord(80,28);
//printf("蛇头head的坐标 %d,%d",head->x,head->y);
if((nextnode->x == food1->x && nextnode->y == food1->y) ||
(nextnode->x == food2->x && nextnode->y == food2->y)) //移动过程中吃到了食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Food_Score(food1->color);
free(food1);
Food_Show();
}
else //移动过程没有遇到食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp->next->next != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Goto_Coord(temp->next->x,temp->next->y);
printf(" ");
free(temp->next);
temp->next = NULL;
}
}
if(Key_Status == Key_Down)
{
nextnode->x = head->x;
nextnode->y = (head->y) + 1;
//Goto_Coord(80,28);
//printf("蛇头head的坐标 %d,%d",head->x,head->y);
if((nextnode->x == food1->x && nextnode->y == food1->y) ||
(nextnode->x == food2->x && nextnode->y == food2->y)) //移动过程中吃到了食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Food_Score(food1->color);
free(food1);
Food_Show();
}
else //移动过程没有遇到食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp->next->next != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Goto_Coord(temp->next->x,temp->next->y);
printf(" ");
free(temp->next);
temp->next = NULL;
}
}
if(Key_Status == Key_Left)
{
nextnode->x = (head->x) - 2;
nextnode->y = head->y;
//Goto_Coord(80,28);
//printf("蛇头head的坐标 %d,%d",head->x,head->y);
if((nextnode->x == food1->x && nextnode->y == food1->y) ||
(nextnode->x == food2->x && nextnode->y == food2->y)) //移动过程中吃到了食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Food_Score(food1->color);
free(food1);
Food_Show();
}
else //移动过程没有遇到食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp->next->next != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Goto_Coord(temp->next->x,temp->next->y);
printf(" ");
free(temp->next);
temp->next = NULL;
}
}
if(Key_Status == Key_Right)
{
nextnode->x = (head->x) + 2;
nextnode->y = head->y;
//Goto_Coord(80,28);
//printf("蛇头head的坐标 %d,%d",head->x,head->y);
if((nextnode->x == food1->x && nextnode->y == food1->y) ||
(nextnode->x == food2->x && nextnode->y == food2->y)) //移动过程中吃到了食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Food_Score(food1->color);
free(food1);
Food_Show();
}
else //移动过程没有遇到食物
{
nextnode->next = head;
head = nextnode;
temp = head;
while(temp->next->next != NULL)
{
Goto_Coord(temp->x,temp->y);
printf("■");
temp = temp->next;
}
Goto_Coord(temp->next->x,temp->next->y);
printf(" ");
free(temp->next);
temp->next = NULL;
}
}
while(Key_Status == Key_Space) //按空格健暂停
{
Sleep(300);
if(!(GetAsyncKeyState(VK_SPACE)))
{
Pause(); //暂停函数
break;
}
}
if(Key_Status == Key_Esc)
{
system("cls");
Goto_Coord(40,10);
printf("您已结束游戏!\n");
}
Through_Walls(); //判断是否撞墙
Bit_Oneself(); //判断是否咬到了自己
}
void Pause()
{
while(1)
{
system("title 暂停中····");
Sleep(300);
if(GetAsyncKeyState(VK_SPACE)) //第二次按空格的时候,解除暂停状态
{
Sleep(200);
Key_Status = Save_Status;
break;
}
}
}
//穿墙错误函数
void Through_Walls()
{
if(head->x == 0 || head->x == 70 ||
head->y == 0 || head->y == 26)
{
system("cls");
Goto_Coord(30,10);
printf("抱歉,事故判断,小蛇死亡撞墙\n");
Goto_Coord(30,12);
color(0x07);
printf("当前获得分数:%d\n",total_score);
exit(0);
}
}
//咬到自己错误函数
void Bit_Oneself()
{
snake *temp;
temp = head->next;
while(temp!=NULL)
{
if(head->x == temp->x && head->y == temp->y)
{
system("cls");
Goto_Coord(30,10);
printf("抱歉,事故判断,小蛇咬死了自己\n");
Goto_Coord(30,12);
color(0x07);
printf("当前获得分数:%d\n",total_score);
exit(0);
}
temp = temp->next;
}
}
#include "Gluttonous_Snake.h"
int sleep_time = 200;
int main()
{
Game_Init(); //游戏界面初始化
while(1)
{
system("title 游戏中····");
Game_Control(); //游戏控制,判断按下的是哪一个按键
Sleep(sleep_time); //延时200ms
Snake_Move();
}
}
当然,这个小游戏个人觉得还有很多自己的设想没有实现,以后会抽出时间来实现它。
总结:1、在初始化过程中,如果有某一参数只初始化依次,该参数直接使用常量,如果使用变量,还需要占用一定的存储空间,浪费内存;
2、在链表操作中,用最少的步骤完成操作,不要花里胡哨,链表逻辑要清晰,多看几遍,确保无误;
3、递归函数越少用越好,在一个函数中最好不要超过一个递归函数;
4、如果需要在界面上显示肉眼可见的运动,延时操作必不可少,毕竟系统的运行速度太快了;