C语言实现贪吃蛇游戏

        贪吃蛇作为一款经典的小游戏,不仅陪伴了许多人的童年,也是编程初学者练习编程能力的绝佳项目。通过实现贪吃蛇游戏,我们可以巩固 C 语言基础知识,学习数据结构的应用,以及了解简单的游戏开发逻辑。本文将详细介绍如何使用 C 语言在 Windows 环境下实现一个功能完整的贪吃蛇游戏,包括设计思路、代码实现、常见问题及解决方法。

一、开发环境准备

  • 操作系统:Windows(使用 Win32 API)
  • 开发工具:Visual Studio 或 Dev-C++ 等 C 语言开发环境
  • 基础知识:C 语言基础、链表数据结构基础

二、游戏设计思路

游戏核心功能分析

贪吃蛇游戏的核心功能包括:

  1. 地图绘制:创建游戏边界
  2. 蛇身控制:初始化蛇身和控制蛇的移动
  3. 食物生成:随机生成食物,且不能与蛇身重叠
  4. 碰撞检测:检测蛇是否穿墙和撞到自身
  5. 分数计算:根据吃到的食物类型计算分数
  6. 速度控制:允许玩家加速或减速
  7. 游戏状态管理:包括开始、暂停、继续和结束等状态

游戏流程设计

整个游戏的流程可以分为三个主要阶段:

  1. 游戏初始化(GameStart)

    • 设置控制台窗口大小和标题
    • 隐藏光标
    • 显示欢迎界面
    • 绘制游戏地图
    • 初始化蛇身
    • 生成初始食物
  2. 游戏运行(GameRun)

    • 处理用户输入(方向控制、加速减速、暂停等)
    • 蛇的移动逻辑
    • 食物碰撞检测与处理
    • 墙壁和自身碰撞检测
    • 更新分数显示
    • 控制游戏速度
  3. 游戏结束(GameEnd)

    • 显示游戏结束信息
    • 释放动态分配的内存
    • 询问是否重新开始游戏

三、数据结构设计

在贪吃蛇游戏中,我们需要设计合适的数据结构来表示游戏中的元素:

蛇身节点结构

蛇身由多个节点组成,每个节点包含坐标信息和指向下一个节点的指针:

typedef struct SnakeNode
{
    int x;              //x坐标
    int y;              //y坐标
    struct SnakeNode* next;  //指向下一个节点的指针
} SnakeNode, * pSnakeNode;

食物节点结构

食物节点包含坐标信息和食物类型:

typedef struct FoodNode
{
    int x;                  //x坐标
    int y;                  //y坐标
    enum FOOD_TYPE type;    //食物类型
    struct FoodNode* next;  //指向下一个食物节点的指针
} FoodNode, * pFoodNode;

贪吃蛇整体信息结构

这个结构用于管理整个游戏的状态:

typedef struct Snake
{
    pSnakeNode _pSnake;     //指向蛇头的指针
    pFoodNode _pFood;       //指向食物的指针
    enum DIRECTION _dir;    //蛇的移动方向
    enum GAME_STATUS _status;  //游戏状态
    int _food_weight;       //食物的分数权重
    int _score;             //总分数
    int _sleep_time;        //移动间隔时间(控制速度)
} Snake, * pSnake;

枚举类型定义

使用枚举类型定义方向和游戏状态,使代码更清晰:

//方向枚举
enum DIRECTION
{
    UP = 1,
    DOWN,
    LEFT,
    RIGHT
};

//游戏状态枚举
enum GAME_STATUS
{
    OK,              //正常运行
    KILL_BY_WALL,    //撞墙
    KILL_BY_SELF,    //撞到自己
    END_NORMAL       //正常退出
};

//食物类型枚举
enum FOOD_TYPE
{
    NORMAL,   //普通食物:+10分
    BONUS,    //奖励食物:+20分
    SLOW,     //减速食物:+5分,增加延迟
    FAST      //加速食物:+15分,减少延迟
};

