用CVI写一个贪吃蛇小游戏

欲看图文版,请用PC浏览器下载附件pdf文档。

得闲刷知乎,有一个提问是:用C语言,能在100行之内实现贪吃蛇吗?https://www.zhihu.com/question/360814879。

哇塞,我瞬间就回忆起,上个世纪末大二还是大三时上《微机原理》课,发量茂密且发型板正的任课老师说,学完了这本书,你们就应该会用汇编语言,写一个计算器出来了。但毕业只差2年就满卅年了,我都还一直搞不来,主要是卡在如何用户输入的字符串解析成汇编公式来运算。

网帖盖楼很踊跃,我坐下来拿电脑瞟了眼代码,多是C语言的,就寻思着要不拷贝一个源码出来,在CVI2010里面跑跑?结果一编译,不是报没有找到conio.h就是报没找到termios.h。

去问了豆包,说conio.h是Windows/DOS下的非标准控制台IO头文件,提供无缓冲字符输入/输出功能;而termios.h是Linux下实现"非阻塞键盘监听、关闭字符回显"的核心工具,相当于Windows下conio.h的替代方案。心想也对,做贪吃蛇需要非阻塞读取键盘(不卡死等按键,蛇能自己移动),像标准C库的getchar()就是阻塞式回去键盘,会让进程卡住,不能用的。

很快地,豆包直接给出了有效代码仅95行的、在CVI2010可运行的、C89风格的贪吃蛇C源码,这是程序在控制台窗口运行的截图:

豆包把这份贪吃蛇代码的核心逻辑拆解成5个关键模块,然后用最通俗的语言解释了每个模块的作用、核心代码行的含义,以及它们如何协同工作------新手也能一眼看懂贪吃蛇的运行原理。

先说结论

贪吃蛇的核心是:主循环不断更新蛇的坐标(头动身随),检测按键改方向、检测碰撞判结束,同时把坐标绘制成画面。

  1. 交互核心:非阻塞按键监听(kbhit()+getch()),实现边移边操作;
  2. 移动核心:蛇头更新坐标,蛇身复制前一个坐标,吃到食物变长;
  3. 可视化核心:光标定位 + 字符绘制,把坐标转换成可见的画面;
  4. 规则核心:碰撞检测(边框 / 自身)+ 方向限制(不反向)。
    这就是整个贪吃蛇最核心的逻辑,其他代码都是为了适配 CVI2010(比如替代conio.h、兼容 C89),核心玩法就靠这 5 个模块支撑。

一、核心模块 1:非阻塞键盘监听(游戏交互的核心)

作用:贪吃蛇需要 "边移动边响应按键",不能像getchar()那样卡死等按键,这部分代码实现了无阻塞检测按键 + 读取方向,是游戏能流畅操作的关键。

注意,CVI2010似乎不能#include <conio.h>,所以豆包给出了调用windows API的替代方案,来实现非阻塞式检测方向键/ESC是否按下的kbhit()函数,和判断按键具体是哪个方向键/ESC的getch()函数。很显然,这里用到了GetAsyncKeyState()函数,这是WindowsAPI的非阻塞检测按键函数,按下返回负数,没按返回0。

复制代码
// ============= 替代 conio.h 的非阻塞键盘函数 =============  
// 模拟 kbhit():检测方向键/ESC是否按下  
int kbhit()  
{  
    if (GetAsyncKeyState(VK_UP) < 0 ||   
        GetAsyncKeyState(VK_DOWN) < 0 ||   
        GetAsyncKeyState(VK_LEFT) < 0 ||   
        GetAsyncKeyState(VK_RIGHT) < 0 ||   
        GetAsyncKeyState(VK_ESCAPE) < 0)  
    {  
        return 1;  
    }  
    return 0;  
}  
  
// 模拟 getch():读取方向键/ESC  
int getch()  
{  
    if (GetAsyncKeyState(VK_UP) < 0)  
    {  
        Sleep(50); // 消抖  
        return UP;  
    }  
    else if (GetAsyncKeyState(VK_DOWN) < 0)  
    {  
        Sleep(50);  
        return DOWN;  
    }  
    else if (GetAsyncKeyState(VK_LEFT) < 0)  
    {  
        Sleep(50);  
        return LEFT;  
    }  
    else if (GetAsyncKeyState(VK_RIGHT) < 0)  
    {  
        Sleep(50);  
        return RIGHT;  
    }  
    else if (GetAsyncKeyState(VK_ESCAPE) < 0)  
    {  
        Sleep(50);  
        return ESC;  
    }  
    return 0;  
}  

