1. 引言
很多初学者学完 C 语言基础后都会问:C 语言能写小游戏吗?怎么写?
答案是肯定的。C 语言虽然不像 Python、JavaScript 那样有丰富的游戏框架,但它足够底层、足够灵活,完全可以用标准库写出贪吃蛇、扫雷、俄罗斯方块、井字棋等经典小游戏。而且用 C 写游戏,能让你更深刻地理解数据结构、逻辑控制、输入输出这些核心概念。
本文以贪吃蛇为例,带你从零开始,用纯 C 语言(只依赖标准库)写一个能在终端里运行的小游戏。全程手把手,代码可直接复制运行。
2. 准备工作
2.1 开发环境
写 C 语言小游戏,只需要一个 C 编译器即可:
- Windows:安装 Dev-C++ 或 Visual Studio,也可以使用 MinGW 的 gcc。
- Linux / macOS:系统自带 gcc,直接终端编译即可。
2.2 验证环境
先写一个最简单的程序,确认编译环境正常:
c
#include <stdio.h>
int main() {
printf("Hello, C Game!\n");
return 0;
}
编译运行:
bash
gcc test.c -o test
./test
能输出 Hello, C Game! 就说明环境没问题。
3. 游戏设计思路
写游戏之前,先想清楚几个问题:
3.1 游戏地图
贪吃蛇在一个固定大小的矩形区域里活动。我们用二维数组(或一维数组模拟)来表示地图:
0表示空地1表示蛇身2表示食物
3.2 蛇的表示
蛇是一串连续的格子。我们用两个数组分别记录蛇身每个格子的 x 和 y 坐标,数组下标 0 是蛇头。
3.3 游戏循环
所有小游戏的核心都是游戏循环:
- 接收玩家输入(方向键)
- 更新游戏状态(蛇移动、吃食物)
- 判断游戏结束条件(撞墙、撞自己)
- 绘制画面
- 短暂延时,控制速度
#mermaid-svg-fTcuDInM5cS4QrnM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fTcuDInM5cS4QrnM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fTcuDInM5cS4QrnM .error-icon{fill:#552222;}#mermaid-svg-fTcuDInM5cS4QrnM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fTcuDInM5cS4QrnM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fTcuDInM5cS4QrnM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fTcuDInM5cS4QrnM .marker.cross{stroke:#333333;}#mermaid-svg-fTcuDInM5cS4QrnM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fTcuDInM5cS4QrnM p{margin:0;}#mermaid-svg-fTcuDInM5cS4QrnM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fTcuDInM5cS4QrnM .cluster-label text{fill:#333;}#mermaid-svg-fTcuDInM5cS4QrnM .cluster-label span{color:#333;}#mermaid-svg-fTcuDInM5cS4QrnM .cluster-label span p{background-color:transparent;}#mermaid-svg-fTcuDInM5cS4QrnM .label text,#mermaid-svg-fTcuDInM5cS4QrnM span{fill:#333;color:#333;}#mermaid-svg-fTcuDInM5cS4QrnM .node rect,#mermaid-svg-fTcuDInM5cS4QrnM .node circle,#mermaid-svg-fTcuDInM5cS4QrnM .node ellipse,#mermaid-svg-fTcuDInM5cS4QrnM .node polygon,#mermaid-svg-fTcuDInM5cS4QrnM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fTcuDInM5cS4QrnM .rough-node .label text,#mermaid-svg-fTcuDInM5cS4QrnM .node .label text,#mermaid-svg-fTcuDInM5cS4QrnM .image-shape .label,#mermaid-svg-fTcuDInM5cS4QrnM .icon-shape .label{text-anchor:middle;}#mermaid-svg-fTcuDInM5cS4QrnM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fTcuDInM5cS4QrnM .rough-node .label,#mermaid-svg-fTcuDInM5cS4QrnM .node .label,#mermaid-svg-fTcuDInM5cS4QrnM .image-shape .label,#mermaid-svg-fTcuDInM5cS4QrnM .icon-shape .label{text-align:center;}#mermaid-svg-fTcuDInM5cS4QrnM .node.clickable{cursor:pointer;}#mermaid-svg-fTcuDInM5cS4QrnM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fTcuDInM5cS4QrnM .arrowheadPath{fill:#333333;}#mermaid-svg-fTcuDInM5cS4QrnM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fTcuDInM5cS4QrnM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fTcuDInM5cS4QrnM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fTcuDInM5cS4QrnM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fTcuDInM5cS4QrnM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fTcuDInM5cS4QrnM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fTcuDInM5cS4QrnM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fTcuDInM5cS4QrnM .cluster text{fill:#333;}#mermaid-svg-fTcuDInM5cS4QrnM .cluster span{color:#333;}#mermaid-svg-fTcuDInM5cS4QrnM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fTcuDInM5cS4QrnM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fTcuDInM5cS4QrnM rect.text{fill:none;stroke-width:0;}#mermaid-svg-fTcuDInM5cS4QrnM .icon-shape,#mermaid-svg-fTcuDInM5cS4QrnM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fTcuDInM5cS4QrnM .icon-shape p,#mermaid-svg-fTcuDInM5cS4QrnM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fTcuDInM5cS4QrnM .icon-shape .label rect,#mermaid-svg-fTcuDInM5cS4QrnM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fTcuDInM5cS4QrnM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fTcuDInM5cS4QrnM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fTcuDInM5cS4QrnM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
游戏初始化
接收输入
更新蛇位置
是否吃到食物?
蛇变长 + 生成新食物
蛇正常移动
是否撞墙或撞自己?
游戏结束
绘制画面
4. 完整代码实现
下面就是完整的贪吃蛇代码。为了简单易懂,我们使用 wasd 控制方向,按 q 退出。
c
#include <stdio.h>
#include <stdlib.h>
#include <conio.h> // Windows 下用 _kbhit 和 _getch
#include <windows.h> // Windows 下用 Sleep
#define WIDTH 20
#define HEIGHT 15
// 蛇身坐标
int snakeX[100], snakeY[100];
int snakeLen = 3;
// 食物坐标
int foodX, foodY;
// 当前方向:0上 1下 2左 3右
int dir = 3;
// 游戏是否结束
int gameOver = 0;
// 初始化游戏
void initGame() {
// 蛇初始位置:在中间,水平向右
snakeX[0] = WIDTH / 2;
snakeY[0] = HEIGHT / 2;
snakeX[1] = snakeX[0] - 1;
snakeY[1] = snakeY[0];
snakeX[2] = snakeX[0] - 2;
snakeY[2] = snakeY[0];
// 生成第一个食物
foodX = rand() % WIDTH;
foodY = rand() % HEIGHT;
}
// 生成新食物(避开蛇身)
void generateFood() {
int onSnake;
do {
foodX = rand() % WIDTH;
foodY = rand() % HEIGHT;
onSnake = 0;
for (int i = 0; i < snakeLen; i++) {
if (snakeX[i] == foodX && snakeY[i] == foodY) {
onSnake = 1;
break;
}
}
} while (onSnake);
}
// 绘制画面
void draw() {
system("cls"); // 清屏
// 上边框
for (int i = 0; i < WIDTH + 2; i++) printf("#");
printf("\n");
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
if (x == 0) printf("#"); // 左边框
int isSnake = 0;
for (int i = 0; i < snakeLen; i++) {
if (snakeX[i] == x && snakeY[i] == y) {
isSnake = 1;
break;
}
}
if (isSnake) {
printf("O");
} else if (foodX == x && foodY == y) {
printf("*");
} else {
printf(" ");
}
if (x == WIDTH - 1) printf("#"); // 右边框
}
printf("\n");
}
// 下边框
for (int i = 0; i < WIDTH + 2; i++) printf("#");
printf("\n");
printf("得分: %d\n", (snakeLen - 3) * 10);
printf("方向: W上 S下 A左 D右 | Q退出\n");
}
// 读取输入
void input() {
if (_kbhit()) {
char ch = _getch();
switch (ch) {
case 'w': case 'W': if (dir != 1) dir = 0; break;
case 's': case 'S': if (dir != 0) dir = 1; break;
case 'a': case 'A': if (dir != 3) dir = 2; break;
case 'd': case 'D': if (dir != 2) dir = 3; break;
case 'q': case 'Q': gameOver = 1; break;
}
}
}
// 更新游戏逻辑
void update() {
// 新蛇头位置
int newX = snakeX[0];
int newY = snakeY[0];
switch (dir) {
case 0: newY--; break;
case 1: newY++; break;
case 2: newX--; break;
case 3: newX++; break;
}
// 撞墙判断
if (newX < 0 || newX >= WIDTH || newY < 0 || newY >= HEIGHT) {
gameOver = 1;
return;
}
// 撞自己判断(新蛇头是否碰到身体)
for (int i = 0; i < snakeLen; i++) {
if (snakeX[i] == newX && snakeY[i] == newY) {
gameOver = 1;
return;
}
}
// 吃到食物
int ateFood = (newX == foodX && newY == foodY);
// 蛇身整体前移
for (int i = snakeLen - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] = newX;
snakeY[0] = newY;
// 吃到食物则变长并生成新食物
if (ateFood) {
snakeLen++;
generateFood();
}
}
int main() {
srand((unsigned)time(NULL));
initGame();
while (!gameOver) {
draw();
input();
update();
Sleep(150); // 控制速度,单位毫秒
}
printf("\n游戏结束!最终得分: %d\n", (snakeLen - 3) * 10);
return 0;
}
注意:上面的代码使用了
<conio.h>和<windows.h>,这是 Windows 专属 的头文件。如果你在 Linux / macOS 上运行,需要改用<unistd.h>的usleep()和终端原始模式来读取按键,见下文「跨平台改造」。
5. 代码讲解
5.1 全局变量
我们用全局变量保存蛇、食物、方向等状态,这样各个函数都能直接访问,代码更简洁:
snakeX[]/snakeY[]:蛇身每个格子的坐标,snakeX[0]是蛇头。snakeLen:蛇的长度。foodX/foodY:食物坐标。dir:当前移动方向。gameOver:游戏结束标志。
5.2 游戏循环
main() 里的 while (!gameOver) 就是游戏循环,每一轮做四件事:
draw()绘制画面input()读取按键update()更新逻辑Sleep(150)延时控制速度
5.3 蛇的移动
蛇移动的本质是:蛇头向前走一格,身体每个格子跟上前面一格 。我们用循环把 snakeX[i] = snakeX[i-1] 实现这个「跟上」的动作。
5.4 碰撞检测
- 撞墙:新蛇头坐标超出地图边界。
- 撞自己:新蛇头坐标与身体任意一格重合。
这两种情况都让 gameOver = 1,游戏结束。
6. 跨平台改造(Linux / macOS)
如果你在 Linux 或 macOS 上,上面的代码不能直接编译。需要做两处替换:
6.1 替换头文件
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // usleep
#include <termios.h> // 终端原始模式
#include <fcntl.h> // fcntl
6.2 替换按键读取和延时
c
// 非阻塞读取按键
int kbhit() {
struct termios oldt, newt;
int ch;
int oldf;
tcgetattr(STDIN_FILENO, &oldt);
newt = oldt;
newt.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
ch = getchar();
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
fcntl(STDIN_FILENO, F_SETFL, oldf);
if (ch != EOF) {
ungetc(ch, stdin);
return 1;
}
return 0;
}
然后把 _kbhit() 换成 kbhit(),_getch() 换成 getchar(),Sleep(150) 换成 usleep(150000)(单位是微秒,150000 微秒 = 150 毫秒)。
7. 扩展与进阶
贪吃蛇只是起点,你可以在此基础上做很多扩展:
7.1 增加难度
- 每吃一个食物,
Sleep的时间减少一点,蛇越跑越快。 - 增加障碍物,让地图更复杂。
7.2 增加计分与关卡
- 用文件保存最高分,下次启动时读取。
- 达到一定分数进入下一关,地图变大或速度变快。
7.3 换一个游戏
掌握了游戏循环的思路,你可以尝试写:
- 井字棋:双人对战,用二维数组判断胜负。
- 扫雷:用二维数组 + 随机布雷 + 递归展开。
- 俄罗斯方块:用二维数组表示方块,实现旋转和消行。
8. 总结
用 C 语言写小游戏并不难,核心就三件事:
- 用数据结构表示游戏状态(数组、结构体)
- 用游戏循环驱动逻辑(输入 → 更新 → 绘制)
- 用碰撞检测判断结束(边界、自身、目标)
贪吃蛇这个项目麻雀虽小五脏俱全,非常适合作为 C 语言的练手项目。建议你亲手敲一遍代码,再试着加一两个自己的功能,收获会更大。
祝你写出第一个属于自己的 C 语言小游戏!