☰
【C语言】贪吃蛇项目:用Windows API宽字符打造控制台游戏,TaoToken统一Key管理API调用
2026/10/1 15:12:59 网站建设 项目流程

1. 从零搭建贪吃蛇:控制台宽字符渲染与 Windows API 实战

C语言贪吃蛇项目是很多人从语法练习走向"能跑起来的小软件"的第一道坎。它不依赖图形库,只用 Windows API 就能在控制台里画出蛇、食物和地图,核心难点集中在三件事:控制台光标定位、宽字符渲染、键盘实时输入。把这三点吃透,你就能理解控制台程序如何"假装"成一个游戏界面。

这个项目适合已经学过 C 语言基础语法、指针和结构体,但还没做过完整小项目的人。它不需要你装 Visual Studio 之外的任何东西,一个.c文件加windows.h就能编译运行。我试过把整个项目拆成"控制台初始化 → 宽字符绘制 → 蛇身移动 → 碰撞检测"四层,每层单独验证,比一次性写完再调试省心得多。

除了游戏本身,这篇还会带你把项目里可能用到的 API 调用配置统一管理起来。很多同学写小项目时 Key 散落在各个文件里,改一次要翻半天。用 TaoToken 的统一 Key 通道,把模型调用、编码辅助这类配置集中到一处,项目结构会干净很多。下面从控制台处理开始,一步步把可复制的代码交给你。

2. 控制台初始化与宽字符渲染:Windows API 贪吃蛇控制台游戏开发教程

控制台默认是一个"文本流"界面,光标位置、窗口大小、字符宽度都由系统控制。要让它变成游戏画布,第一步是拿到标准输出设备的句柄,然后通过SetConsoleCursorPosition精确控制光标落点。

先看窗口尺寸和标题。system函数来自stdlib.h,可以直接执行控制台命令:

#include <stdlib.h> system("mode con cols=100 lines=30"); // 设置窗口 100 列 30 行 system("title 贪吃蛇"); // 设置窗口标题

mode con cols=100 lines=30把控制台缓冲区改成 100×30,这是游戏地图的物理边界。注意列数建议用偶数,因为后面宽字符占两个窄字符宽度,奇数会导致地图右边界对不齐。

接下来是光标定位三件套。COORD结构体存放坐标,GetStdHandle拿到设备句柄,SetConsoleCursorPosition把光标移到指定位置:

#include <windows.h> void SetPos(int x, int y) { COORD pos = { (SHORT)x, (SHORT)y }; HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(hOutput, pos); }

STD_OUTPUT_HANDLE是一个宏,本质是(DWORD)-11,传给GetStdHandle就返回标准输出的句柄。句柄类型是HANDLE,可以理解成系统给你的一个"遥控器",后续所有对控制台的修改都通过它进行。

光标默认是可见的,游戏里闪烁的光标会破坏画面。用CONSOLE_CURSOR_INFO结构体控制它的外观:

