贪吃蛇小游戏 (C语言)

一、 游戏介绍

贪吃蛇(也叫做贪食蛇)游戏是一款休闲类游戏,有PC和手机等多平台版本。 既简单又耐玩。 该游戏通过控制蛇头移动方向吃食物,从而使得蛇变得越来越长。 贪吃蛇游戏最初为单机模式,后续又陆续推出团战模式、赏金模式、挑战模式等多种玩法。本文就来讲解一下简单的贪吃蛇游戏的做法。

二、技术内容

本文将会使用C语言来制作最简单的贪吃蛇游戏。所用知识将包含C语言基础知识和语法,同时也将用到 Win32 API 等其它拓展知识。

三、Win32 API 介绍

API是Application Programming Interface的缩写,即应用程序接口,它是由Win32操作系统提供给程序员的一系列函数接口的集合,这些函数可以对计算机系统进行各种各样的操作,比如创建窗口、描绘图形、使用周边设备等等,它们犹如“积木块”一样,可以搭建出各种界面丰富,功能灵活的应用程序。 简单来说 Win32 API 就是Microsoft Windows 32位平台的应⽤程序编程接⼝。

本文涉及到部分Win32 API 接口。接下来一一讲解。

1.  控制台指令

在贪吃蛇程序运行之后,将弹出一个窗口,这个窗口就是控制台,也将是我们说的 cmd。

我们可以使用控制台指令来设置控制台的大小和名字。

mode指令

使用mode指令可以设置控制台窗口的大小

代码如下:

system("mode con cols=100 lines=32");

这样我们就能将控制台的长设置为100,宽设置为32。

需要注意:控制台的长和宽比例并不是1:1,而是类似于这样的比例。这非常重要,一定要理解。

使用system需要包含头文件stdlib.h

title指令

title指令能修改控制台的名字。

代码如下:

system("title 贪吃蛇");

此时控制台左上角的名字就会改为贪吃蛇

2.GetStdHandle

检索指定标准设备的句柄(标准输入、标准输出或标准错误)。

语法:

HANDLE WINAPI GetStdHandle(
  _In_ DWORD nStdHandle
);

参数的值有以下:

返回值:如果该函数成功,则返回值为指定设备的句柄,或为由先前对 SetStdHandle 的调用设置的重定向句柄。 除非应用程序已使用 SetStdHandle 来设置具有较少访问权限的标准句柄,否则该句柄具有 GENERIC_READ 和 GENERIC_WRITE 访问权限。

如果函数失败,则返回值为 INVALID_HANDLE_VALUE。

如果应用程序没有关联的标准句柄(例如在交互式桌面上运行的服务),并且尚未重定向这些句柄,则返回值为 NULL。

更多信息:GetStdHandle 函数 - Windows Console | Microsoft Learn

3.GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。
语法:
BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE               hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

返回值:

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

4.CONSOLE_CURSOR_INFO

包含有关控制台游标的信息。

语法:

