贪吃蛇作为一款经典的小游戏,不仅陪伴了许多人的童年,也是编程初学者练习编程能力的绝佳项目。通过实现贪吃蛇游戏,我们可以巩固 C 语言基础知识,学习数据结构的应用,以及了解简单的游戏开发逻辑。本文将详细介绍如何使用 C 语言在 Windows 环境下实现一个功能完整的贪吃蛇游戏,包括设计思路、代码实现、常见问题及解决方法。
一、开发环境准备
- 操作系统:Windows(使用 Win32 API)
- 开发工具:Visual Studio 或 Dev-C++ 等 C 语言开发环境
- 基础知识:C 语言基础、链表数据结构基础
二、游戏设计思路
游戏核心功能分析
贪吃蛇游戏的核心功能包括:
- 地图绘制:创建游戏边界
- 蛇身控制:初始化蛇身和控制蛇的移动
- 食物生成:随机生成食物,且不能与蛇身重叠
- 碰撞检测:检测蛇是否穿墙和撞到自身
- 分数计算:根据吃到的食物类型计算分数
- 速度控制:允许玩家加速或减速
- 游戏状态管理:包括开始、暂停、继续和结束等状态
游戏流程设计
整个游戏的流程可以分为三个主要阶段:
-
游戏初始化(GameStart):
- 设置控制台窗口大小和标题
- 隐藏光标
- 显示欢迎界面
- 绘制游戏地图
- 初始化蛇身
- 生成初始食物
-
游戏运行(GameRun):
- 处理用户输入(方向控制、加速减速、暂停等)
- 蛇的移动逻辑
- 食物碰撞检测与处理
- 墙壁和自身碰撞检测
- 更新分数显示
- 控制游戏速度
-
游戏结束(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);
}
}
五、完整代码结构
整个游戏的代码分为三个文件:
- 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;
}
六、开发过程中遇到的问题及解决方法
-
宽字符显示问题
问题:在控制台中无法正确显示中文和特殊字符(如蛇身、食物、墙壁)。
解决方法:使用宽字符函数(如 wprintf)并设置本地环境:
setlocale(LC_ALL, ""); //在main函数中设置
同时,在输出宽字符时使用L前缀,如L"欢迎来到贪吃蛇小游戏"。
-
蛇身坐标对齐问题
问题:蛇身移动时出现错位或与食物不对齐的情况。
解决方法:确保蛇身和食物的 x 坐标都是 2 的倍数,因为宽字符在控制台中占用两个字符位置。
-
内存泄漏问题
问题:游戏运行时间过长后可能出现内存泄漏。
解决方法:在游戏结束时,遍历蛇身链表和食物链表,释放所有动态分配的节点。
-
按键响应不灵敏问题
问题:键盘输入有时不响应或响应延迟。
解决方法:使用
GetAsyncKeyState函数检测按键状态,并优化游戏循环逻辑。 -
蛇移动逻辑错误
问题:蛇在移动时出现身体断裂或显示异常。
解决方法:仔细检查蛇的移动逻辑,特别是在处理吃到食物和未吃到食物两种情况下的链表操作,确保每个节点都正确更新和显示。
总结
本文详细介绍了使用 C 语言实现贪吃蛇游戏的全过程,从游戏设计思路到具体代码实现,再到开发过程中遇到的问题及解决方法。希望这篇文章能帮助想学习游戏开发的初学者快速上手,实现自己的第一个小游戏。贪吃蛇游戏还有很多可以扩展的地方,例如增加不同难度级别、添加背景音乐、实现排行榜功能等。读者可以在现有代码的基础上进行扩展,进一步提升自己的编程能力。
31万+

被折叠的 条评论
为什么被折叠?