void HideCursor() { HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursorInfo; GetConsoleCursorInfo(hOutput, &cursorInfo); // 先获取当前信息 cursorInfo.bVisible = FALSE; // 再修改可见性 SetConsoleCursorInfo(hOutput, &cursorInfo); // 最后写回 }

这里有个容易踩的坑:必须先GetConsoleCursorInfo拿到完整结构体,再改bVisible,最后SetConsoleCursorInfo写回。如果直接声明一个未初始化的结构体去设置,dwSize字段是随机值,光标可能变成一条粗线或者直接消失异常。

宽字符渲染是控制台游戏的关键。控制台坐标系里 y 轴单位长度是 x 轴的两倍,因为英文字符宽高比约为 1:2。一个宽字符(汉字、方块符号)占两个窄字符宽度,正好填满一个"正方形"格子。要让wprintf正确输出宽字符,必须先设置本地化环境:

#include <locale.h> setlocale(LC_ALL, ""); // 使用系统本地环境,支持宽字符输出

程序启动时默认执行的是setlocale(LC_ALL, "C"),也就是英文环境,此时宽字符会输出乱码。改成""表示跟随系统区域设置,中文 Windows 下就能正常显示汉字和方块符号。蛇身节点用■、食物用●,视觉上比*和#清晰得多。

地图绘制用双重循环,边界用宽字符填充,内部留空:

#define POS_X 24 #define POS_Y 5 void DrawMap() { SetPos(0, 0); for (int i = 0; i <= 56; i += 2) { wprintf(L"□"); // 上边界 } for (int i = 0; i < 24; i++) { SetPos(0, i + 1); wprintf(L"□"); // 左边界 SetPos(56, i + 1); wprintf(L"□"); // 右边界 } SetPos(0, 25); for (int i = 0; i <= 56; i += 2) { wprintf(L"□"); // 下边界 } }

POS_X和POS_Y是地图左上角在控制台里的偏移,留出空间打印分数和帮助信息。宽字符用L"..."前缀,配合wprintf输出。地图边界每两个坐标放一个宽字符,所以循环步长是 2。

3. 蛇身移动与碰撞检测:C语言贪吃蛇 Windows API 完整代码实现

蛇身用链表维护,每个节点存一个坐标。为什么用链表而不是数组?因为蛇在移动时头部增加、尾部删除,链表插入删除都是 O(1),数组要整体搬移。节点结构体:

typedef struct SnakeNode { int x; int y; struct SnakeNode* next; } SnakeNode, *pSnakeNode;

全局状态统一放在一个结构体里,避免到处传参:

enum STATUS { OK, ESC, KILL_BY_WALL, KILL_BY_SELF }; typedef struct Snake { pSnakeNode _pSnake; // 蛇身链表头指针 pSnakeNode _pFood; // 食物节点 enum STATUS _status; // 游戏状态 int _score; // 当前分数 int _food_weight; // 一个食物的分数 int _sleep_time; // 每步睡眠时间,控制速度 } Snake, *pSnake;

初始化蛇身时,头节点放在(POS_X + 2, POS_Y),后续节点依次向右偏移 2 个坐标(因为宽字符占两格):

void InitSnake(pSnake ps) { pSnakeNode cur = NULL; for (int i = 0; i < 5; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); cur->x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; // 头插法,最后创建的节点成为新头 if (ps->_pSnake == NULL) { ps->_pSnake = cur; } else { cur->next = ps->_pSnake; ps->_pSnake = cur; } } }

头插法让链表头始终是蛇头,移动时只需在头部插入新节点、尾部删除旧节点。食物节点随机生成在地图内部,坐标要保证是偶数 x(宽字符对齐):

void CreateFood(pSnake ps) { int x, y; do { x = rand() % 53 + 2; // 2 到 54 之间 y = rand() % 24 + 1; // 1 到 24 之间 if (x % 2 != 0) x++; // 保证偶数,宽字符对齐 } while (CheckFoodPos(ps, x, y)); // 不能和蛇身重叠 pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode)); food->x = x; food->y = y; food->next = NULL; ps->_pFood = food; }

按键检测用GetAsyncKeyState,它返回一个SHORT,最低位为 1 表示自上次调用以来该键被按过:

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0) void GetKey(pSnake ps) { if (KEY_PRESS(VK_UP) && ps->_status != DOWN) { ps->_status = UP; } else if (KEY_PRESS(VK_DOWN) && ps->_status != UP) { ps->_status = DOWN; } else if (KEY_PRESS(VK_LEFT) && ps->_status != RIGHT) { ps->_status = LEFT; } else if (KEY_PRESS(VK_RIGHT) && ps->_status != LEFT) { ps->_status = RIGHT; } else if (KEY_PRESS(VK_ESCAPE)) { ps->_status = ESC; } else if (KEY_PRESS(VK_SPACE)) { Pause(); } }

VK_UP、VK_DOWN这些是虚拟键码宏,定义在windows.h里。方向判断里加了"不能反向"的限制,防止蛇直接掉头撞自己。

移动一步的逻辑分两种情况。吃到食物时,头部插入新节点,不删尾部,蛇变长;没吃到时,头部插入新节点,删掉尾部,蛇长度不变:

void MoveSnake(pSnake ps) { pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode)); // 根据当前方向计算下一步坐标 switch (ps->_status) { case UP: next->x = ps->_pSnake->x; next->y = ps->_pSnake->y - 1; break; case DOWN: next->x = ps->_pSnake->x; next->y = ps->_pSnake->y + 1; break; case LEFT: next->x = ps->_pSnake->x - 2; next->y = ps->_pSnake->y; break; case RIGHT: next->x = ps->_pSnake->x + 2; next->y = ps->_pSnake->y; break; } // 头插 next->next = ps->_pSnake; ps->_pSnake = next; if (next->x == ps->_pFood->x && next->y == ps->_pFood->y) { EatFood(ps, next); // 吃到食物,不删尾 } else { NoFood(ps, next); // 没吃到,删尾 } }

EatFood里把食物节点释放,重新生成食物,分数增加,速度加快;NoFood里遍历到倒数第二个节点,释放最后一个节点。碰撞检测在每步移动后判断:头部坐标撞墙(超出地图边界)或撞到自己(头部坐标等于某个身体节点坐标),就把_status设为对应错误状态,主循环退出。

主循环结构:

while (ps._status == OK) { PrintScore(&ps); GetKey(&ps); MoveSnake(&ps); Sleep(ps._sleep_time); Judge(&ps); }

Sleep的时间存在_sleep_time里,初始 200 毫秒,每吃一个食物减 10 毫秒,最低 80 毫秒,这样蛇会越跑越快。

4. TaoToken 统一 Key 管理:项目 API 调用配置集中化实践

小项目写到后面,往往不止一个 API 调用点。比如你想给贪吃蛇加一个"AI 生成地图"功能,或者用模型辅助生成关卡配置,Key 就会散落在多个文件里。改一次环境要翻遍整个项目,还容易把 Key 提交到 Git。TaoToken 的统一 Key 通道就是解决这个问题的:一个 Key 管所有模型调用,配置集中在一处。

先拿到 Key。访问 https://taotoken.net/api-keys 创建,复制出来形如sk-xxxxxxxx的字符串。注意这个 Key 只在创建时显示一次,丢了只能重新生成。

拿到 Key 后,在项目根目录建一个config.json,把 Base URL、Key、Model ID 三件套写进去:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model_id": "claude-sonnet-4-20250514", "timeout": 30 }

Base URL 用https://taotoken.net/api,不要加末尾斜杠。Model ID 按你实际要用的模型填,这里以 Claude 系列为例。timeout是请求超时秒数,小项目设 30 秒足够。

C 语言里读 JSON 需要第三方库,如果不想引入依赖,可以用更简单的.env风格配置文件:

// config.h #ifndef CONFIG_H #define CONFIG_H #define TAOTOKEN_BASE_URL "https://taotoken.net/api" #define TAOTOKEN_API_KEY "sk-你的Key" #define TAOTOKEN_MODEL_ID "claude-sonnet-4-20250514" #endif

把这三个宏集中在一个头文件里,其他文件#include "config.h"就能用。改 Key 只改这一处。生产环境建议把 Key 放到环境变量里,代码里用getenv读取:

#include <stdlib.h> const char* GetApiKey() { const char* key = getenv("TAOTOKEN_API_KEY"); if (key == NULL) { fprintf(stderr, "TAOTOKEN_API_KEY not set\n"); return NULL; } return key; }

这样 Key 不会出现在代码和 Git 历史里。Windows 下设置环境变量用set TAOTOKEN_API_KEY=sk-xxx,Linux/macOS 用export TAOTOKEN_API_KEY=sk-xxx。

如果你用 Claude Code 做编码辅助,配置方式类似。在项目根目录建.claude/settings.json:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }

三个字段对应 Base URL、Key、Model ID,缺一不可。配好后 Claude Code 的请求就走 TaoToken 通道,和贪吃蛇项目共用同一个 Key。

Cline 的 MCP 配置也是同样的三件套。在 Cline 设置里找到 MCP Servers,添加:

{ "mcpServers": { "taotoken": { "command": "npx", "args": ["-y", "@taotoken/mcp-server"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_MODEL_ID": "claude-sonnet-4-20250514" } } } }

Codex 用户则在~/.codex/auth.json里配置:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model": "claude-sonnet-4-20250514" }

不管用哪个工具,记住三件套:Base URL 填https://taotoken.net/api,Key 填sk-开头的字符串,Model ID 填你要用的模型名。三者必须同时正确,缺一个就会报 401 或模型不存在。

5. 常见报错排查:401、local proxy failed、reading choices 与 OAuth 问题

配置过程中最容易遇到四类报错,逐个说清楚原因和解决办法。

401 Unauthorized。这是 Key 问题,三种可能:Key 复制时带了空格或换行,Key 已过期或被删除,Key 没有对应模型的权限。先检查 Key 字符串首尾有没有空白字符,用echo $TAOTOKEN_API_KEY | wc -c看长度对不对。如果 Key 没问题,去 https://taotoken.net/console 看 Key 状态和余额。确认无误后重新生成一个 Key 再试。

local proxy failed。这个报错通常出现在本地网络环境有额外代理设置时。检查系统环境变量里有没有HTTP_PROXY、HTTPS_PROXY,如果有,临时清掉再试:

# Windows set HTTP_PROXY= set HTTPS_PROXY= # Linux/macOS unset HTTP_PROXY unset HTTPS_PROXY

另外确认 Base URL 写的是https://taotoken.net/api,不要写成http://或者带端口号的形式。

reading choices 报错。这个错误一般出现在响应体解析阶段,说明请求发出去了但返回格式不符合预期。常见原因是 Model ID 写错了,比如把claude-sonnet-4-20250514写成了claude-sonnet-4。去 https://taotoken.net/doc 查一下当前支持的模型列表,复制准确的 Model ID。另一个可能是 Base URL 末尾多了斜杠,导致请求路径变成//v1/messages,去掉末尾斜杠即可。

OAuth 相关报错。如果你用的是 Claude Code 或类似工具,它可能默认走 OAuth 登录流程。配置了ANTHROPIC_API_KEY后,工具应该优先用 Key 而不是 OAuth。如果还是报 OAuth 错误,检查settings.json里env字段的键名是否写对,必须是ANTHROPIC_BASE_URL、ANTHROPIC_API_KEY、ANTHROPIC_MODEL这三个。键名写错工具读不到,就会回退到 OAuth 流程。

排查顺序建议:先确认 Key 有效(用 curl 直接测),再确认 Base URL 正确,最后确认 Model ID 存在。三步都过了还报错,去 https://taotoken.net/doc 看接入文档里的最新说明。

6. 编译运行与功能验证:贪吃蛇项目完整交付步骤

代码写完后,编译运行验证。用 GCC 编译:

gcc snake.c -o snake.exe -lwinmm

-lwinmm链接 Windows 多媒体库,Sleep函数需要它。如果用 Visual Studio,直接新建空项目,把.c文件加进去,在项目属性里确认字符集设为"使用多字节字符集"或"未设置",避免宽字符输出异常。

运行后按顺序验证这几个功能点:

第一,窗口尺寸和标题。运行后窗口应该是 100×30,标题栏显示"贪吃蛇"。如果尺寸不对,检查system("mode con ...")的参数。

第二,地图和蛇的显示。地图边界应该是连续的方块,蛇身 5 个节点水平排列在地图左上角。如果出现乱码,检查setlocale(LC_ALL, "")是否在wprintf之前调用。

第三,按键响应。按方向键蛇应该改变方向,按 ESC 退出,按空格暂停。如果按键没反应,检查GetAsyncKeyState的虚拟键码是否正确,以及主循环里GetKey是否每帧都调用。

第四,吃食物和加速。蛇头碰到食物时,分数增加,蛇身变长,速度加快。如果食物生成在蛇身上,检查CreateFood里的重叠判断逻辑。

第五,碰撞检测。蛇撞墙或撞自己时,游戏结束并显示对应提示。如果撞墙没反应,检查Judge函数里的边界判断条件。

编译时如果报undefined reference to 'GetAsyncKeyState',说明没链接user32库,加-luser32。如果报wprintf相关警告,确认包含了<wchar.h>和<locale.h>。

整个项目跑通后,你会得到一个约 300 行的单文件程序。后续想扩展的话,可以加最高分记录(写文件)、难度选择(改初始_sleep_time)、双人模式(第二套按键映射)。这些扩展都不需要改核心逻辑,只是在现有结构上加分支。

如果编码过程中想让工具帮你补全函数或解释 Windows API 用法,用 TaoToken 的 Coding Plan 通道就行,配置方式和上面 §4 里写的一样,三件套填对即可。模型对话调试去 https://taotoken.net/model-chat,接入文档在 https://taotoken.net/doc,Key 管理在 https://taotoken.net/api-keys。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询