四、核心功能实现

1. 控制台设置

为了创建良好的游戏界面,我们需要对控制台进行一些设置:

//设置光标位置
void SetPos(short x, short y)
{
    COORD pos = { x, y };
    HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(hOutput, pos);
}

//隐藏光标
void HideCursor()
{
    HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(hOutput, &CursorInfo);
    CursorInfo.bVisible = false;  //隐藏光标
    SetConsoleCursorInfo(hOutput, &CursorInfo);
}

在游戏初始化时调用这些函数,设置控制台窗口大小和标题:

system("mode con cols=100 lines=30");  //设置窗口大小
system("title 贪吃蛇");  //设置窗口标题

2. 绘制游戏地图

游戏地图由墙壁组成,我们需要在控制台的相应位置绘制墙壁:

void CreatMap()
{
    int i = 0;
    //上边界
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //下边界
    SetPos(0, 26);
    for (i = 0; i < 29; i++)
    {
        wprintf(L"%lc", WALL);
    }
    //左边界
    for (i = 1; i <= 25; i++)
    {
        SetPos(0, i);
        wprintf(L"%lc", WALL);
    }
    //右边界
    for (i = 1; i <= 25; i++)
    {
        SetPos(56, i);
        wprintf(L"%lc", WALL);
    }
}

其中WALL是一个宏定义,表示墙壁的宽字符:#define WALL L'□'

3. 初始化蛇身

游戏开始时,我们需要创建一个初始长度的蛇身:

void InitSnake(pSnake ps)
{
    pSnakeNode cur = NULL;
    int i = 0;
    //创建5个初始节点
    for (i = 0; i < 5; i++)
    {
        cur = (pSnakeNode)malloc(sizeof(SnakeNode));
        if (cur == NULL)
        {
            perror("InitSnake()::malloc()");
            return;
        }
        cur->next = NULL;
        //初始位置从(POS_X, POS_Y)开始,向右排列
        cur->x = POS_X + i * 2;
        cur->y = POS_Y;
        //头插法插入链表
        if (ps->_pSnake == NULL)
        {
            ps->_pSnake = cur;
        }
        else
        {
            cur->next = ps->_pSnake;
            ps->_pSnake = cur;
        }
    }
    //打印初始蛇身
    cur = ps->_pSnake;
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //初始化蛇的属性
    ps->_dir = RIGHT;       //默认向右
    ps->_score = 0;
    ps->_food_weight = 10;
    ps->_sleep_time = 200;  //初始速度
    ps->_status = OK;
}

4. 生成食物

食物需要随机生成在地图内,且不能与蛇身重叠:

void CreatFood(pSnake ps)
{
    pFoodNode newFood = (pFoodNode)malloc(sizeof(FoodNode));
    if (newFood == NULL)
    {
        perror("CreatFood()::malloc()");
        return;
    }
    int x, y;
    //生成x坐标(2的倍数)和y坐标
again:
    do
    {
        x = rand() % 53 + 2;  //x范围:2~54
        y = rand() % 25 + 1;  //y范围:1~25
    } while (x % 2 != 0);  //确保x是2的倍数,与蛇身对齐
    
    //检查是否与蛇身冲突
    pSnakeNode scur = ps->_pSnake;
    while (scur) 
    {
        if (x == scur->x && y == scur->y) 
        {
            goto again;  //冲突则重新生成
        }
        scur = scur->next;
    }
    
    //检查是否与其他食物冲突
    pFoodNode fcur = ps->_pFood;
    while (fcur)
    {
        if (x == fcur->x && y == fcur->y)
        {
            goto again;  //冲突则重新生成
        }
        fcur = fcur->next;
    }
    
    //随机食物类型
    newFood->type = rand() % 4;
    newFood->x = x;
    newFood->y = y;
    //将新食物添加到链表头部
    newFood->next = ps->_pFood;
    ps->_pFood = newFood;
    
    //绘制食物
    SetPos(x, y);
    switch (newFood->type)
    {
    case NORMAL:
        wprintf(L"%lc", NORMAL_FOOD);  //普通食物
        break;
    case BONUS:  
        wprintf(L"%lc", BONUS_FOOD);   //奖励食物
        break; 
    case SLOW:   
        wprintf(L"%lc", SLOW_FOOD);    //减速食物
        break; 
    case FAST:   
        wprintf(L"%lc", FAST_FOOD);    //加速食物
        break; 
    }
}