二、核心模块 2:蛇的移动逻辑(游戏的核心规则)

作用:蛇的 "移动" 不是真的 "动",而是更新坐标:蛇头向前走一步,蛇身每个节点复制前一个节点的坐标,没吃到食物就删掉最后一个节点(吃到则保留,蛇变长)。

复制代码
// 功能:蛇的移动逻辑(核心)+ 碰撞检测 + 吃食物判断  
void move(){  
    int i;            // C89要求循环变量提前声明  
      
    // 1. 蛇身跟随移动:从最后一个节点开始,复制前一个节点的坐标  
    for(i=len-1;i>0;i--){x[i]=x[i-1];y[i]=y[i-1];}  
      
    // 2. 蛇头按方向移动:更新第一个节点的坐标  
    if(dir==UP)y[0]--;        // 向上:Y坐标-1  
    else if(dir==DOWN)y[0]++; // 向下:Y坐标+1  
    else if(dir==LEFT)x[0]--; // 向左:X坐标-1  
    else if(dir==RIGHT)x[0]++; // 向右:X坐标+1  
      
    // 3. 判断是否吃到食物:蛇头坐标=食物坐标  
    if(x[0]==fx&&y[0]==fy){  
        len++;               // 蛇长度+1  
        score+=10;           // 分数+10  
        fx=rand()%(W-2)+1;   // 重新生成食物  
        fy=rand()%(H-2)+1;  
    }  
      
    // 4. 碰撞检测:撞边框(X/Y出界)则游戏结束  
    if(x[0]<=0||x[0]>=W||y[0]<=0||y[0]>=H)over=1;  
    // 5. 碰撞检测:撞自身(蛇头坐标=蛇身坐标)则游戏结束  
    for(i=1;i<len;i++){if(x[0]==x[i]&&y[0]==y[i])over=1;}  
}  

三、核心模块 3:界面绘制(把游戏状态可视化)