typedef struct _CONSOLE_CURSOR_INFO {
  DWORD dwSize;
  BOOL  bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

成员:

dwSize
由游标填充的字符单元的百分比。 该值介于 1 到 100 之间。 游标外观各不相同,范围从完全填充单元到显示为单元底部的横线。

bVisible
游标的可见性。 如果游标可见,则此成员为 TRUE

因为在游戏当中,我们需隐藏游标,所以我们将bVisible设置为false

CursorInfo.bVisible = false;

5.SetConsoleCursorInfo

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

语法:

BOOL WINAPI SetConsoleCursorInfo(
  _In_       HANDLE              hConsoleOutput,
  _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

参数:

hConsoleOutput [in]
控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。

pConsoleCursorInfo [in]
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构为控制台屏幕缓冲区的光标提供新的规范。

返回值:

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

要将游标隐藏,光将bVisible设置为false还不行,如下:

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false; 
SetConsoleCursorInfo(hOutput, &CursorInfo);

这样才能将游标隐藏。

 6.COORD

COORD定义控制台屏幕缓冲区中字符单元的坐标。 坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。

语法:

typedef struct _COORD {
  SHORT X;
  SHORT Y;
} COORD, *PCOORD;

7.SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置。

语法:

BOOL WINAPI SetConsoleCursorPosition(
  _In_ HANDLE hConsoleOutput,
  _In_ COORD  dwCursorPosition
);

参数:

hConsoleOutput [in]
控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。

dwCursorPosition [in]
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内。

返回值:

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。

8.GetAsyncKeyState

确定调用函数时键是向上还是向下,以及上次调用 GetAsyncKeyState 后是否按下了该键。

语法:

SHORT GetAsyncKeyState(
  [in] int vKey
);

参数

[in] vKey

类型: int

虚拟密钥代码。 有关详细信息,请参阅 虚拟密钥代码

可以使用左右区分常量来指定某些键。

返回值

类型: SHORT

如果函数成功,则返回值指定自上次调用 GetAsyncKeyState 以来是否按下了键,以及键当前是打开还是关闭。 如果设置了最有效位,则键关闭,如果设置了最小有效位,则上一次调用 GetAsyncKeyState 后按下了键。

对于以下情况,返回值为零:

  • 当前桌面不是活动桌面
  • 前台线程属于另一个进程,桌面不允许挂钩或日志记录。

以上就是将使用到的部分Win32 API知识。

四、贪吃蛇完整代码

snake.h

#pragma once

#pragma once

#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<stdio.h>
#include<time.h>

//蛇初始位置
#define POS_X 24
#define POS_Y 5

#define WALL L'□'
#define BODY L'●'
#define FOOD L'▲'

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )


//贪吃蛇节点 
typedef struct SnakeNode
{
	//身体坐标
	int x;
	int y;

	struct SnakeNode* next;
}SnakeNode;

//蛇的状态
enum SNAKE_STATUS
{
	OK,
	KILL_BY_WALL,
	KILL_BY_SELF,
	END_NORMAL
};

//蛇的移动方向
enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的内容
typedef struct Snake
{
	SnakeNode* pSnake;//指向蛇的头节点
	SnakeNode* pFood;//指向食物节点
	enum SNAKE_STATUS pStatus;//蛇的状态
	enum DIRECTION dir;//蛇的移动方向
	int food_weight;//一个食物的分数
	int score;//总分数
	int sleep_time;//休息时间,时间越长,移动越慢,时间越短,移动越快
}Snake;

//初始化游戏
void GameStart(Snake* snake);

//设置光标的坐标
void SetPos(short x, short y);

//欢迎界面
void WelcomeToGame();

//创建地图
void CreatMap();

//创建蛇身
void InitSnake(Snake* snake);

//创建食物
void CreateFood(Snake* snake);

//运行游戏
void GameRun(Snake* snake);

//游戏信息
void GameInfo(Snake* snake);

void GameStop();

void SnakeFast(Snake* snake);
void SnakeSlow(Snake* snake);

//蛇的移动
void SnakeMode(Snake* snake);


//结束游戏
void GameEnd(Snake* snake);

snake.c

#include"snake.h"

//设置光标的坐标
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(hOutput, pos);
}

// 欢迎界面
void WelcomeToGame()
{
	SetPos(40, 16);
	wprintf(L"欢迎进入贪吃蛇游戏");
	SetPos(40, 28);
	system("pause");
	system("cls");
	SetPos(35, 15);
	wprintf(L"用 ↑↓←→ 来控制蛇的移动,F3加速,F4减速");
	SetPos(35, 16);
	wprintf(L"加速可获得更多分数");
	SetPos(40, 28);
	system("pause");
	system("cls");
}