5. 蛇的移动逻辑

蛇的移动是游戏的核心,需要处理蛇头的移动、吃到食物后的增长、未吃到食物时尾部的移动等:

void SnakeMove(pSnake ps)
{
    //创建下一个节点(蛇头将要移动到的位置)
    pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (pNextNode == NULL)
    {
        perror("SnakeMove()::malloc()");
        return;
    }
    
    //根据当前方向计算下一个节点的坐标
    switch (ps->_dir)
    {
    case UP:
        pNextNode->x = ps->_pSnake->x;
        pNextNode->y = ps->_pSnake->y - 1;
        break;
    case DOWN:
        pNextNode->x = ps->_pSnake->x;
        pNextNode->y = ps->_pSnake->y + 1;
        break;
    case LEFT:
        pNextNode->x = ps->_pSnake->x - 2;
        pNextNode->y = ps->_pSnake->y;
        break;
    case RIGHT:
        pNextNode->x = ps->_pSnake->x + 2;
        pNextNode->y = ps->_pSnake->y;
        break;
    }
    
    //处理穿墙逻辑
    ThroughWall(pNextNode);
    
    //检测下一个位置是否是食物
    pFoodNode food = NextIsFood(pNextNode, ps);
    if (food)  //吃到食物
    {
        EatFood(pNextNode, ps, food);
    }
    else  //没吃到食物
    {
        NoFood(pNextNode, ps);
    }
    
    //检测是否撞到自己
    KillBySelf(ps);
}

当蛇吃到食物时,需要增加分数,并根据食物类型调整速度:

void EatFood(pSnakeNode pn, pSnake ps, pFoodNode food)
{
    //从链表中删除食物
    pFoodNode prev = NULL;
    pFoodNode cur = ps->_pFood;
    while (cur != NULL && cur != food) 
    {
        prev = cur;
        cur = cur->next;
    }
    if (cur == food) 
    {
        if (prev == NULL) 
        {
            ps->_pFood = food->next;  //食物是头节点
        }
        else 
        {
            prev->next = food->next;  //食物在链表中间
        }
    }
    
    //蛇头新增节点(食物位置)
    pn->next = ps->_pSnake;
    ps->_pSnake = pn;
    
    //打印蛇身
    pSnakeNode scur = ps->_pSnake;
    while (scur)
    {
        SetPos(scur->x, scur->y);
        wprintf(L"%lc", BODY);
        scur = scur->next;
    }
    
    //根据食物类型处理分数和速度
    switch (food->type) 
    {
    case NORMAL: 
        ps->_score += 10; 
        break;
    case BONUS:  
        ps->_score += 20; 
        break;
    case SLOW:   
        ps->_score += 5;  
        if (ps->_sleep_time < 500)
            ps->_sleep_time += 50;  //减速
        break;
    case FAST:   
        ps->_score += 15; 
        if (ps->_sleep_time > 50)
            ps->_sleep_time -= 30;  //加速
        break;
    }
    
    //释放食物节点并生成新食物
    free(food);
    CreatFood(ps);
}

当蛇没有吃到食物时,需要移动尾部:

void NoFood(pSnakeNode pn, pSnake ps)
{
    //头插法添加新节点
    pn->next = ps->_pSnake;
    ps->_pSnake = pn;
    
    //移动尾部
    pSnakeNode cur = ps->_pSnake;
    while (cur->next->next != NULL)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    
    //清除最后一个节点
    SetPos(cur->next->x, cur->next->y);
    printf("  ");
    free(cur->next);
    cur->next = NULL;
}