作用:把 "蛇的坐标、食物坐标、边框、分数" 转换成控制台的字符(#边框、■蛇头、●蛇身、★食物),让你能看到游戏画面。

注意,每次调用该函数,都会先清屏,再重新绘制边框、食物、蛇头蛇身。

注意,printf字符前,会先用setpos(a,b)函数移动光标到指定坐标,就能精准绘制字符到控制台的对应位置了。

复制代码
// 功能:绘制游戏界面(边框、蛇、食物、分数)  
void draw(){  
    int i;            // C89要求循环变量提前声明  
    cls();            // 清屏(每次绘制前清空旧界面)  
      
    // 绘制上下边框:#号填充,X从0到W,Y=0(上边框)、Y=H+1(下边框)  
    for(i=0;i<=W;i++){setpos(i,0);printf("#");setpos(i,H+1);printf("#");}  
    // 绘制左右边框:#号填充,X=0(左边框)、X=W(右边框),Y从1到H  
    for(i=1;i<=H;i++){setpos(0,i);printf("#");setpos(W,i);printf("#");}  
      
    // 绘制食物:★符号  
    setpos(fx,fy);printf("★");  
    // 绘制蛇:蛇头用■,蛇身用●  
    for(i=0;i<len;i++){setpos(x[i],y[i]);printf(i==0?"■":"●");}  
      
    // 绘制分数:在游戏区域右侧显示  
    setpos(W+2,2);printf("分数:%d",score);  
}  

四、核心模块 4:游戏初始化(开局准备)

作用:游戏开始前,给蛇、食物赋初始值,避免随机值导致的异常。

这里先用了srand(time(0))来设置随机数种子(基于PC时间),这之后rand()函数给出的整数,才是随机的,才能保证食物的位置是随机的。

而蛇头的初始坐标,总是位于游戏区域的正中间,蛇身总是朝右:比如宽度W=20,高度H=10,则x[0]=10, x[1]=9, x[2]=8且y=5。

复制代码
// 功能:初始化蛇和食物(游戏开始/重启时调用)  
void init(){  
    int i;            // C89要求循环变量提前声明  
    srand(time(0));   // 设置随机数种子(基于系统时间,保证食物位置随机)  
    // 初始化蛇身坐标:初始在游戏区域中间,向右排列3个节点  
    for(i=0;i<len;i++){x[i]=W/2-i;y[i]=H/2;}  
    // 随机生成食物坐标:范围在边框内(1~W-1, 1~H-1),避免出界  
    fx=rand()%(W-2)+1;  
    fy=rand()%(H-2)+1;  
}  

五、核心模块 5:主循环(游戏的 "心脏")

作用:把所有模块串起来,循环执行 "绘制→读按键→移蛇→延时",直到游戏结束。

循环逻辑:绘制→读按键→移蛇→延时,每秒执行 5 次(Sleep(200)),这是游戏的帧率;。

这里有个判断条件比如dir!=DOWN非常关键!比如蛇正在向下走,按向上键无效,避免蛇 "回头撞自己",符合贪吃蛇的经典规则。

复制代码
// ===================== 主函数(CVI2010程序入口) =====================  
int main(int argc,char*argv[]){  
    int k;            // 存储按下的按键值  
      
    // CVI2010必须:初始化运行时环境,返回0则初始化失败  
    if(InitCVIRTE(0,argv,0)==0)return -1;  
      
    init();           // 初始化蛇和食物  
    // 游戏主循环:over=0时持续运行  
    while(!over){  
        draw();       // 绘制界面  
        // 非阻塞读取按键:有按键才处理,无则继续移动  
        if(kbhit()){  
            k=getch();// 读取按键  
            // 方向控制:防止反向移动(如向右时不能直接向左)  
            if(k==UP&&dir!=DOWN)dir=UP;  
            else if(k==DOWN&&dir!=UP)dir=DOWN;  
            else if(k==LEFT&&dir!=RIGHT)dir=LEFT;  
            else if(k==RIGHT&&dir!=LEFT)dir=RIGHT;  
            else if(k==27)over=1; // 按ESC键结束游戏  
        }  
        move();       // 蛇移动  
        Sleep(200);   // 控制游戏速度(数值越小越快,200ms=5帧/秒)  
    }  
      
    // 游戏结束:显示最终分数  
    setpos(W/2-5,H/2);printf("游戏结束!分数:%d",score);  
    Sleep(2000);      // 停留2秒,让用户看到分数  
    return 0;         // 程序正常退出  
} 

完整的CVI2010可用的贪吃蛇源码如下,带注释和空行一共160行:

复制代码
// 【CVI2010 专用头文件】必须包含,初始化CVI运行时环境  
#include <cvirte.h>  
// 【Windows API头文件】用于控制台光标控制、按键检测(替代conio.h)  
#include <windows.h>  
// 【标准输入输出】用于printf打印游戏界面  
#include <stdio.h>  
// 【标准库】用于rand()随机数、malloc内存分配  
#include <stdlib.h>  
// 【时间库】用于srand()设置随机数种子  
#include <time.h>  
  
// ===================== 游戏常量定义(C89要求每行一个#define) =====================  
#define W 20          // 游戏区域宽度(边框内可移动范围)  
#define H 10          // 游戏区域高度  
#define UP 1          // 方向键-上 标识  
#define DOWN 2        // 方向键-下 标识  
#define LEFT 3        // 方向键-左 标识  
#define RIGHT 4       // 方向键-右 标识  
  
// ===================== 全局变量定义(简化核心逻辑,新手易理解) =====================  
int x[100];           // 蛇身每个节点的X坐标(数组最大存100个节点,足够用)  
int y[100];           // 蛇身每个节点的Y坐标  
int len=3;            // 蛇的初始长度(3个节点)  
int dir=RIGHT;        // 蛇的初始移动方向(向右)  
int fx, fy;           // 食物的X/Y坐标  
int score=0;          // 游戏分数(吃1个食物+10分)  
int over=0;           // 游戏结束标志(0=继续,1=结束)  
  
// ===================== 替代conio.h的非阻塞键盘函数(CVI2010适配核心) =====================  
// 功能:非阻塞检测是否有按键按下(模拟kbhit())  
// 返回值:1=有按键,0=无按键  
int kbhit(){  
    // GetAsyncKeyState:Windows API,检测按键状态(返回<0表示按下)  
    return GetAsyncKeyState(VK_UP)<0||GetAsyncKeyState(VK_DOWN)<0||  
           GetAsyncKeyState(VK_LEFT)<0||GetAsyncKeyState(VK_RIGHT)<0||  
           GetAsyncKeyState(VK_ESCAPE)<0; // 包含ESC键检测  
}  
  
// 功能:读取按下的按键(模拟getch()),含消抖防止重复触发  
// 返回值:UP/DOWN/LEFT/RIGHT(方向键)、27(ESC键)、0(无有效按键)  
int getch(){  
    if(GetAsyncKeyState(VK_UP)<0){Sleep(50);return UP;}    // 上键,Sleep(50)消抖  
    else if(GetAsyncKeyState(VK_DOWN)<0){Sleep(50);return DOWN;} // 下键  
    else if(GetAsyncKeyState(VK_LEFT)<0){Sleep(50);return LEFT;} // 左键  
    else if(GetAsyncKeyState(VK_RIGHT)<0){Sleep(50);return RIGHT;} // 右键  
    else if(GetAsyncKeyState(VK_ESCAPE)<0){Sleep(50);return 27;}  // ESC键(ASCII码27)  
    return 0; // 无有效按键  
}  
  
// ===================== 控制台控制函数(CVI2010兼容) =====================  
// 功能:设置控制台光标位置(替代C99的复合字面量,兼容C89)  
// 参数:a=X坐标,b=Y坐标  
void setpos(int a,int b){  
    COORD p;          // Windows API的坐标结构体(C89要求先定义变量)  
    p.X=a;            // 赋值X坐标  
    p.Y=b;            // 赋值Y坐标  
    // 设置光标位置:GetStdHandle(STD_OUTPUT_HANDLE)=获取控制台输出句柄  
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),p);  
}  
  
