C语言_贪吃蛇

引言

"我们看到的只是结果,只有深入内存,才能 看见真相。" "What we see is just the result; only by diving into memory can we see the truth." ------ 献给那个曾欺骗过你的int i。


1. 贪吃蛇伪代码


2. Greedy_Snake.h

c 复制代码
#include <stdio.h>
#include <windows.h>
#include <conio.h>
#include <time.h>

#define Width 20
#define Height 20

//坐标信息
typedef struct Point Point;
struct Point
{
	int x;
	int y;
};

//蛇的结构设计
typedef struct Snake Snake;
struct Snake
{
	Point Body[100];
	int length;
	int dir;
};

//贪吃蛇游戏逻辑底层设计
typedef struct Game Game;
struct Game
{
	//食物
	Point food;
	//蛇结构
	Snake snake;
	//分数
	int score;
	//速度
	int speed;
};

void InitSnake(Game* g_game);
void HideCursor();
void draw(Game* g_game);
void Input(Game* g_game);
void SnakeMove(Game* g_game);
void EatFood(Game* g_game);
int CheckCollision(Game* g_game);
void GameOver(Game* g_game);

3. Greedy_Snake.c

3.1 初始化

c 复制代码
void InitSnake(Game* g_game)
{
	//第一次食物刷新可以固定
	g_game->food.x = 10;
	g_game->food.y = 5;
	//初始化蛇的速度和长度
	g_game->snake.length = 1;
	g_game->snake.dir = 0;
	//初始化分数和速度
	g_game->score = 0;
	g_game->speed = 200;
	//初始化蛇头坐标
	g_game->snake.Body[0].x = Width / 2;
	g_game->snake.Body[0].y = Height / 2;
}

3.2 隐藏光标

c 复制代码
// --- 工具函数:隐藏光标 (为了美观) ---
void HideCursor()
{
	CONSOLE_CURSOR_INFO curInfo;
	curInfo.dwSize = 1;
	curInfo.bVisible = FALSE;
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorInfo(handle, &curInfo);
}

3.3 渲染画面

c 复制代码
//光标定位
void GotoXY(int x, int y)
{
	COORD pos;
	pos.X = x;
	pos.Y = y;
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}

//渲染画面
void draw(Game* g_game)
{
	//光标定位
	GotoXY(0, 0);
	printf("=== 等比例极简版贪吃蛇(ConsoleSnake)===\n");
	printf("操作:W/A/S/D 控制方向,X 退出\n");
	//绘制顶部围墙 -> 棋盘宽度 + 2
	for (int i = 0; i < Width + 2; i++)
	{
		printf("##");
	}
	printf("\n");
	//绘制中间核心部分
	for (int i = 0; i < Height; i++)
	{
		printf("##");
		for (int j = 0; j < Width; j++)
		{
			int flag = 0;
			//判断是否是蛇头
			if (g_game->snake.Body[0].x == j && g_game->snake.Body[0].y == i)
			{
				flag = 1;
				printf("O ");
			}
			//判断是否是食物
			else if (g_game->food.x == j && g_game->food.y == i)
			{
				flag = 1;
				printf("F ");
			}
			//判断是否是蛇身,需要遍历蛇身跟当前坐标进行比较
			else
			{
				for (int k = 1; k < g_game->snake.length; k++)
				{
					if (g_game->snake.Body[k].x == j && g_game->snake.Body[k].y == i)
					{
						flag = 1;
						printf("o ");
					}
				}
			}
			//如果什么都不是,则输出空字符
			if (!flag)
			{
				printf("  ");
			}
		}
		printf("##\n");
	}
	//绘制顶部围墙 -> 棋盘宽度 + 2
	for (int i = 0; i < Width + 2; i++)
	{
		printf("##");
	}
	printf("\n");
	//分数
	GotoXY(Width + 35, 8);
	printf("得分:%d,每个食物分数:10", g_game->score);
	//蛇身长度
	GotoXY(Width + 35, 9);
	printf("蛇身长度:%d", g_game->snake.length);
	//提示词
	GotoXY(Width + 35, 11);
	printf("提示词:");
	GotoXY(Width + 35, 12);
	printf("不可以穿墙,不能咬到自己");
	GotoXY(Width + 35, 13);
	printf("用 W . S . A . D分别控制蛇的方向");
	GotoXY(Width + 35, 14);
	printf("作者:XingC");
}