6. 碰撞检测

游戏需要检测两种碰撞情况:穿墙和撞到自己:

//穿墙功能实现(也可改为撞墙死亡)
void ThroughWall(pSnakeNode pn)
{
    //左墙穿到右墙内侧
    if (pn->x <= 0)
        pn->x = 54;
    //右墙穿到左墙内侧
    else if (pn->x >= 56)
        pn->x = 2;
    //上墙穿到下墙内侧
    else if (pn->y <= 0)
        pn->y = 25;
    //下墙穿到上墙内侧
    else if (pn->y >= 26)
        pn->y = 1;
}

//检测是否撞到自己
void KillBySelf(pSnake ps)
{
    pSnakeNode cur = ps->_pSnake->next;
    while (cur)
    {
        if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
        {
            ps->_status = KILL_BY_SELF;
            break;
        }
        cur = cur->next;
    }
}

7. 处理用户输入

游戏需要响应键盘输入来控制蛇的移动方向、速度等:

void GameRun(pSnake ps)
{
    PrintHelpInfo();  //打印帮助信息
    do
    {
        //更新分数显示
        SetPos(64, 10);
        printf("总分数:%d   \n", ps->_score);
        SetPos(64, 11);
        printf("当前食物分数:%2d   \n", ps->_food_weight);
        
        //处理方向键输入
        if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
            ps->_dir = UP;
        else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
            ps->_dir = DOWN;
        else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
            ps->_dir = LEFT;
        else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
            ps->_dir = RIGHT;
        //处理空格键暂停
        else if (KEY_PRESS(VK_SPACE))
            Pause();
        //处理ESC键退出
        else if (KEY_PRESS(VK_ESCAPE))
            ps->_status = END_NORMAL;
        //处理F3加速
        else if (KEY_PRESS(VK_F3))
        {
            if (ps->_sleep_time > 80)
            {
                ps->_sleep_time -= 30;
                ps->_food_weight += 2;
            }
        }
        //处理F4减速
        else if (KEY_PRESS(VK_F4))
        {
            if (ps->_food_weight > 2)
            {
                ps->_sleep_time += 30;
                ps->_food_weight -= 2;
            }
        }
        
        SnakeMove(ps);  //蛇移动一步
        Sleep(ps->_sleep_time);  //控制游戏速度
        
    } while (ps->_status == OK);  //游戏正常状态则继续循环
}

8. 游戏结束处理

当游戏结束时,需要显示结束信息并释放动态分配的内存:

void GameEnd(pSnake ps)
{
    SetPos(24, 12);
    //根据游戏状态显示相应信息
    switch (ps->_status)
    {
    case END_NORMAL:
        wprintf(L"您主动结束游戏\n");
        break;
    case KILL_BY_SELF:
        wprintf(L"您撞到了自己,游戏结束\n");
        break;
    case KILL_BY_WALL:
        wprintf(L"您撞到墙上,游戏结束\n");
        break;
    }
    
    //释放蛇身链表
    pSnakeNode curSnake = ps->_pSnake;
    while (curSnake)
    {
        pSnakeNode del = curSnake;
        curSnake = curSnake->next;
        free(del);
    }
    
    //释放食物链表
    pFoodNode curFood = ps->_pFood;
    while (curFood)
    {
        pFoodNode del = curFood;
        curFood = curFood->next;
        free(del);
    }
}

五、完整代码结构

整个游戏的代码分为三个文件:

  1. Snake.h:包含结构体定义、枚举类型定义、宏定义和函数声明
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#include <time.h>
#include <locale.h>

#define POS_X 24
#define POS_Y 5

#define WALL L'□'
#define BODY L'●'
#define NORMAL_FOOD L'★'
#define BONUS_FOOD L'☆'
#define SLOW_FOOD L'▲'
#define FAST_FOOD L'▼'

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

//类型的声明