//创建地图
void CreatMap()
{
	//上
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

//创建蛇身
void InitSnake(Snake* snake)
{
	int i = 0;
	for (i = 0; i < 5; i++)//初始有5个蛇身
	{
		SnakeNode* newSnakeNode = (SnakeNode*)malloc(sizeof(SnakeNode));
		if (newSnakeNode == NULL)
		{
			perror("malloc() fail");
			return;
		}

		//每个蛇身的信息
		newSnakeNode->next = NULL;
		newSnakeNode->x = POS_X + i * 2;
		newSnakeNode->y = POS_Y;

		//头插法插入节点
		if (snake->pSnake == NULL)
		{
			snake->pSnake = newSnakeNode;
		}
		else
		{
			newSnakeNode->next = snake->pSnake;
			snake->pSnake = newSnakeNode;
		}
	}

	//打印蛇身
	SnakeNode* cur = snake->pSnake;
	for (i = 0; i < 5; i++)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	snake->dir = RIGHT;
	snake->food_weight = 10;
	snake->score = 0;
	snake->sleep_time = 200;
	snake->pStatus = OK;
}

//创建食物
void CreateFood(Snake* snake)
{
	int x = 0;
	int y = 0;
	//创建食物节点
	SnakeNode* food = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (food == NULL)
	{
		perror("malloc()fail");
		return;
	}

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	SnakeNode* cur = snake->pSnake;
	while (cur)
	{
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	food->x = x;
	food->y = y;
	food->next = NULL;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);

	snake->pFood = food;
}


void GameStart(Snake* snake)
{
	//0. 设置画面大小,隐藏光标
	system("mode con cols=100 lines=32");
	system("title 贪吃蛇");

	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//影藏光标操作
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

	//1. 欢迎界面
	WelcomeToGame();

	//2. 创建地图
	CreatMap();

	//3. 创建蛇身,蛇的属性
	InitSnake(snake);
	
	//4. 创建食物
	CreateFood(snake);
}

//游戏信息
void GameInfo(Snake* snake)
{
	
	SetPos(68, 17);
	printf("F3加速,F4减速");
	SetPos(68, 18);
	printf("按ESC退出游戏,SPACE 暂停游戏");

}

void GameStop()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

void SnakeFast(Snake* snake)
{
	if (snake->sleep_time > 80)
	{
		snake->sleep_time -= 20;
		snake->food_weight += 2;
	}
}

void SnakeSlow(Snake* snake)
{
	if (snake->sleep_time < 280)
	{
		snake->sleep_time += 20;
		snake->food_weight -= 2;
	}
}

int NextIsFood(Snake* snake, SnakeNode* NextNode)
{
	return snake->pFood->x == NextNode->x && snake->pFood->y == NextNode->y;
}

void EatFood(Snake* snake, SnakeNode* NextNode)
{
	NextNode->next = snake->pSnake;
	snake->pSnake = NextNode;

	SnakeNode* cur = snake->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	free(snake->pFood);
	snake->pFood = NULL;

	snake->score += snake->food_weight;
	CreateFood(snake);
}

void NextNoFood(Snake* snake, SnakeNode* NextNode)
{
	NextNode->next = snake->pSnake;
	snake->pSnake = NextNode;

	SnakeNode* cur = snake->pSnake;
	while (cur->next->next)
	{
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;

	cur = snake->pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
}

//撞墙
void kill_by_wall(Snake* snake)
{
	if (snake->pSnake->x == 0 || snake->pSnake->x == 56 
		|| snake->pSnake->y == 0 || snake->pSnake->y == 27)
	{
		snake->pStatus = KILL_BY_WALL;
	}
}

//撞到自己
void kill_by_self(Snake* snake)
{
	SnakeNode* cur = snake->pSnake->next;
	while (cur)
	{
		if (cur->x == snake->pSnake->x && cur->y == snake->pSnake->y)
		{
			snake->pStatus = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

void SnakeMode(Snake* snake)
{
	SnakeNode* NextNode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (NextNode == NULL)
	{
		perror("malloc()fail");
		return;
	}

	switch(snake->dir)
	{
	case UP:
		NextNode->x = snake->pSnake->x;
		NextNode->y = snake->pSnake->y - 1;
		break;
	case DOWN:
		NextNode->x = snake->pSnake->x;
		NextNode->y = snake->pSnake->y + 1;
		break;
	case LEFT:
		NextNode->x = snake->pSnake->x - 2;
		NextNode->y = snake->pSnake->y;
		break;
	case RIGHT:
		NextNode->x = snake->pSnake->x + 2;
		NextNode->y = snake->pSnake->y;
		break;
	}

	if (NextIsFood(snake, NextNode))
	{
		EatFood(snake, NextNode);
	}
	else
	{
		NextNoFood(snake, NextNode);
	}
	
	//撞墙
	kill_by_wall(snake);

	//撞到自己
	kill_by_self(snake);
}


//运行游戏
void GameRun(Snake* snake)
{
	//游戏信息
	GameInfo(snake);
	do
	{
		SetPos(68, 14);
		printf("一个食物的分数:%2d", snake->food_weight);
		SetPos(68, 15);
		printf("总分数:%d", snake->score);
		if (KEY_PRESS(VK_UP) && snake->dir != DOWN)
		{
			snake->dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && snake->dir != UP)
		{
			snake->dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && snake->dir != RIGHT)
		{
			snake->dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && snake->dir != LEFT)
		{
			snake->dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			snake->pStatus = END_NORMAL;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			GameStop();
		}
		else if (KEY_PRESS(VK_F3))
		{
			SnakeFast(snake);
		}
		else if (KEY_PRESS(VK_F4))
		{
			SnakeSlow(snake);
		}

		//蛇的移动
		SnakeMode(snake);
		Sleep(snake->sleep_time);

	} while (snake->pStatus == OK);
}

//结束游戏
void GameEnd(Snake* snake)
{
	SetPos(25, 14);
	switch (snake->pStatus)
	{
	case END_NORMAL:
		printf("正常退出游戏");
		break;
	case KILL_BY_SELF:
		printf("撞到自己,游戏结束");
		break;
	case KILL_BY_WALL:
		printf("撞到墙,游戏结束");
		break;
	}

	SnakeNode* cur = snake->pSnake;
	while (cur)
	{
		SnakeNode* del = cur;
		cur = cur->next;
		free(del);
		del = NULL;
	}
}

test.c


#include <locale.h>
#include  "snake.h"

test()
{
	int ch = 0;
	do
	{
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };

		//初始化游戏
		GameStart(&snake);
		//包括:
		//1. 欢迎界面
		//2. 创建地图
		//3. 创建蛇
		//4. 创建食物


		//运行游戏
		GameRun(&snake);

		//结束游戏
		GameEnd(&snake);
		SetPos(25, 23);
		system("pause");
		SetPos(25, 24);
		printf("是否再玩一局?(Y/N):");
		ch = getchar();
		while(getchar()!='\n');
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 28);
}


int main()
{
	//本地化
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));

	test();

	return 0;
}

注意:设置本地化信息setlocale()

使用说明:setlocale - C++ Reference (cplusplus.com)

因为我们会使用到宽字符的打印,所以需要进行本地化设置。

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值