首页 / 资讯中心 / 文章详情

C语言贪吃蛇小游戏完整实现:从零构建控制台版并接入TaoToken统一Key通道

C语言贪吃蛇小游戏完整实现:从零构建控制台版并接入TaoToken统一Key通道 ★ FEATURED ARTICLE
1. 从控制台贪吃蛇到在线排行榜一个练手项目的完整落地路径C语言贪吃蛇小游戏几乎是每个学C的人都会碰的项目它把数组、链表、键盘响应、控制台坐标控制这些知识点串成了一条线。但很多人写完本地版本就停了蛇能跑、能吃食物、能计分然后代码往硬盘里一扔再也没打开过。我这次想做的是在这个经典项目上再往前推一步让游戏结束后能把分数上传到一个在线排行榜看看自己到底排第几。这个扩展听起来简单但它逼着你把C语言从单机玩具变成能跟外部服务对话的程序涉及HTTP请求、JSON解析、API Key管理这些真实开发里绕不开的东西。你可能会问一个控制台小游戏接排行榜有必要吗我的答案是非常有必要。因为排行榜这个需求恰好覆盖了从本地程序到网络服务的完整链路。你需要一个稳定的API通道来发请求需要管理Key避免硬编码泄露需要处理网络失败的情况还需要解析返回的JSON。这些技能在你后面写任何联网程序时都会用到。而TaoToken提供的统一Key通道正好可以把接入在线服务这件事的门槛降到最低——你不需要自己搭服务器不需要处理复杂的鉴权流程拿到Key就能调。这篇文章会分成两条线走。第一条线是贪吃蛇本体的完整实现我会把蛇身链表、方向控制、食物生成、碰撞检测这些核心逻辑讲清楚代码可以直接编译运行。第二条线是在这个基础上扩展排行榜功能演示怎么通过TaoToken的API通道把分数传上去、把排名拉下来。两条线合在一起你得到的不只是一个能玩的游戏而是一个能跑通本地程序在线服务的最小闭环。适合有C语言基础、想找个项目练手、同时对API调用感兴趣的人。下面先从游戏本身开始。2. 贪吃蛇核心逻辑拆解蛇身链表、方向控制与碰撞检测贪吃蛇的本质是一个状态机在循环里不断更新。每一帧你要做几件事读键盘、算新蛇头位置、判断有没有吃到食物、判断有没有撞墙或撞自己、重绘画面、休眠一小段时间。听起来简单但每个环节都有坑。我先把数据结构定下来再逐个讲实现。蛇身我用链表存每个节点是一个坐标加一个next指针。为什么不用数组因为蛇的移动是头插尾删链表在这两个操作上都是O(1)数组做尾删虽然也快但头插要挪整个数组蛇越长越慢。链表还有个好处是蛇身长度理论上无上限不用预先分配固定大小。节点结构体长这样typedef struct SnakeNode { int x; int 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;方向控制这块很多人第一次写会犯一个错允许蛇直接反向。比如蛇正在向右走你按左键蛇头直接撞到第二节身体上游戏瞬间结束。所以方向切换必须加一个判断当前方向是RIGHT时不能切到LEFT是UP时不能切到DOWN。这个判断放在GameRun的键盘检测里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; }键盘检测用的是Windows的GetAsyncKeyState它返回一个short最低位为1表示自上次调用后这个键被按过。我封装成一个宏用起来清爽#define KEY_PRESS(vKey) (GetAsyncKeyState(vKey) 0x0001)食物生成有个细节要注意坐标不能跟蛇身重叠。我的做法是随机生成一个坐标然后遍历蛇身链表检查如果撞上了就重新生成。这里用goto做重试循环最直观again: x 2 * (rand() % 27 1); y rand() % 25 1; pSnakeNode cur ps-_pSnake; while (cur) { if (x cur-x y cur-y) { goto again; } cur cur-next; }x坐标为什么是偶数因为宽字符比如●、★在控制台里占两列如果x是奇数蛇身和食物会错位看起来就是斜的。这个坑我踩过当时蛇身画出来是锯齿状的调了半天才发现是坐标没对齐。碰撞检测分两种撞墙和撞自己。撞墙就是看蛇头坐标有没有碰到边界撞自己就是遍历蛇身从第二节开始看有没有跟蛇头坐标重合。这两个检测在每次移动后都要跑一遍一旦触发就把游戏状态改成对应的结束状态主循环自然退出。移动逻辑是核心中的核心。蛇移动时先根据当前方向算出新蛇头的坐标然后判断这个位置是不是食物。如果是食物就把食物节点接到蛇头前面不删尾蛇身长度加一分数加上当前食物的分值然后重新生成食物。如果不是食物就新建一个节点插到蛇头前面然后把蛇尾删掉保持长度不变。这里有个容易忽略的点删尾之前要先把蛇尾那个位置的符号用空格覆盖掉否则屏幕上会残留一个●。宽字符占两列所以要输出两个空格SetPos(cur-next-x, cur-next-y); printf( ); free(cur-next); cur-next NULL;速度控制我用_sleep_time这个变量值越小蛇跑得越快。按F3加速就是减30毫秒同时食物分值加2按F4减速就是加30毫秒食物分值减2。加速有下限我设的80毫秒减速有下限食物分值不能低于2不然游戏会失控。这个设计让玩家在跑得快但风险高和跑得慢但稳之间做取舍比固定速度有意思。3. 接入TaoToken统一Key通道配置文件与请求封装游戏本体跑通之后接下来做排行榜。排行榜的本质是游戏结束时把玩家分数发到一个服务端服务端存下来下次打开游戏时拉取前几名显示出来。传统做法是自己搭个后端但那样你就得处理服务器、数据库、鉴权一大堆事。用TaoToken的统一Key通道你只需要一个API Key就能调用它提供的接口省掉后端搭建的环节。先说Key怎么拿。访问TaoToken官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册登录后进控制台在API Keys页面创建一个新的Key。这个Key就是你调用接口的凭证格式类似sk-开头的一串字符。创建完之后复制下来存到一个本地文件里不要直接写死在代码里。为什么因为如果你把Key硬编码进源码万一代码传到GitHub上Key就泄露了别人可以拿你的Key去刷接口。正确做法是放在一个配置文件里代码运行时读取。我在项目根目录建一个config.json内容长这样{ api_base: https://taotoken.net/api, api_key: sk-你的实际Key填这里, model_id: gpt-4o-mini, leaderboard_endpoint: /v1/chat/completions }注意api_base用的是https://taotoken.net/api不带任何UTM参数这是接口调用的基础地址。model_id填你实际要用的模型ID这个在TaoToken的文档里能查到。leaderboard_endpoint这里我先用chat/completions做演示实际排行榜你可以让模型帮你格式化数据或者用TaoToken的其他接口。C语言读JSON没有标准库我选了一个轻量级的单头文件库cJSON把它下载下来放到项目里编译时一起编进去。读配置的代码大概是这样#include cJSON.h typedef struct { char api_base[256]; char api_key[256]; char model_id[128]; } Config; int load_config(const char* path, Config* cfg) { FILE* fp fopen(path, rb); if (!fp) { perror(open config.json failed); return -1; } fseek(fp, 0, SEEK_END); long len ftell(fp); fseek(fp, 0, SEEK_SET); char* buf (char*)malloc(len 1); fread(buf, 1, len, fp); buf[len] \0; fclose(fp); cJSON* root cJSON_Parse(buf); free(buf); if (!root) { printf(parse config.json failed\n); return -1; } cJSON* base cJSON_GetObjectItem(root, api_base); cJSON* key cJSON_GetObjectItem(root, api_key); cJSON* model cJSON_GetObjectItem(root, model_id); if (base key model) { strncpy(cfg-api_base, base-valuestring, sizeof(cfg-api_base) - 1); strncpy(cfg-api_key, key-valuestring, sizeof(cfg-api_key) - 1); strncpy(cfg-model_id, model-valuestring, sizeof(cfg-model_id) - 1); } cJSON_Delete(root); return 0; }发HTTP请求这块Windows下可以用WinHTTPLinux下用libcurl。为了跨平台我选libcurl它接口统一文档也全。发一个POST请求的核心代码#include curl/curl.h struct Memory { char* data; size_t size; }; static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) { size_t realsize size * nmemb; struct Memory* mem (struct Memory*)userp; char* ptr realloc(mem-data, mem-size realsize 1); if (!ptr) return 0; mem-data ptr; memcpy((mem-data[mem-size]), contents, realsize); mem-size realsize; mem-data[mem-size] \0; return realsize; } int post_score(Config* cfg, int score) { CURL* curl curl_easy_init(); if (!curl) return -1; struct Memory chunk {0}; char url[512]; snprintf(url, sizeof(url), %s/v1/chat/completions, cfg-api_base); char body[1024]; snprintf(body, sizeof(body), {\model\:\%s\,\messages\:[{\role\:\user\,\content\:\记录分数:%d\}]}, cfg-model_id, score); struct curl_slist* headers NULL; headers curl_slist_append(headers, Content-Type: application/json); char auth[512]; snprintf(auth, sizeof(auth), Authorization: Bearer %s, cfg-api_key); headers curl_slist_append(headers, auth); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, chunk); CURLcode res curl_easy_perform(curl); if (res ! CURLE_OK) { printf(curl failed: %s\n, curl_easy_strerror(res)); curl_slist_free_all(headers); curl_easy_cleanup(curl); free(chunk.data); return -1; } printf(response: %s\n, chunk.data ? chunk.data : (empty)); curl_slist_free_all(headers); curl_easy_cleanup(curl); free(chunk.data); return 0; }这段代码里Authorization头用的是Bearer加你的Key这是TaoToken接口的标准鉴权方式。请求体是JSON格式model字段填配置里的model_idmessages里放你要发的内容。返回的数据会写到chunk.data里你可以用cJSON解析出来看。编译的时候要链接libcurl和cJSON。Makefile大概长这样CC gcc CFLAGS -Wall -O2 -I./include LDFLAGS -lcurl -lwinhttp SRCS main.c snake.c config.c api.c cJSON.c OBJS $(SRCS:.c.o) TARGET snake.exe $(TARGET): $(OBJS) $(CC) -o $ $^ $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: del /Q *.o $(TARGET)Windows下编译需要先装libcurl的开发包或者用vcpkg装。Linux下直接apt install libcurl4-openssl-dev就行。cJSON.c直接放进项目里编译不用额外装库。4. 编译运行与接口验证从本地游戏到在线排行榜代码写完之后先验证游戏本体能不能跑。编译命令make如果一切正常会生成snake.exe。双击运行应该能看到欢迎界面按任意键进入游戏方向键控制蛇移动F3加速F4减速空格暂停ESC退出。蛇吃到食物会变长分数会增加撞墙或撞自己会结束游戏然后问你要不要再来一局。游戏跑通之后验证排行榜接口。我写了一个独立的测试函数不依赖游戏流程直接调post_score发一个分数上去int main() { setlocale(LC_ALL, ); srand((unsigned int)time(NULL)); Config cfg; if (load_config(config.json, cfg) ! 0) { printf(load config failed\n); return 1; } printf(api_base: %s\n, cfg.api_base); printf(model_id: %s\n, cfg.model_id); post_score(cfg, 120); test(); return 0; }运行之后如果Key和配置都对你会看到类似这样的返回{ id: chatcmpl-xxx, object: chat.completion, created: 1700000000, model: gpt-4o-mini, choices: [ { index: 0, message: { role: assistant, content: 已记录分数:120 }, finish_reason: stop } ], usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 } }看到choices数组里有内容说明请求成功了。如果返回的是401说明Key不对或者没带上Authorization头。如果返回404说明URL拼错了检查api_base和endpoint有没有多余或缺失的斜杠。把排行榜功能接进游戏流程就是在GameEnd里加一段游戏结束时如果分数大于0就调post_score把分数发上去。然后下次游戏启动时可以再发一个请求拉取排行榜把前几名显示在欢迎界面旁边。拉取排行榜的请求跟发分数类似只是body里的content换成查询排行榜前10名然后解析返回的content字段显示出来。这里有个实际的问题网络请求会阻塞游戏主线程。如果网络慢游戏结束到显示排行榜之间会卡几秒。解决办法是把网络请求放到一个单独的线程里或者用异步IO。但为了简单我建议先同步做等跑通了再优化。毕竟排行榜不是核心玩法卡一下可以接受。验证的时候还要注意一点TaoToken的接口有速率限制别在循环里疯狂发请求。我一般是在游戏结束时发一次启动时拉一次频率很低不会触发限制。如果你要压测记得看文档里的限流说明。5. 常见报错排查401、local proxy failed与JSON解析失败接入API的过程中报错是难免的。我把几个高频错误和排查方法列出来你遇到了可以对照着看。401 Unauthorized这是最常见的错误意思是鉴权失败。原因通常有三个Key没填对、Authorization头格式不对、Key过期了。排查步骤先打印cfg.api_key看看是不是空字符串或者还是占位符再检查Authorization头的格式必须是Bearer 加Key中间有个空格很多人漏掉空格最后去TaoToken控制台确认Key还有效。如果Key泄露过建议删掉重新创建一个。local proxy failed / Could not resolve host这个错误说明curl连不上服务器。原因可能是网络不通、DNS解析失败、或者系统代理配置有问题。排查步骤先用ping命令试试能不能通taotoken.net如果ping不通检查网络连接如果能ping通但curl还是失败检查环境变量里有没有http_proxy或https_proxy有的话临时unset掉再试。注意这里说的是正常的网络代理配置问题不是让你去搞什么特殊网络工具就是排查本机环境变量。reading choices failed / JSON解析失败这个错误说明请求发出去了返回也拿到了但解析返回的JSON时出错了。原因可能是返回的不是JSON格式比如返回了一个HTML错误页或者JSON结构跟预期不一样。排查步骤先把chunk.data原样打印出来看看返回的到底是什么。如果是一段HTML说明请求打到了错误的地址检查URL如果是JSON但解析失败用在线JSON校验工具看看格式对不对。cJSON解析失败时可以用cJSON_GetErrorPtr()看具体错在哪。curl_easy_perform返回CURLE_WRITE_ERROR这个错误说明写回调函数返回了0导致curl中断传输。原因通常是realloc失败内存不够了。排查步骤检查write_cb里realloc的返回值如果为NULL要处理另外确认mem-data初始化为NULL不然realloc会出问题。编译时报undefined reference to curl_easy_init这是链接错误说明没链接libcurl。检查Makefile里的LDFLAGS有没有-lcurlWindows下还要确认libcurl的库文件路径对不对。如果用vcpkg装的编译时要加-I和-L指向vcpkg的include和lib目录。游戏画面闪烁严重这不是API的问题是控制台重绘的问题。每次移动都全屏重绘会导致闪烁。解决办法是用双缓冲或者只重绘变化的部分。我的做法是移动时只画新蛇头和擦掉蛇尾不重绘整个蛇身这样闪烁会少很多。蛇身坐标错位如果蛇身看起来是斜的或者有重叠检查x坐标是不是偶数。宽字符占两列x必须是偶数才能对齐。另外检查SetPos的坐标有没有算错特别是左右移动时x的增减是2不是1。食物生成在墙里或者蛇身上检查CreateFood的坐标范围。x的范围是2到54的偶数y的范围是1到25。如果范围写错了食物可能生成在墙里。另外检查遍历蛇身的循环有没有正确跳过确保食物不会跟蛇身重叠。6. 继续扩展把排行榜做成真正的在线服务游戏能跑、接口能通之后你可以在这个基础上继续加东西。比如把排行榜做成真正的在线服务每次游戏结束自动上传分数启动时拉取前10名显示。这需要你在TaoToken的接口基础上再包一层或者用它的其他能力来存储数据。具体怎么做可以看接入文档里的说明。如果你想把游戏分享给别人玩可以把Key放在服务端客户端只调你自己的接口这样Key不会暴露。但这又回到了搭后端的老路。折中方案是用TaoToken的Coding Plan它提供了一些现成的能力可以省掉一部分后端工作。具体适不适合你的场景去Coding Plan页面看看就知道了。模型选择上排行榜这种场景不需要太强的模型用gpt-4o-mini这种便宜的就够了。如果你要做的排行榜需要更复杂的逻辑比如按分数段分组、计算排名变化可以换更强的模型。Model ID在文档里都能查到换的时候只改config.json里的model_id就行代码不用动。最后说一个实际经验网络请求一定要加超时。我一开始没加结果网络卡的时候游戏直接卡死体验很差。curl_easy_setopt里加CURLOPT_TIMEOUT设个5秒超时就放弃不要让玩家等太久。另外请求失败要有降级方案比如显示排行榜暂时不可用而不是直接崩溃。这些细节决定了你的程序是能跑还是好用。代码写到这里贪吃蛇已经不只是一个练手项目了。它有了本地游戏循环、有了网络通信、有了配置管理、有了错误处理这些都是一般C语言教程里不会教但实际工作中天天用的东西。你可以继续往上加功能比如多人在线对战、分数曲线图、成就系统每加一个都是在练不同的技能。关键是先把这条最小闭环跑通后面加什么都是在这个基础上长出来的。
阅读完成 · 觉得有帮助?
咨询建站