//食物类型(不同效果)
enum FOOD_TYPE
{
	NORMAL,   //普通食物:+10分
	BONUS,    //奖励食物:+20分
	SLOW,     //减速食物:+5分,增加延迟
	FAST      //加速食物:+15分,减少延迟
};

//食物节点(存储坐标和类型)
typedef struct FoodNode
{
	int x;
	int y;
	enum FOOD_TYPE type;
	struct FoodNode* next; //用于多个食物的链表
} FoodNode, * pFoodNode;

//蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
	OK,//正常
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//正常退出
};

//蛇身的节点类型
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

//贪吃蛇的基本信息
typedef struct Snake
{
	pSnakeNode _pSnake;	//指向蛇头的指针
	pFoodNode _pFood;//指向食物的指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status; //游戏的状态
	int _food_weight;//一个食物的分数
	int _score;//总成绩
	int _sleep_time;//休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;

//函数声明

//定位光标位置
void SetPos(short x, short y);

//游戏初始化
void GameStart(pSnake ps);

//打印欢迎界面
void WelcomeToGame();

//绘制地图
void CreatMap();

//创建蛇身
void InitSnake(pSnake ps);

//创建食物
void CreatFood(pSnake ps);

//游戏的运行逻辑
void GameRun(pSnake ps);

//打印帮助信息
void PrintHelpInfo();

//蛇的移动-走一步
void SnakeMove(pSnake ps);

//判断下一个坐标是否是食物,返回吃到的食物
pFoodNode NextIsFood(pSnakeNode pn, pSnake ps);

//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps, pFoodNode pfood);

//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);

//穿墙功能
void ThroughWall(pSnakeNode pn);

//检测蛇是否撞到自己
void KillBySelf(pSnake ps);

//游戏善后的工作
void GameEnd(pSnake ps);

        2.Snake.c:包含所有函数的实现

#include "Snake.h"

//定位光标
void SetPos(short x, short y)
{
	//获取标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定位光标的位置
	COORD pos = { x, y };
	SetConsoleCursorPosition(houtput, pos);
}

//打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(42, 20);
	system("pause");
	system("cls");
	SetPos(25, 14);
	wprintf(L"用↑ ↓ ← → 来控制蛇的移动,按F3加速,F4减速\n");
	SetPos(25, 15);
	wprintf(L"加速能够得到更高的分数\n");

	SetPos(42, 20);
	system("pause");
	system("cls");

}

//绘制地图
void CreatMap()
{
	//上
	int i = 0;
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//下
	SetPos(0, 26);
	for (i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//左
	for (i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}

	//右
	for (i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

}

//创建蛇身
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		//采用头插法插入链表

		//空链表
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		//非空链表
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//打印蛇身
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//设置蛇的基本属性
	ps->_dir = RIGHT;//默认向右
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//单位是毫秒
	ps->_status = OK;

}

//创建食物
void CreatFood(pSnake ps)
{
	pFoodNode newFood = (pFoodNode)malloc(sizeof(FoodNode));
	if (newFood == NULL)
	{
		perror("CreatFood()::malloc()");
		return;
	}

	int x;
	int y;
	//生成的x是2的倍数
	//x:2~54
	//y:1~25

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	//检查是否与蛇身冲突
	pSnakeNode scur = ps->_pSnake;
	while (scur) 
	{
		if (x == scur->x && y == scur->y) 
		{
			goto again;
		}
		scur = scur->next;
	}

	//食物的坐标冲突
	pFoodNode fcur = ps->_pFood;
	while (fcur)
	{
		if (x == fcur->x && y == fcur->y)
		{
			goto again;
		}
		fcur = fcur->next;
	}

	//随机食物类型(0~3对应四种食物)
	newFood->type = rand() % 4;
	newFood->x = x;
	newFood->y = y;
	newFood->next = ps->_pFood;//插入链表头部

	//将新食物添加到链表头部
	newFood->next = NULL;
	if (ps->_pFood == NULL) 
	{
		ps->_pFood = newFood;
	}
	else 
	{
		newFood->next = ps->_pFood;
		ps->_pFood = newFood;
	}

	//绘制食物
	SetPos(x, y);
	switch (newFood->type)
	{
	case NORMAL:
		wprintf(L"%lc", NORMAL_FOOD); //普通食物
		break;
	case BONUS:  
		wprintf(L"%lc", BONUS_FOOD); //奖励食物
		break; 
	case SLOW:   
		wprintf(L"%lc", SLOW_FOOD); //减速食物
		break; 
	case FAST:   
		wprintf(L"%lc", FAST_FOOD); //加速食物
		break; 
	}

}

//游戏初始化
void GameStart(pSnake ps)
{
	//0.先设置窗口的大小,再隐藏光标
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");
	//隐藏光标
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;	//隐藏控制台光标
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态

	//1.打印环境界面和功能介绍
	WelcomeToGame();

	//2.绘制地图
	CreatMap();

	//3.创建蛇
	InitSnake(ps);

	//生成3个初始食物
	for (int i = 0; i < 3; i++)
	{
		CreatFood(ps);
	}

}

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 15);
	wprintf(L"%ls", L"用↑ ↓ ← → 来控制蛇的移动");
	SetPos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");
	SetPos(64, 17);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
}

