【C语言超详细实现贪吃蛇(1)】

本文详细介绍了如何使用C语言实现贪吃蛇游戏的基础部分,包括常量和数据结构定义、Win32API的应用、游戏界面设计、蛇的移动、食物生成、碰撞检测和基本控制。后续会继续探讨游戏主体和结束条件等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C语言实现简易贪吃蛇(详细版)(1)


欢迎来到本博客!今天我们将会一起来学习如何使用C语言实现一个简易版的贪吃蛇游戏!
在本博客中,我们将从头开始一步步地实现这个游戏,包括贪吃蛇的移动、食物的生成、碰撞检测等等。
如果你对C语言、游戏开发或者贪吃蛇游戏有兴趣,那么就跟着我一起来探索吧!
本博客的技术要点有C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等。
下面详细介绍如何实现贪吃蛇。(注意终端要使用Windows控制台主机


想要实现贪吃蛇我们需要考虑到:

  1. 制作地图
  2. 制作蛇的身体
  3. 蛇的状态
  4. 按键控制方向
  5. 制作食物
  6. 记录分数
  7. 蛇的加速减速
  8. 游戏的结束条件

我将贪吃蛇分为基础部分、主体部分、和末尾部分三部分进行介绍。

本篇介绍基础部分

这一部分要介绍的内容如目录。
为了方便管理,这里我们将与蛇有关的分数,状态,方向,加速,减速等信息封装为一个结构体

  • 头文件声明:定义游戏所需的常量、数据结构和函数声明

    #pragma once
      #include <stdlib.h>
      #include <stdio.h>
      #include <windows.h>
      #include <stdbool.h>
      #include <wchar.h>
      #include <locale.h>
      #include <time.h>
      #define BODY L'●'
      #define FOOD L'★'
      //判断按键是否被按下
      #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
      //默认蛇身长度
      #define Length 5
      //蛇的状态
      enum STATUS {
      	OK, //正常
      	KILL_BY_WALL, //撞墙
      	KILL_BY_SELF, //撞到自己
      	END_NORMAL //正常退出
      };
      //蛇的移动
      enum DIR{
      	UP = 1,
      	DOWN,
      	LEFT,
      	RIGHT
      };
      //蛇体
      typedef struct body {
      	int x;
      	int y;
      	struct body* next;
      }body, * Body;
      
      //游戏主体
      typedef struct Snake
      {
      	Body _pSnake;//指向蛇头的指针
      	Body _pFood;//指向食物节点的指针
      	enum DIR _dir;//蛇的方向
      	enum STATUS _status;//游戏的状态
      	int _food_weight;//一个食物的分数
      	int _score;      //总成绩
      	int _sleep_time; //休息时间,时间越短,速度越快,时间越长,速度越慢
      }Snake, * pSnake;
      //长 56
      //宽 27
      //定位光标
      void SetPos(short x, short y);
      
      void GameStart(Snake* ps);
      
      void WelcomeToGame();
      
      void CreateMap();
      
      //创建蛇身
      void CreateBody(Snake** ps);
      
      //创建食物
      void CreateFood(Snake* ps);
      
      //蛇的移动
      void SnakeMove(Snake* ps);
      
      //游戏暂停
      void Pause();
      
      //游戏运行
      void GameRun(pSnake ps);
      
      //游戏结束条件 
      void GameOver(pSnake ps);
    

    刚开始大家可能没看懂,不过别急,接下来会进行详细介绍

  • Win32 API 使用:利用 Win32 API 创建游戏窗口,并处理用户输入等交互操作

    Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32API 也就是Microsoft Windows32位平台的应⽤程序编程接。

    平常我们运行起来的黑框程序其实就是控制台程序
    我们可以使用 cmd命令来设置控制台窗口的长宽 :设置控制台窗口的大小,30行,100列,使用 title命令 来设置窗口的名字。
    这些能在控制台窗口执行的命令,也可以调⽤C语⾔函数system来执行

    #include <stdio.h>
    	 
    int main()
    {
    		system("mode con cols=100 lines=30");
    		//设置窗口名称
    		system("title 贪吃蛇");
    		getchar();
    		return 0;
    }
    

    效果: 效果

    • GetStdHandle函数

      GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。

      HANDLE GetStdHandle(DWORD nStdHandle);
      //列:
      HANDLE hOutput = NULL;
       //获取标准输出的句柄(⽤来标识不同设备的数值)
       hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
      
    • GetConsoleCursorInfo函数

      检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息

      BOOL WINAPI GetConsoleCursorInfo(
      	HANDLE hConsoleOutput,
      	PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
      );
      
      //PCONSOLE_CURSOR_INFO 
      是指向 CONSOLE_CURSOR_INFO结构的指针,
      该结构接收有关主机游标(光标)的信息
        ````
      
    • CONSOLE_CURSOR_INFO函数

      这个结构体,包含有关控制台光标的信息

       typedef struct _CONSOLE_CURSOR_INFO {
      		DWORD dwSize;
      		BOOL bVisible;
       } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
      
      • dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
      • bVisible,游标的可⻅性。如果光标可见,则此成员为 TRUE。
    • SetConsoleCursorInfo函数

      设置指定控制台屏幕缓冲区的光标的大小和可见性。

      BOOL WINAPI SetConsoleCursorInfo(
             HANDLE hConsoleOutput,
             const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
      );
             
            //列;
           //获取标准输出的句柄(⽤来标识不同设备的数值)
      HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
           //隐藏光标操作(创建变量)
      CONSOLE_CURSOR_INFO CursorInfo;
           //获取控制台光标信息
      GetConsoleCursorInfo(hOutput, &CursorInfo);
           //隐藏控制台光标
      CursorInfo.bVisible = false; 
           //设置控制台光标状态
      SetConsoleCursorInfo(hOutput, &CursorInfo);
      
    • 控制台屏幕上的坐标COORD

      COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系
      (0,0) 的原点位于缓冲区的顶部左侧单元格。

      	  typedef struct _COORD {
      		SHORT X;
      		SHORT Y;
      	  } COORD, *PCOORD;
      	  //给坐标赋值
      	  COORD pos = { 10, 15 };
      
    • SetConsoleCursorPosition函数

      设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标 信息放在COORD类型的pos中,调
      ⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。

       		BOOL WINAPI SetConsoleCursorPosition(
       		HANDLE hConsoleOutput,
       		COORD pos
       		);
       		//例:
       		COORD pos = { 10, 5};
       		HANDLE hOutput = NULL;
       		//获取标准输出的句柄(⽤来标识不同设备的数值)
       		hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
       		//设置标准输出上光标的位置为pos
       		SetConsoleCursorPosition(hOutput, pos);
      
    • GetAsyncKeyState函数

      获取按键情况,GetAsyncKeyState的函数原型如下:

        SHORT GetAsyncKeyState(
         	int vKey
        );
      

      将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
      GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明当前按键的状态是按下,如果最⾼是0,说明当前按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
      如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1

        //定义宏
        #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 1) ? 1 : 0 )
      

      参考: 虚拟键码 (Winuser.h) - Win32 apps
      熟悉这些函数以后我们就可以正式开始了!

  • 制作游戏开始界面

    创建初始化游戏的函数进行封装

    void GameStart(Snake* ps)
    { 
       system("mode con cols=100 lines=30");
       system("title 贪吃蛇");
       HANDLE hOutput = NULL;
       //获取标准输出的句柄(⽤来标识不同设备的数值)
       hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    
       CONSOLE_CURSOR_INFO CursorInfo;
       GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
       CursorInfo.bVisible = false; // 设置光标不可见
       SetConsoleCursorInfo(hOutput, &CursorInfo);
       //1. 打印环境界面和功能介绍
       WelcomeToGame();
       //2. 绘制地图
       CreateMap();
       //等等......
    }
    
    • 打印游戏介绍界面:在开始界面提供一个选项,让玩家了解游戏规则和操作说明
      //定位光标函数
      void SetPos(short x, short y)
      {
       	COORD pos = { x, y };
       	HACCEL hehe = NULL;
       	hehe = GetStdHandle(STD_OUTPUT_HANDLE);
       	SetConsoleCursorPosition(hehe, pos);
      }
      
       void WelcomeToGame()
      {
      	//第一页
          //定位光标
      	SetPos(40, 14);
      	//打印
       	printf("欢迎来到贪吃蛇小游戏\n");
       	//定位光标
       	SetPos(42, 20);
       	//暂停
       	system("pause");
       	//清屏
       	system("cls");
       	
       	//第二页
       	SetPos(25, 14);
       	printf("用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
       	SetPos(42, 20);
       	system("pause");
       	system("cls");
       	
       	//第三页
       	SetPos(39, 15);
       	printf("加速能够得到更高的分数\n");
       	SetPos(42, 20);
       	system("pause");
       	system("cls");
      }
      

      效果展示:

      打印游戏介绍界面

    • 打印游戏地图:在游戏界面打印贪吃蛇的初始地图,包括蛇、食物和边界等元素

      注意:

      头文件<locale.h>本地化
      <locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样行为的部分。
      在标准中,依赖地区的部分有以下几项:
      • 数字量的格式
      • 货币量的格式
      • 字符集
      • ⽇期和时间的表⽰形式
      –类项–
      通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部
      分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,
      指定⼀个类项:
      • LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
      • LC_CTYPE:影响字符处理函数的⾏为。
      • LC_MONETARY:影响货币格式。
      • LC_NUMERIC:影响 printf() 的数字格式。
      • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
      • LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
      每个类项的详细说明,请参考
      setlocale函数

      char* setlocale (int category, const char* locale);
      

      – setlocale函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
      – setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参
      数是LC_ALL,就会影响所有的类项。
      C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和" "(本地模式)。
      在任意程序执⾏开始,都会隐藏式执⾏调⽤:

         setlocale(LC_ALL, "C");
      

      当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。
      当程序运⾏起来后想改变地区就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale
      函数就可以切换到本地模式,这种模式下程序会适应本地环境。

      简而言之: 要打印特殊宽字符墙体 “□” 就要将程序适应本地环境

      //适应本地环境
      setlocale(LC_ALL, " ");
      
      • 制作墙体:
      • 函数名称: CreateMap
        • 函数功能: 创建墙体
        • 输入参数: 无
        • 返回值: 无
         void CreateMap()
         {
         	//上
         	int i = 0;
         	for (i = 0; i < 29; i++)
         	{
         		//打印宽字符固定格式,记住就好。
         		wprintf(L"%lc", L'□');
         	}
         	//下
         	SetPos(0, 26);
         	for (i = 0; i < 29; i++)
         	{
         		wprintf(L"%lc", L'□');
         	}
         	//左
         	for (i = 1; i <= 25; i++)
         	{
         		SetPos(0, i);
         		wprintf(L"%lc", L'□');
         	}
         	//右
         	for (i = 1; i <= 25; i++)
         	{
         		SetPos(56, i);
         		wprintf(L"%lc", L'□');
         	}
         	getchar();
         }
        
        效果:
        地图
      • 制作蛇身:
      • 函数名称: CreateBody
        • 函数功能: 创建贪吃蛇的身体,并初始化蛇的相关属性。
        • 输入参数: Snake** ps - 指向贪吃蛇结构体指针的指针,用于更新蛇的信息。
        • 返回值: 无
        void CreateBody(Snake** ps)
        {
            // 循环创建蛇的每个身体节点
            for (int i = 0; i < Length; i++)
            {
                // 为当前身体节点分配内存空间
                Body P = (Body)malloc(sizeof(body));
                // 设置当前节点的位置坐标
                P->x = X + i * 2;
                P->y = Y;
                P->next = NULL;
                
                // 将当前节点添加到蛇身链表中
                if ((*ps)->_pSnake == NULL)
                {
                    // 如果蛇身链表为空,则将当前节点设为蛇身链表的头节点
                    (*ps)->_pSnake = P;
                }
                else {
                    // 否则将当前节点插入到蛇身链表的头部
                    P->next = (*ps)->_pSnake;
                    (*ps)->_pSnake = P;
                }
            }
            
            // 遍历蛇身链表,将蛇身节点显示在游戏界面上
            Body T = (*ps)->_pSnake;
            while (T)
            {
                SetPos(T->x, T->y);
                wprintf(L"%lc", BODY);
                T = T->next;
            }
            
            // 初始化蛇的其他属性
            (*ps)->_dir = RIGHT;        // 蛇的初始移动方向为向右
            (*ps)->_sleep_time = 200;   // 设置蛇的移动速度
            (*ps)->_status = OK;        // 设置蛇的状态为正常
            (*ps)->_food_weight = 10;   // 设置初始食物的重量
        }
        
      • 制作食物:
      • 函数名称: CreateFood
        • 函数功能: 在游戏界面上生成食物,并确保食物不与蛇身重叠。
        • 输入参数: Snake* ps - 指向贪吃蛇结构体的指针,用于获取蛇的信息。
        • 返回值: 无
        void CreateFood(Snake* ps)
        {
              int y = 0;  // 食物的纵坐标
              int x = 0;  // 食物的横坐标
              int i = 0;  // 用于判断食物位置是否与蛇身冲突的标志
              
              // 使用当前时间作为随机数种子
              srand(time(NULL));
              
              // 在随机位置生成食物,直到食物位置不与蛇身冲突为止
              do 
              {
                  i = 0;  // 重置冲突标志
                  
                  // 随机生成食物的纵坐标
                  y = rand() % 23 + 2;
                  
                  // 随机生成食物的横坐标,确保为偶数
                  do
                  {
                      x = rand() % 55 + 1;
                  } while (x % 2 != 0);
                  
                  // 检查食物位置是否与蛇身冲突
                  Body B = ps->_pSnake;
                  while (B)
                  {
                      if (B->x == x && B->y == y)
                      {
                          i = 1;  // 若冲突,则设置标志为1
                          break;
                      }
                      B = B->next;
                  }
              } while (i);  // 若发现冲突,则重新生成食物位置
              
              // 分配内存空间,创建食物节点
              Body F = (Body)malloc(sizeof(body));
              ps->_pFood = F;
              ps->_pFood->next = NULL;
              ps->_pFood->x = x;
              ps->_pFood->y = y;
              
              // 在游戏界面上显示食物
              SetPos(x, y);
              wprintf(L"%lc", FOOD);
        }
        
        效果:
        贪吃蛇

那么关于贪吃蛇第一部分内容到这里结束了,在下篇博客我们会介绍主体部分:

蛇的移动:根据用户输入控制蛇的移动方向,并更新游戏地图。
食物生成:在地图上随机生成食物,并确保不与蛇身重叠。
碰撞检测:检测蛇头是否与边界或蛇身相撞,以及是否吃到了食物。
分数计算:根据蛇吃到的食物数量计算玩家的得分。
等等…

如有什么问题或疑问请大家在评论区指出~
谢谢大家的观看!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ヾ慈城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值