1.简介
小游戏实现是基于C语言和链表,基本思想是通过改变控制台打印地区,在不同地方打印相应字符
2.初始化工作
- 显示首页提示信息
/*
功能:首页显示提示信息
参数:无
返回值:无
*/
void show_first_page(void)
{
printf("/******欢迎进入游戏******/\n");
printf("/******按空格开始游戏******/\n");
printf("/******w s a d控制蛇移动方向******/\n");
return;
}
- 播放音乐
/*
功能:实现音乐播放
参数:无
返回值:0,1
*/
bool play_music(void)
{
bool b = PlaySound(TEXT("a.wav"), NULL, SND_FILENAME | SND_ASYNC);
return b;
}
- 检测用户是否按下开始键(空格)
/*
功能:用于检测用户是否按下开始键(空格键)
参数:无
返回值:无
*/
void case_key(void)
{
char key;
while (true)
{
key = _getch();
if (key == ' ') {
return;
}
}
}
- 按下开始键后便停止播放音乐
/*
功能:停止音乐播放
参数:无
返回值:无
*/
void stop_play_music(void)
{
PlaySound(NULL, NULL, NULL);
return;
}
- 绘制游戏背景
在头文件中定义了#define map_width 40
,#define map_height 20
.此处先用for循环画出第0行,再用两层for循环画出中间部分,最后画出第19行,在此需要注意:一个■
对应2个空格
/*
功能:画背景
参数:无
返回值:无
*/
void draw_background()
{
for (int i = 0; i < map_width; i++) {
printf("■");
}
printf("\n");
for (int i = 1; i < map_height - 1; i++) {
for (int j = 0; j < map_width; j++) {
if (j == 0 || j == map_width - 10 || j == map_width - 1) {
printf("■");
}
else {
printf(" ");//此处为2个空格
}
}
printf("\n");
}
for (int i = 0; i < map_width; i++) {
printf("■");
}
printf("\n");
return;
}
绘制结果如下:
- 移动光标位置
/*
功能:控制光标移动到指定位置
参数:
参数1:坐标x
参数2:坐标y
返回值:无
*/
void move_to_cursor(int x, int y)
{
COORD pos = { 2 * x, y };
HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(output, pos);
}
- 显示当前难度和当前得分
/*
功能:在指定位置显示提示信息,此处为难度和得分
参数:无
返回值:无
*/
void show_message()
{
move_to_cursor(32, 8);
printf("当前难度:%d\n", level);
move_to_cursor(32, 9);
printf("当前得分:%d\n", score);
}
结果如下:
3.关于蛇的设计
- 首先是设计蛇,此处采用链表的方式保存蛇,故数据结构设计如下:
其中x,y分别保存蛇每个节点坐标,在定义一个结构体指针指向下一个蛇节点
typedef struct snake {
int x, y;
struct snake* next;
}snake, *snake_p;
- 创建蛇
/*
功能:创建一个长度为2的蛇
参数:无
返回值:无
*/
void create_a_snake()
{
snake_p snake_node;
snake_head = (snake_p)malloc(sizeof(snake));
if (snake_head == NULL) {
puts("创建蛇失败");
return;
}
//蛇头节点无数据
snake_head->x = 0;
snake_head->y = 0;
snake_head->next = NULL;
snake_tail = snake_head;
for (int i = 0; i < 2; i++) {
snake_node = (snake_p)malloc(sizeof(snake));
if (snake_node == NULL) {
puts("创建蛇失败");
return;
}
//采用尾插法插入
snake_node->x = 6 - i;
snake_node->y = 2;
snake_node->next = NULL;
snake_tail->next = snake_node;
snake_tail = snake_node;
}
//保留蛇尾坐标
tail_area[0].x = snake_tail->x;
tail_area[0].y = snake_tail->y;
show_snake();
return;
}
- 随机产生食物
/*
功能:随机产生一个食物
参数:无
返回值:无
*/
void create_a_food()
{
srand((int)time(0));
//随机数取值范围:x = [1, 28],y = [1, 18]
int x = rand() % 28 + 1;
int y = rand() % 18 + 1;
bool b_flag = true;
while (1) {
snake_body = snake_head;
while (snake_body->next) {
//如果产生的食物位置与蛇身位置重合则重新产生食物
if (snake_body->next->x == x && snake_body->next->y == y) {
x = rand() % 28 + 1;
y = rand() % 18 + 1;
b_flag = false;
break;
}
snake_body = snake_body->next;
}
if (b_flag) {
break;
}
}
//保存食物位置,便于判断蛇是否吃到食物
a_food.x = x;
a_food.y = y;
move_to_cursor(a_food.x, a_food.y);
printf("■");
show_message();
return;
}
- 按键检测
此处用于检测按键方向:上下左右,判断设移动方向;
/*
功能:检测按键方向,控制蛇移动方位
参数:无
返回值:无
*/
void watch_move_key()
{
if (GetAsyncKeyState(VK_UP) && 0x8000) {
direction = UP;
}
if (GetAsyncKeyState(VK_DOWN) && 0x8000) {
direction = DOWN;
}
if (GetAsyncKeyState(VK_LEFT) && 0x8000) {
direction = LEFT;
}
if (GetAsyncKeyState(VK_RIGHT) && 0x8000) {
direction = RIGHT;
}
if (GetAsyncKeyState(VK_RETURN) && 0x0D) {
while (1) {
if (GetAsyncKeyState(VK_RETURN) && 0x0D) {
break;
}
}
}
}
- 清除蛇,防止重复显示蛇
/*
功能:清除蛇
参数:无
返回值:无
*/
void kill_snake()
{
snake_body = snake_head;
while (snake_body->next) {
move_to_cursor(snake_body->next->x, snake_body->next->y);
printf(" ");
snake_body = snake_body->next;
}
move_to_cursor(0, 25);
}
- 显示蛇
/*
功能:显示蛇
参数:无
返回值:无
*/
void show_snake()
{
snake_body = snake_head;
while (snake_body->next) {
move_to_cursor(snake_body->next->x, snake_body->next->y);
printf("■");
snake_body = snake_body->next;
}
tail_area[1].x = tail_area[0].x;
tail_area[1].y = tail_area[0].y;
tail_area[0].x = snake_tail->x;
tail_area[0].y = snake_tail->y;
move_to_cursor(0, 25);
}
- 判断蛇是否吃到食物
/*
功能:判断蛇是否吃到食物
参数:无
返回值:无
*/
void if_get_food()
{
//当吃到食物后
if (snake_head->next->x == a_food.x && snake_head->next->y == a_food.y) {
score++;
if (score % 5 == 0) {
level++;
pause_time -= 20000000;
}
snake_p new_body = (snake_p)malloc(sizeof(snake));
if (new_body == NULL) {
puts("游戏运行错误");
return;
}
new_body->x = tail_area[1].x;
new_body->y = tail_area[1].y;
//尾插法将新节点插入到蛇尾
new_body->next = NULL;
snake_tail->next = new_body;
snake_tail = new_body;
snake_body = snake_head;
create_a_food();//重新产生食物
kill_snake();
show_snake();
}
return;
}
- 判断蛇是否撞墙
/*
功能:判断蛇是否撞墙
参数:无
返回值:无
*/
void if_touch_wall()
{
snake_body = snake_head;
if (snake_body->next->x == 0 || snake_body->next->x == 29 || snake_body->next->y == 0 || snake_body->next->y == 19) {
move_to_cursor(10, 20);
puts("你撞到了墙哈->x<-");
free_all();
exit(0);
}
}
- 判断蛇是否咬到自己
void if_touch_yourself()
{
snake_body = snake_head->next;
while (snake_body->next) {
if (snake_head->next->x == snake_body->next->x && snake_head->next->y == snake_body->next->y) {
move_to_cursor(10, 20);
puts("你撞到了你自己哈->x<-");
free_all();
exit(0);
}
snake_body = snake_body->next;
}
}
- 开始游戏
/*
功能:开始游戏
参数:无
返回值:无
*/
void gameing()
{
while (1) {
for (int i = 0; i < pause_time; i++) {
}
kill_snake();//清除蛇
watch_move_key();//按键检测
snake_body = snake_head;
int newx = 0, newy = 0;
switch (direction)
{
case UP:
newx = snake_body->next->x;
newy = snake_body->next->y - 1;
break;
case DOWN:
newx = snake_body->next->x;
newy = snake_body->next->y + 1;
break;
case LEFT:
newx = snake_body->next->x - 1;
newy = snake_body->next->y;
break;
case RIGHT:
newx = snake_body->next->x + 1;
newy = snake_body->next->y;
break;
default:
break;
}
snake_food p1, p2;
snake_body = snake_head;
p1.x = snake_body->x;
p1.y = snake_body->y;
p2.x = 0;
p2.y = 0;
while (snake_body->next) {
p2.x = p1.x;
p2.y = p1.y;
p1.x = snake_body->next->x;
p1.y = snake_body->next->y;
snake_body->next->x = p2.x;
snake_body->next->y = p2.y;
snake_body = snake_body->next;
}
snake_body = snake_head;
snake_body->next->x = newx;
snake_body->next->y = newy;
show_snake();
if_get_food();
if_touch_wall();
if_touch_yourself();
}
}
4.一些注意点
- 对蛇的操作基本就是对链表的一些插入和遍历操作
- 为什么要保存上次蛇尾位置
(4,0) | (5,0) | (6,0) | (7,0) | (8,0) |
(4,1) | (5,1) | (6,1) | (7,1) | (8,1) |
(4,2) | (5,2) | (6,2) | (7,2) | (8,2) |
(4,3) | (5,3) | (6,3) | (7,3) | (8,3) |
(4,4) | (5,4) | (6,4) | (7,4) | (8,4) |
(4,5) | (5,5) | (6,5) | (7,5) | (8,5) |
(4,6) | (5,6) | (6,6) | (7,6) | (8,6) |
- 问题产生原因分析:假设蛇头坐标为(7,2),食物坐标为(7,4).在移动到(7,3)时,(7,3)坐标被头插到链表中,成为0节点,(7,2)和(7,1)向后移动一位,成为节点1和节点2,当移动到(7,4)时此时蛇第0和第1个节点为(7,4),(7,3),之后再进行判断是否吃到食物,很明显此处吃到了,所以就将(7,4)插入到蛇头,原来的节点0(7,4),节点1(7,3)向后移动1为,故此时蛇为(7,4),(7,4),(7,3),(7,2),然而正确的应该为(7,4),(7,3),(7,2),(7,1).所以不能直接将食物坐标(7,4)直接插入到链表中,否则会重复打印,即虽然吃到食物,蛇长变为4,但显示却为3
- 解决方案:既然不能直接插入食物坐标,那我们可以在尾部直接插入(7,1),即上次的蛇尾坐标.
- 保存蛇尾坐标问题:假设开始蛇坐标为(7,2),(7,1),(7,0),此时蛇尾坐标为(7,0);蛇向下移动,坐标为(7,3),(7,2),(7,1),此时蛇尾坐标为(7,1);继续向下移动,坐标为(7,4),(7,3),(7,2),蛇尾坐标为(7,2),此时满足蛇尾插入条件,但如果插入蛇尾坐标后会变成(7,4),(7,3),(7,2),(7,2),仍然会重复显示,所以我们需要在蛇吃到食物时在尾部插入上次蛇尾坐标
- 解决方案:定义一个结构体数组,一个保存当前蛇尾坐标
tail_area[0]
,一个保存上次蛇尾坐标tail_area[1]
,每当蛇显示一次就tail_area[0]的值给tail_area[1],即
void show_snake()
{
...
//保存上次蛇尾位置
tail_area[1].x = tail_area[0].x;
tail_area[1].y = tail_area[0].y;
tail_area[0].x = snake_tail->x;
tail_area[0].y = snake_tail->y;
...
}
在吃到食物后采用尾插法插入tail_area[1]
中数据,即
void if_get_food()
{
//当吃到食物后
if (snake_head->next->x == a_food.x && snake_head->next->y == a_food.y) {
...
snake_p new_body = (snake_p)malloc(sizeof(snake));
new_body->x = tail_area[1].x;
new_body->y = tail_area[1].y;
//尾插法将新节点插入到蛇尾
new_body->next = NULL;
snake_tail->next = new_body;
snake_tail = new_body;
snake_body = snake_head;
...
}
return;
}