//暂停游戏
void Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//判断下一个坐标是否是食物,返回吃到的食物
pFoodNode NextIsFood(pSnakeNode pn, pSnake ps)
{
	pFoodNode cur = ps->_pFood;
	while (cur)
	{
		if (pn->x == cur->x && pn->y == cur->y)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps, pFoodNode food)
{
	//从链表中删除食物
	pFoodNode prev = NULL;
	pFoodNode cur = ps->_pFood;

	//查找食物节点及其前驱节点
	while (cur != NULL && cur != food) 
	{
		prev = cur;
		cur = cur->next;
	}

	//如果找到食物节点
	if (cur == food) 
	{
		if (prev == NULL) 
		{
			//食物是链表头节点
			ps->_pFood = food->next;
		}
		else 
		{
			//食物在链表中间
			prev->next = food->next;
		}
	}

	//蛇头新增节点(食物位置)
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	//打印蛇
	pSnakeNode scur = ps->_pSnake;
	while (scur)
	{
		SetPos(scur->x, scur->y);
		wprintf(L"%lc", BODY);
		scur = scur->next;
	}

	// 不同食物的效果
	switch (food->type) 
	{
	case NORMAL: 
		ps->_score += 10; 
		break;
	case BONUS:  
		ps->_score += 20; 
		break;
	case SLOW:   
		ps->_score += 5;  
		if (ps->_sleep_time < 500)
		{
			ps->_sleep_time += 50;
		}
		break;
	case FAST:   
		ps->_score += 15; 
		if (ps->_sleep_time > 50)
		{
			ps->_sleep_time -= 30;
		}
		break;
	}

	//释放食物的节点
	free(food);
	food = NULL;

	//生成新食物
	CreatFood(ps);

}

//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	//头插法
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next != NULL)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//把最后一个节点打印成空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	//释放最后一个节点
	free(cur->next);

	//把倒数第二个节点的地址置为空
	cur->next = NULL;
}

//穿墙功能
void ThroughWall(pSnakeNode pn)
{

	//左墙穿到右墙内侧(x=0 → x=54,右墙为x=56)
	if (pn->x <= 0)
	{
		pn->x = 54;
	}
	//右墙穿到左墙内侧(x=56 → x=2,左墙为x=0)
	else if (pn->x >= 56)
	{
		pn->x = 2;
	}
	//上墙穿到下墙内侧(y=0 → y=25,下墙为y=26)
	else if (pn->y <= 0)
	{
		pn->y = 25;
	}
	//下墙穿到上墙内侧(y=26 → y=1,上墙为y=0)
	else if (pn->y >= 26)
	{
		pn->y = 1;
	}
	
}

//检测蛇是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

