C语言实现贪吃蛇游戏完整教程【最终版】

贪吃蛇是一款经典的游戏,通过C语言实现它可以帮助我们掌握结构体、链表、动态内存管理、Win32 API等核心概念。本文将详细介绍贪吃蛇游戏的完整实现,包含所有源代码,不省略任何关键部分。

目录

游戏设计概述

核心数据结构设计

模块划分

完整代码实现

[头文件 snake.h](#头文件 snake.h)

[源文件 snake.c](#源文件 snake.c)

[测试文件 test.c](#测试文件 test.c)

关键技术解析

[1. 输入缓冲区管理](#1. 输入缓冲区管理)

[2. 链表管理蛇身](#2. 链表管理蛇身)

[3. 坐标系统设计](#3. 坐标系统设计)

[4. 游戏状态机](#4. 游戏状态机)

编译和运行

编译命令

游戏操作说明

总结


游戏设计概述

核心数据结构设计

游戏使用链表来表示蛇身,每个节点代表蛇的一节。同时使用控制结构来管理整个游戏的状态。

模块划分

  1. 游戏初始化:设置控制台窗口,隐藏光标,打印欢迎界面,绘制地图,初始化蛇和食物

  2. 游戏运行:处理用户输入,更新蛇的位置,检测碰撞,更新分数

  3. 游戏结束:释放资源,显示结束信息

完整代码实现

头文件 snake.h

头文件定义了游戏所需的所有数据类型、常量和函数声明。

cpp 复制代码
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define POS_X 24
#define POS_Y 5

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)

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

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

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

// 贪吃蛇控制结构
typedef struct Snake
{
	pSnakeNode _pSnake;       // 指向蛇头的指针
	pSnakeNode _pFood;        // 指向食物节点的指针
	enum DIRECTION _dir;      // 蛇的方向
	enum GAME_STATUS _status; // 游戏状态
	int _food_weight;         // 一个食物的分数
	int _score;               // 总成绩
	int _sleep_time;          // 休息时间,控制蛇的移动速度
}Snake, * pSnake;

// 函数声明
void GameStart(pSnake ps);
void SetPos(short x, short y);
void WelcomeToGame();
void CreateMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);
void GameRun(pSnake ps);
void SnakeMove(pSnake ps);
int NextIsFood(pSnakeNode pn, pSnake ps);
void EatFood(pSnakeNode pn, pSnake ps);
void NoFood(pSnakeNode pn, pSnake ps);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);
void GameEnd(pSnake ps);
void ClearInputBuffer();

源文件 snake.c

源文件包含了所有游戏功能的实现。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
#include <conio.h>  // 用于 _kbhit 和 _getch 函数

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

// 清空输入缓冲区
void ClearInputBuffer()
{
	fflush(stdin);
	while (_kbhit()) {
		_getch();
	}
}

// 欢迎界面
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 CreateMap()
{
	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)
{
	int i = 0;
	pSnakeNode cur = NULL;

	// 创建5个初始蛇身节点
	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 + 2 * i;  // x坐标必须是偶数
		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;        // 初始分数为0
	ps->_food_weight = 10; // 每个食物10分
	ps->_sleep_time = 200; // 移动速度200毫秒
	ps->_status = OK;      // 游戏状态正常
}

// 创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	// 生成随机坐标,x必须是2的倍数
	do
	{
		x = rand() % 53 + 2;  // 2-54范围内
		y = rand() % 25 + 1;  // 1-25范围内
	} while (x % 2 != 0);

	// 检查食物是否与蛇身重叠
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;  // 重叠则重新生成
		}
		cur = cur->next;
	}

	// 创建食物节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}

	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	// 打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

	ps->_pFood = pFood;
}

// 游戏初始化
void GameStart(pSnake ps)
{
	// 清空输入缓冲区,防止残留输入影响
	ClearInputBuffer();
	
	// 设置控制台窗口
	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);

	// 打印欢迎界面
	WelcomeToGame();
	
	// 绘制地图
	CreateMap();
	
	// 初始化蛇
	InitSnake(ps);
	
	// 创建第一个食物
	CreateFood(ps);
}

// 打印帮助信息
void PrintHelpInfo()
{
	SetPos(60, 13);
	wprintf(L"%ls", L"游戏规则:");
	SetPos(60, 15);
	wprintf(L"%ls", L"1.不能穿墙,不能咬到自己");
	SetPos(60, 17);
	wprintf(L"%ls", L"2.用 ↑. ↓ . ← . → 来控制蛇的移动");
	SetPos(60, 19);
	wprintf(L"%ls", L"3.按F3加速,F4减速");
	SetPos(60, 21);
	wprintf(L"%ls", L"4.按ESC退出游戏,按空格暂停游戏");
}

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

// 判断下一个位置是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}

// 吃食物处理
void EatFood(pSnakeNode pn, pSnake ps)
{
	// 将食物节点作为新的蛇头
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	// 释放临时节点
	free(pn);
	pn = NULL;

	// 重新打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	
	// 增加分数
	ps->_score += ps->_food_weight;

	// 重新创建食物
	CreateFood(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 KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||
		ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
	{
		ps->_status = KILL_BY_WALL;
	}
}

// 撞自身检测
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;
	}

	pNextNode->next = NULL;

	// 根据方向计算下一个位置
	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;  // 注意:宽字符占2个位置
		pNextNode->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNextNode->x = ps->_pSnake->x + 2;  // 注意:宽字符占2个位置
		pNextNode->y = ps->_pSnake->y;
		break;
	}

	// 检测下一个位置是否是食物
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}

	// 碰撞检测
	KillByWall(ps);
	KillBySelf(ps);
}

