引言
"我们看到的只是结果,只有深入内存,才能 看见真相。" "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()的伏笔。"