// 功能:清屏(替代system("cls"),解决CVI2010调用system报错问题)  
// 原理:用空格填充整个控制台区域,再把光标移回左上角  
void cls(){  
    COORD p={0,0};    // 光标初始位置(左上角)  
    DWORD w;          // 接收填充的字符数(无实际用途,API要求参数)  
    // FillConsoleOutputCharacter:Windows API,填充指定字符到控制台  
    // 参数:句柄、填充字符(空格)、填充数量(80*25=默认控制台大小)、起始位置、接收填充数  
    FillConsoleOutputCharacter(GetStdHandle(STD_OUTPUT_HANDLE),' ',80*25,p,&w);  
    // 光标移回左上角,完成清屏  
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),p);  
}  
  
// ===================== 游戏核心逻辑函数 =====================  
// 功能:初始化蛇和食物(游戏开始/重启时调用)  
void init(){  
    int i;            // C89要求循环变量提前声明  
    srand(time(0));   // 设置随机数种子(基于系统时间,保证食物位置随机)  
    // 初始化蛇身坐标:初始在游戏区域中间,向右排列3个节点  
    for(i=0;i<len;i++){x[i]=W/2-i;y[i]=H/2;}  
    // 随机生成食物坐标:范围在边框内(1~W-1, 1~H-1),避免出界  
    fx=rand()%(W-2)+1;  
    fy=rand()%(H-2)+1;  
}  
  
// 功能:绘制游戏界面(边框、蛇、食物、分数)  
void draw(){  
    int i;            // C89要求循环变量提前声明  
    cls();            // 清屏(每次绘制前清空旧界面)  
      
    // 绘制上下边框:#号填充,X从0到W,Y=0(上边框)、Y=H+1(下边框)  
    for(i=0;i<=W;i++){setpos(i,0);printf("#");setpos(i,H+1);printf("#");}  
    // 绘制左右边框:#号填充,X=0(左边框)、X=W(右边框),Y从1到H  
    for(i=1;i<=H;i++){setpos(0,i);printf("#");setpos(W,i);printf("#");}  
      
    // 绘制食物:★符号  
    setpos(fx,fy);printf("★");  
    // 绘制蛇:蛇头用■,蛇身用●  
    for(i=0;i<len;i++){setpos(x[i],y[i]);printf(i==0?"■":"●");}  
      
    // 绘制分数:在游戏区域右侧显示  
    setpos(W+2,2);printf("分数:%d",score);  
}  
  