// 游戏运行主循环
void GameRun(pSnake ps)
{
	// 打印帮助信息
	PrintHelpInfo();

	do
	{
		// 每帧开始前清空可能的残留输入
		ClearInputBuffer();
		
		// 显示分数信息
		SetPos(60, 9);
		printf("总分数:%d\n", ps->_score);
		SetPos(60, 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);

	// 游戏结束时清空缓冲区
	ClearInputBuffer();
}

// 游戏结束处理
void GameEnd(pSnake ps)
{
	// 显示游戏结束信息
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		wprintf(L"您主动结束游戏\n");
		break;
	case KILL_BY_WALL:
		wprintf(L"您撞到墙上,游戏结束\n");
		break;
	case KILL_BY_SELF:
		wprintf(L"您撞到了自己,游戏结束\n");
		break;
	}

	// 释放食物节点
	if (ps->_pFood != NULL)
	{
		free(ps->_pFood);
		ps->_pFood = NULL;
	}

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

	ps->_pSnake = NULL;  // 避免野指针
}

测试文件 test.c

测试文件包含游戏的主循环和用户交互逻辑。

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <locale.h>
#include "snake.h"

// 游戏测试逻辑
void test()
{
    char ch = 0;

    do
    {
        // 每次开始新游戏前重置蛇结构体
        Snake snake = { 0 };

        // 游戏流程:开始→运行→结束
        GameStart(&snake);
        GameRun(&snake);
        GameEnd(&snake);

        // 询问是否再来一局
        SetPos(20, 15);
        printf("再来一局吗?(Y/N):");
        fflush(stdout);  // 确保提示信息立即显示

        // 安全的输入处理
        ClearInputBuffer();  // 先清空缓冲区

        // 读取第一个非空白字符
        int input;
        do {
            input = getchar();
        } while (input == ' ' || input == '\t' || input == '\n');

        ch = (char)input;

        // 清空该行剩余的所有字符(包括回车)
        while (getchar() != '\n');

        // 再次清空缓冲区确保干净
        ClearInputBuffer();

    } while (ch == 'Y' || ch == 'y');
    
    SetPos(0, 26);
}

// 主函数
int main()
{
    // 设置本地化环境,支持中文宽字符
    setlocale(LC_ALL, "");
    
    // 初始化随机数种子
    srand((unsigned int)time(NULL));
    
    // 运行游戏测试
    test();

    return 0;
}

关键技术解析

1. 输入缓冲区管理

游戏中最重要的技术点之一是输入缓冲区管理。我们通过以下方式彻底解决了输入残留问题:

  • ClearInputBuffer函数 :结合fflush(stdin)_kbhit()/_getch()彻底清空缓冲区

  • 多位置调用:在游戏开始、每帧开始、游戏结束、用户输入前后都清空缓冲区

  • 安全输入:使用跳过空白字符的方式确保读取到有效输入

2. 链表管理蛇身

使用链表来管理蛇身具有以下优势:

  • 动态增长:蛇吃食物时可以方便地增加节点

  • 高效移动:通过头插法和尾删法实现蛇的移动

  • 内存管理:游戏结束时需要仔细释放所有节点

3. 坐标系统设计

游戏中的坐标系统需要特别注意:

  • 宽字符处理:中文字符占2个普通字符宽度,因此x坐标必须是偶数

  • 边界检测:墙体的坐标范围是固定的(0-56, 0-26)

  • 移动计算:左右移动每次±2,上下移动每次±1

4. 游戏状态机

游戏使用状态枚举来管理不同的游戏状态:

  • OK:正常运行

  • KILL_BY_WALL:撞墙结束

  • KILL_BY_SELF:撞自己结束

  • END_NORMAL:正常退出

编译和运行

编译命令

在Windows环境下使用支持Win32 API的编译器编译:

cpp 复制代码
gcc test.c snake.c -o snake.exe

游戏操作说明

  • 方向控制:↑ ↓ ← → 控制蛇的移动方向

  • 加速减速:F3加速,F4减速

  • 暂停游戏:空格键

  • 退出游戏:ESC键

总结

通过这个贪吃蛇项目的完整实现,我们深入学习了:

  1. 链表数据结构的应用和内存管理

  2. Win32 API在控制台程序中的使用

  3. 输入缓冲区管理的重要性和解决方案

  4. 游戏循环和状态管理机制

  5. 模块化编程的设计思想

这个项目不仅是一个完整的游戏实现,更是C语言综合应用的优秀示例。读者可以通过扩展功能(如关卡设计、障碍物、存档等)来进一步加深对C语言的理解。

所有代码都已完整呈现,没有省略任何关键部分,确保读者可以完全理解并复现整个项目。

相关推荐
weixin_481950352 小时前
跟AI学习用python制作下载器-3
开发语言·python·学习
syker2 小时前
3D游戏引擎Bluely Engine 开发手册
开发语言·3d·游戏引擎
HappRobot2 小时前
Python语言有接口概念吗
开发语言·python
霍理迪2 小时前
js数据类型与运算符
开发语言·前端·javascript
被星1砸昏头2 小时前
自定义操作符高级用法
开发语言·c++·算法
如果曾经拥有2 小时前
医学本体识别 映射-UMLS
开发语言·python
2301_810540732 小时前
python第一次作业
开发语言·python·算法
梦想的旅途22 小时前
基于RPA的多线程企微外部群异步推送架构
java·开发语言·jvm
CoderJia程序员甲2 小时前
GitHub 热榜项目 - 日榜(2026-01-21)
ai·开源·大模型·github·ai教程