3.4 交互处理

c 复制代码
void Input(Game* g_game)
{
	if (_kbhit())
	{
		char ch = _getch();
		switch (ch)
		{
		case 'W':
		case 'w':
			//防止自己咬自己
			if (g_game->snake.dir != 2)
			{
				g_game->snake.dir = 1;
			}
			break;
		case 'S':
		case 's':
			if (g_game->snake.dir != 1)
			{
				g_game->snake.dir = 2;
			}
			break;
		case 'A':
		case 'a':
			if (g_game->snake.dir != 4)
			{
				g_game->snake.dir = 3;
			}
			break;
		case 'D':
		case 'd':
			if (g_game->snake.dir != 3)
			{
				g_game->snake.dir = 4;
			}
			break;
		default:
			break;
		}
	}
}

3.5 蛇移动

c 复制代码
void SnakeMove(Game* g_game)
{
	//从蛇尾依次获取前一个蛇身的地址
	for (int i = g_game->snake.length - 1; i >= 1; i--)
	{
		g_game->snake.Body[i].x = g_game->snake.Body[i - 1].x;
		g_game->snake.Body[i].y = g_game->snake.Body[i - 1].y;
	}
	switch (g_game->snake.dir)
	{
	//W
	case 1:
		(g_game->snake.Body[0].y)--;
		break;
	//S
	case 2:
		(g_game->snake.Body[0].y)++;
		break;
	//A
	case 3:
		(g_game->snake.Body[0].x)--;
		break;
	//D
	case 4:
		(g_game->snake.Body[0].x)++;
		break;
	default:
		break;
	}
}

3.6 吃食物逻辑

c 复制代码
void EatFood(Game* g_game)
{
	if (g_game->food.x == g_game->snake.Body[0].x && g_game->food.y == g_game->snake.Body[0].y)
	{
		//重新刷新食物
		while (1)
		{
			int len = 0;
			g_game->food.x = rand() % Width;
			g_game->food.y = rand() % Height;
			for (int i = 0; i < g_game->snake.length; i++)
			{
				if (g_game->snake.Body[i].x != g_game->food.x || g_game->snake.Body[i].y != g_game->food.y)
				{
					len++;
				}
			}
			if (len == g_game->snake.length)
			{
				break;
			}
		}
		g_game->score += 10;
		(g_game->snake.length)++;
	}
}

3.7 判断是否产生碰撞

c 复制代码
//是否撞到围墙
int Is_Wall(Game* g_game)
{
	//我们需要检查 区间是否在 x < 0 && x >= Width
	if (g_game->snake.Body[0].x < 0 || g_game->snake.Body[0].x >= Width)
	{
		return 1;
	}
	if (g_game->snake.Body[0].y < 0 || g_game->snake.Body[0].y >= Height)
	{
		return 1;
	}
	return 0;
}

//是否撞到自己
int Is_Oneself(Game* g_game)
{
	for (int i = 1; i < g_game->snake.length; i++)
	{
		if (g_game->snake.Body[0].x == g_game->snake.Body[i].x && g_game->snake.Body[0].y == g_game->snake.Body[i].y)
		{
			return 1;
		}
	}
	return 0;
}

int CheckCollision(Game* g_game)
{
	//检查是否碰撞围墙
	if (Is_Wall(g_game))
	{
		return 1;
	}
	//检查是否碰撞自己蛇身
	if (Is_Oneself(g_game))
	{
		return 1;
	}
	return 0;
}

3.8 游戏结束标语

c 复制代码
void GameOver(Game* g_game)
{
	GotoXY(0, Height + 5);
	printf("您最终的成绩是:%d\n", g_game->score);
	printf("游戏结束,请按下任意按键即可退出...\n");
	system("pause>nul");
}

4. test.c

c 复制代码
#include "Greedy_Snake.h"