//蛇的移动-走一步
void SnakeMove(pSnake ps)
{
	//创建一个节点,表示蛇即将到达的下一个节点
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}

	switch (ps->_dir)
	{
	case UP:
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_pSnake->x + 2;
		pNextNode->y = ps->_pSnake->y;
		break;
	}

	//蛇穿墙检测
	ThroughWall(pNextNode);

	//检测下一个坐标是否是食物
	pFoodNode food = NextIsFood(pNextNode, ps);
	//吃到食物
	if (food)
	{
		EatFood(pNextNode, ps, food);
		pNextNode = NULL;
	}
	//没有吃到食物
	else
	{
		NoFood(pNextNode, ps);
	}

	//检测蛇是否撞到自己
	KillBySelf(ps);
}

//游戏的运行逻辑
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//立即刷新分数显示
		SetPos(64, 10);
		printf("总分数:%d   \n", ps->_score);
		SetPos(64, 11);
		printf("当前食物分数:%2d   \n", ps->_food_weight);
		if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//暂停游戏
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}
		SnakeMove(ps);//蛇走一步的过程
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
}

//游戏善后的工作
void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		wprintf(L"您主动结束游戏\n");
		break;
	case KILL_BY_SELF:
		wprintf(L"您撞到了自己,游戏结束\n");
		break;
	case KILL_BY_WALL:
		wprintf(L"您撞到墙上,游戏结束\n");
		break;
	}

	//释放蛇身的链表
	pSnakeNode curSnake = ps->_pSnake;
	while (curSnake)
	{
		pSnakeNode del = curSnake;
		curSnake = curSnake->next;
		free(del);
	}

	//释放食物链表
	pFoodNode curFood = ps->_pFood;
	while (curFood)
	{
		pFoodNode del = curFood;
		curFood = curFood->next;
		free(del);
	}
}

        3.test.c:包含主函数和游戏的整体流程控制

#include "Snake.h"

void test()
{
	int ch = 0;
	do
	{
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//结束游戏-善后工作
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一局吗?(Y/N);");
		ch = getchar();
		while (getchar() != '\n');
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();

	return 0;
}

六、开发过程中遇到的问题及解决方法

  1. 宽字符显示问题

    问题:在控制台中无法正确显示中文和特殊字符(如蛇身、食物、墙壁)。

    解决方法:使用宽字符函数(如 wprintf)并设置本地环境:

setlocale(LC_ALL, "");  //在main函数中设置

同时,在输出宽字符时使用L前缀,如L"欢迎来到贪吃蛇小游戏"

  1. 蛇身坐标对齐问题

    问题:蛇身移动时出现错位或与食物不对齐的情况。

    解决方法:确保蛇身和食物的 x 坐标都是 2 的倍数,因为宽字符在控制台中占用两个字符位置。

  2. 内存泄漏问题

    问题:游戏运行时间过长后可能出现内存泄漏。

    解决方法:在游戏结束时,遍历蛇身链表和食物链表,释放所有动态分配的节点。

  3. 按键响应不灵敏问题

    问题:键盘输入有时不响应或响应延迟。

    解决方法:使用GetAsyncKeyState函数检测按键状态,并优化游戏循环逻辑。

  4. 蛇移动逻辑错误

    问题:蛇在移动时出现身体断裂或显示异常。

    解决方法:仔细检查蛇的移动逻辑,特别是在处理吃到食物和未吃到食物两种情况下的链表操作,确保每个节点都正确更新和显示。

总结

        本文详细介绍了使用 C 语言实现贪吃蛇游戏的全过程,从游戏设计思路到具体代码实现,再到开发过程中遇到的问题及解决方法。希望这篇文章能帮助想学习游戏开发的初学者快速上手,实现自己的第一个小游戏。贪吃蛇游戏还有很多可以扩展的地方,例如增加不同难度级别、添加背景音乐、实现排行榜功能等。读者可以在现有代码的基础上进行扩展,进一步提升自己的编程能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值