// 功能:蛇的移动逻辑(核心)+ 碰撞检测 + 吃食物判断  
void move(){  
    int i;            // C89要求循环变量提前声明  
      
    // 1. 蛇身跟随移动:从最后一个节点开始,复制前一个节点的坐标  
    for(i=len-1;i>0;i--){x[i]=x[i-1];y[i]=y[i-1];}  
      
    // 2. 蛇头按方向移动:更新第一个节点的坐标  
    if(dir==UP)y[0]--;        // 向上:Y坐标-1  
    else if(dir==DOWN)y[0]++; // 向下:Y坐标+1  
    else if(dir==LEFT)x[0]--; // 向左:X坐标-1  
    else if(dir==RIGHT)x[0]++; // 向右:X坐标+1  
      
    // 3. 判断是否吃到食物:蛇头坐标=食物坐标  
    if(x[0]==fx&&y[0]==fy){  
        len++;               // 蛇长度+1  
        score+=10;           // 分数+10  
        fx=rand()%(W-2)+1;   // 重新生成食物  
        fy=rand()%(H-2)+1;  
    }  
      
    // 4. 碰撞检测:撞边框(X/Y出界)则游戏结束  
    if(x[0]<=0||x[0]>=W||y[0]<=0||y[0]>=H)over=1;  
    // 5. 碰撞检测:撞自身(蛇头坐标=蛇身坐标)则游戏结束  
    for(i=1;i<len;i++){if(x[0]==x[i]&&y[0]==y[i])over=1;}  
}  
  
// ===================== 主函数(CVI2010程序入口) =====================  
int main(int argc,char*argv[]){  
    int k;            // 存储按下的按键值  
      
    // CVI2010必须:初始化运行时环境,返回0则初始化失败  
    if(InitCVIRTE(0,argv,0)==0)return -1;  
      
    init();           // 初始化蛇和食物  
    // 游戏主循环:over=0时持续运行  
    while(!over){  
        draw();       // 绘制界面  
        // 非阻塞读取按键:有按键才处理,无则继续移动  
        if(kbhit()){  
            k=getch();// 读取按键  
            // 方向控制:防止反向移动(如向右时不能直接向左)  
            if(k==UP&&dir!=DOWN)dir=UP;  
            else if(k==DOWN&&dir!=UP)dir=DOWN;  
            else if(k==LEFT&&dir!=RIGHT)dir=LEFT;  
            else if(k==RIGHT&&dir!=LEFT)dir=RIGHT;  
            else if(k==27)over=1; // 按ESC键结束游戏  
        }  
        move();       // 蛇移动  
        Sleep(200);   // 控制游戏速度(数值越小越快,200ms=5帧/秒)  
    }  
      
    // 游戏结束:显示最终分数  
    setpos(W/2-5,H/2);printf("游戏结束!分数:%d",score);  
    Sleep(2000);      // 停留2秒,让用户看到分数  
    return 0;         // 程序正常退出  
}  
相关推荐
C羊驼2 小时前
C 语言:哥德巴赫猜想
c语言·开发语言·人工智能·经验分享·笔记·算法·课程设计
季明洵2 小时前
预处理详解(上)
linux·c语言·数据结构·预定义
不只会拍照的程序猿2 小时前
《嵌入式AI筑基笔记03:Python流程控制,从C的严谨到Python的简洁》
c语言·开发语言·笔记·python
handler012 小时前
算法:字符串哈希
c语言·数据结构·c++·笔记·算法·哈希算法·散列表
雨落在了我的手上3 小时前
C语言之数据结构初见篇(5):单链表的介绍(1)
c语言·开发语言·数据结构
xiangpanf3 小时前
PHP vs C语言:30字解析两大编程语言差异
c语言·开发语言·php
wdfk_prog4 小时前
MAX14830 可移植 C 驱动实现分析:一个适合多串口扩展场景的开源基础版本
c语言·开发语言·开源
Book思议-4 小时前
【数据结构实战】单向循环单链表判别条件理解
c语言·数据结构·算法
Book思议-4 小时前
【数据结构实战】双向链表头插法
c语言·数据结构·链表