void test()
{
	Game g_game;
	//初始化操作
	InitSnake(&g_game);
	//隐藏光标
	HideCursor();
	srand((unsigned int)time(NULL));
	while (1)
	{
		//渲染画面
		draw(&g_game);
		Input(&g_game);
		SnakeMove(&g_game);
		EatFood(&g_game);
		if (CheckCollision(&g_game))
		{
			break;
		}
		Sleep(g_game.speed);
	}
	GameOver(&g_game);
}

int main()
{
	test();
	return 0;
}

5. C语言控制台贪吃蛇(Console Snake) 项目复盘

5.1 架构设计

  • "上帝结构体" (God Struct):

    • 采用了struct Game封装所有游戏数据(蛇Snake、食物food、分数score、速度speed)。

    • 优势: 内存布局连续,利于指针传递(Game* g_game),也极大地方便了后续的逆向分析(基址查找)。

  • 模块化编程:

    • .h文件: 定义数据结构与接口。

    • .c文件: 实现具体逻辑。

    • main.c: 仅负责调度(初始化-> 循环-> 结束)。

5.2 核心算法(Core Algorithms)

  • 蛇身移动(Movement) ------ 逆向迭代:

    • 原理: Bodyi = Bodyi-1

    • 逻辑: 从蛇尾(length-1)开始,将前一节的坐标赋值给后一节,直到蛇头。

    • 关键: 必须倒序遍历,否则数据会被覆盖。

  • 防反转锁(Input Lock) :

    • 在Input()中加入了逻辑互斥判断(如:当前向右dir=4,则禁止输入左A)。

    • 作用:防止蛇头直接180 度掉头导致"咬到脖子"自杀。

  • 防重叠生成(Anti-Overlap Spawn):

    • 在EatFood()中使用while(1)循环+ 遍历蛇身检测。

    • 作用:确保随机生成的食物坐标不会落在蛇的身体上。

5.3 踩坑与调试经验(Debugging & Lessons)

这是本项目最宝贵的财富:

  • 变量遮蔽(Variable Shadowing) ------ "隐身蛇"事件:

    • 现象: 蛇身数据正常更新,但屏幕上只显示蛇头或断断续续。

    • 原因: 在draw()函数的外层循环用了int i,内层循环再次定义了int i,导致内层Bodyi的索引被行号覆盖。

    • 修正: 内层循环变量改为k,与外层i区分开。

  • 返回值陷阱(Undefined Behavior):

    • 现象: CheckCollision未撞墙时可能随机返回真,导致游戏秒退。

    • 原因: C语言函数若无显式return,会返回寄存器遗留的垃圾值。

    • 修正: 在函数末尾补全return 0;。

  • 边界判定(Boundary Check):

    • 逻辑: 数组下标从0 开始,所以右墙和下墙的判定条件必须是>= Width(而非>)。

    • 自撞判定: 遍历必须从i=1开始(避开蛇头本身),且不能-1(必须包含蛇尾)。

5.4 渲染引擎(Rendering Engine)

  • 基于栅格的重绘(Grid-based Redraw):

    • 每一帧都通过system("cls")或光标跳转覆盖(本项目使用GotoXY)来重绘整个地图。

    • 通过双重循环遍历Height和Width,逐格判断当前坐标是墙、蛇头、蛇身还是食物。


结语:"每一个完美的return 0;,都是下一个int main()的伏笔。"

相关推荐
LDR00618 小时前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
Luminous.19 小时前
C语言--day30
c语言·开发语言
玖玥拾19 小时前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
謓泽20 小时前
C语言不是语法,是通往机器的地图。
c语言·开发语言
不会C语言的男孩20 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
2601_9516438821 小时前
C语言长文整理,关键字和数据类型
c语言·数据类型·关键字·嵌入式开发·格式化输出
m0_547486661 天前
《C#语言程序设计与实践》 全套PPT课件
c语言·c#·c语言程序设计
✎ ﹏梦醒͜ღ҉繁华落℘1 天前
编程基础 --高内聚,低耦合
c语言·单片机
QK_001 天前
C语言 static 关键字三大作用
c语言·开发语言
隔窗听雨眠1 天前
C语言函数递归从入门到精通(下):性能优化与工程实践
c语言·算法·性能优化