1.引言
使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇
实现基本的功能:
- 贪吃蛇地图绘制
- 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞⾃⾝死亡
- 计算得分
- 蛇⾝加速、减速
- 暂停游戏
2.运行图
游戏指引页面
游戏页面
2.涉及知识
- 指针;
- 动态内存;
- 结构体;
- locale本地化字符格式;
- #define宏;
- srand()、time()、rand();
- 枚举;
- 单链表;
- Windows API。
3 Windows API
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程式达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。
3.1 控制台
平常运⾏起来的⿊框程序其实就是控制台程序,可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩。
mode con cols=100 lines=30
通过命令设置控制台窗⼝的名字:
title 贪吃蛇
3.2 控制台屏幕坐标
COORD 是Windows API中定义的⼀种结构,表⽰⼀个字符在控制台屏幕上的坐标。
// 原型
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, * PCOORD;
控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。
3.3 操作句柄
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
// 原型
HANDLE GetStdHandle(DWORD nStdHandle);
例:
// 获取标准输出的句柄(⽤来标识不同设备的数值)
HANDLE stdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
官方文档:GetStdHandle 函数
3.4 控制台屏幕光标
可以指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
// 原型
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
// 原型
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
官方文档:GetConsoleCursorInfo 函数
// 原型
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
官方文档:SetConsoleCursorInfo 函数
例:
HANDLE stdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO consoleCursorInfo;
GetConsoleCursorInfo(stdOutputHandle , &consoleCursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; // 隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo); //设置控制台光标状态
===================================================================
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
// 原型
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
官方文档:SetConsoleCursorPosition 函数
例:
COORD pos = { 10, 5 };
//获取标准输出的句柄(⽤来标识不同设备的数值)
HANDLE stdOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(stdOutputHandle , pos);
3.5 监视按键
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
int vKey
);
官方文档:getAsyncKeyState 函数 (winuser.h))
键值表:虚拟键代码
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
例:
// 用宏比较方便
#define KEY_PRESSED(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
使用这个宏,先到虚拟键代码表去找对应键值,比如 ↑(上箭头 UP ARROW key),在表中说明常量为VK_UP
,值是0x26
,则这么使用这个宏就可以监测到上箭头是否被按下:
KEY_PRESSED(VK_UP); 或者 KEY_PRESSED(0x26);
4. 设计说明
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇身体使⽤宽字符●,打印蛇头使用宽字符■,打印⻝物使⽤宽字符★,宽字符即占两个字节大小的字符,在控制台屏幕占的位置也是比正常键盘敲的字符大一倍。
由于宽字符引入,就不得不设置本地化字符。过去C语⾔并不适合⾮英语国家(地区)使⽤,后来为了使C语⾔适应国家化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊和宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊和<locale.h>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
在标准可以中,依赖地区的部分有以下⼏项:
• 数字量的格式
• 货币量的格式
• 字符集
• ⽇期和时间的表⽰形式
setlocale函数
char* setlocale (int category, const char* locale);
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,指定⼀个类项:
• LC_COLLATE
• LC_CTYPE
• LC_MONETARY
• LC_NUMERIC
• LC_TIME
• LC_ALL - 针对所有类项修改
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。C标准给第⼆个参数仅定义了2种可能取值:“C"和” "。
在任意程序执⾏开始,都会隐藏式执⾏调⽤。
当地区设置为"C"时,库函数按正常⽅式执⾏:
setlocale(LC_ALL, "C");
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出:
setlocale(LC_ALL, " "); // 切换到本地环境
宽字符打印:
wchar_t ch1 = L'●';
wchar_t ch2 = L'★';
wprintf(L"%c\n", ch1);
wprintf(L"%c\n", ch2);
5. 完整代码
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void game() {
// 1.创建一条贪吃蛇
Snake snake = { NULL };
// 2.游戏开始(初始化)
Init(&snake);
// 3.游戏进行
Play(&snake);
// 4.游戏结束
GameOver(&snake);
}
int main() {
// 1.本地化字符格式
setlocale(LC_ALL, "");
// 2.游戏逻辑
game();
return 0;
}
snake.h文章来源:https://www.toymoban.com/news/detail-803238.html
#pragma once
#include <stdio.h> // 标准输入输出
#include <locale.h> // 本地化格式
#include <stdlib.h> // 动态内存、system()
#include <Windows.h> // Windows API
#include <stdbool.h> // 布尔值
#include <time.h> // 时间戳
#define WALL_WIDTH 80 // 墙宽
#define WALL_HEIGHT 32 // 墙高
#define BRICK L'□' // 墙身
#define SNAKE_BODY L'●' // 蛇身
#define SNAKE_HEAD L'■' // 蛇头
#define FOOD L'★' // 食物
#define POS_X 34 // 蛇头起始X位置
#define POS_Y 15 // 蛇头起始Y位置
// 蛇初始长度
#define DEFAULT_LENGTH 4
// 蛇走一步休息的间隔时间(毫秒),影响蛇移动速度
#define PACE_INTERVAL_SLOW 250
#define PACE_INTERVAL_MODERATE 200
#define PACE_INTERVAL_QUICK 150
#define PACE_INTERVAL_EXTREME 100
#define PACE_INTERVAL_GREEDY 50
// 食物的分数(受移动速度影响)
#define SLOW_SCORE 10
#define MODERATE_SCORE (SLOW_SCORE*1.5)
#define QUICK_SCORE (SLOW_SCORE*2)
#define EXTREME_SCORE (SLOW_SCORE*2.5)
#define GREEDY_SCORE (SLOW_SCORE*3)
// 按键是否被按下
#define KEY_PRESSED(VK) ((GetAsyncKeyState(VK) & 0x1) ? true : false)
// 蛇身
typedef struct SnakeNode {
int x;
int y;
struct SnakeNode* next;
} SnakeNode, * pSnakeNode;
// 蛇移动的方向
typedef enum DIRECTION {
UP = 1,
DOWN,
LEFT,
RIGHT
} DIRECTION;
// 游戏状态
typedef enum GAME_STATE {
OK, // 游戏正常进行
EXIT, // 正常退出
KILL_BY_WALL, // 蛇头撞墙
KILL_BY_SELF // 吃到尾巴
} GAME_STATE;
// 蛇状态
typedef struct Snake {
pSnakeNode _pSnakeHead; // 蛇头指针维护蛇链表
pSnakeNode _pFood; // 维护食物的指针
double _totalScore; // 总分数
int _paceInterval; // 移动速度
int _length; // 蛇的长度
double _foodScore; // 吃一个食物能得到的分数值(加速减速有影响)
DIRECTION _direction; // 移动方向
GAME_STATE _gameState; // 游戏状态
} Snake, * pSnake;
// 设置光标位置
void SetCursorPosition(short x, short y);
// 游戏开始(初始化)
void Init(pSnake pSnake);
// 主界面
void Welcome();
// 地图
void CreateMap();
// 初始化蛇
void InitSnake(pSnake ps);
// 打印蛇
void ShowSnake(pSnakeNode psHead);
// 食物
void CreateFood(pSnake ps);
// 游戏进行
void Play(pSnake ps);
// 暂停
void Pause();
// 蛇移动
void Move(pSnake ps);
// 移动的位置是否有食物
bool IsFood(pSnakeNode pNext, pSnakeNode pFood);
// 吃食物
void EatFood(pSnake ps, pSnakeNode pNext);
// 不吃食物
void NoFood(pSnake ps, pSnakeNode pNext);
// 移动约束
void CheckMove(pSnake ps);
// 游戏结束(释放资源等)
void GameOver(pSnake ps);
snake.c文章来源地址https://www.toymoban.com/news/detail-803238.html
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
// 游戏开始(初始化)
void Init(pSnake ps)
{
// 1.控制台窗口大小、游戏名
system("title 贪吃蛇"); // 失效,不知原因
system("mode con cols=128 lines=32");
// 2.隐藏光标
HANDLE stdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
GetConsoleCursorInfo(stdOutput, &cursorInfo);
cursorInfo.bVisible = false;
SetConsoleCursorInfo(stdOutput, &cursorInfo);
// 3.主界面
Welcome();
// 4.地图
CreateMap();
// 5.蛇
InitSnake(ps);
// 6.食物
srand((unsigned int)time(NULL));
CreateFood(ps);
}
// 设置光标位置
void SetCursorPosition(short x, short y) {
COORD pos = { x, y };
HANDLE stdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(stdOutput, pos);
}
// 主界面
void Welcome()
{
SetCursorPosition(50, 10);
printf("WELCOME TO GREEDY SNAKE\n");
SetCursorPosition(52, 18);
system("pause");
system("cls");
SetCursorPosition(52, 8);
printf("移动:WASD 或↑↓←→");
SetCursorPosition(52, 10);
printf("加速:J 或 1");
SetCursorPosition(52, 12);
printf("减速:K 或 2");
SetCursorPosition(52, 14);
printf("退出游戏:ESC");
SetCursorPosition(52, 16);
printf("暂停游戏:空格");
SetCursorPosition(52, 20);
system("pause");
system("cls");
// 避免造成一进游戏就暂停游戏
KEY_PRESSED(VK_SPACE); // space
// 避免一进游戏就加速了
KEY_PRESSED(0x4A); // J
KEY_PRESSED(0x31); // 1
// 避免一进游戏就已经被改变了移动方向(默认向右)
KEY_PRESSED(0x57); // W
KEY_PRESSED(0x53); // S
KEY_PRESSED(VK_UP); // ↑
KEY_PRESSED(VK_DOWN); // ↓
}
// 地图界面
void CreateMap() {
SetCursorPosition(0, 0);
for (int i = 0; i < WALL_WIDTH; i+=2)
{
wprintf(L"%lc", BRICK);
}
SetCursorPosition(0, WALL_HEIGHT-1);
for (int i = 0; i < WALL_WIDTH; i+=2)
{
wprintf(L"%lc", BRICK);
}
for (int i = 1; i <= WALL_HEIGHT-2; i++)
{
SetCursorPosition(0, i);
wprintf(L"%lc", BRICK);
}
for (int i = 1; i <= WALL_HEIGHT-2; i++)
{
SetCursorPosition(WALL_WIDTH-2, i);
wprintf(L"%lc", BRICK);
}
// 提示信息
SetCursorPosition(94, 16);
printf("移动:WASD 或↑↓←→");
SetCursorPosition(94, 18);
printf("加速:J 或 1");
SetCursorPosition(94, 20);
printf("减速:K 或 2");
SetCursorPosition(94, 22);
printf("退出游戏:ESC");
SetCursorPosition(94, 24);
printf("暂停游戏:空格");
}
// 初始化蛇
void InitSnake(pSnake ps) {
// 链表形式,蛇初始长度为4
pSnakeNode cur = NULL;
for (int i = 0; i < DEFAULT_LENGTH; i++)
{
cur = (SnakeNode*)calloc(1, sizeof(SnakeNode));
if (cur == NULL) {
perror("InitSnake(pSnake): malloc");
return;
}
cur->x = POS_X + i * 2;
cur->y = POS_Y;
// 形成蛇身
if (ps->_pSnakeHead == NULL) {
ps->_pSnakeHead = cur;
}
else {
cur->next = ps->_pSnakeHead;
ps->_pSnakeHead = cur;
}
}
// 打印蛇头和蛇身
ShowSnake(ps->_pSnakeHead);
// 蛇状态
ps->_totalScore = 0;
ps->_direction = RIGHT;
ps->_gameState = OK;
ps->_paceInterval = PACE_INTERVAL_SLOW;
ps->_pFood = NULL;
ps->_foodScore = SLOW_SCORE;
ps->_length = DEFAULT_LENGTH;
}
// 打印蛇
void ShowSnake(pSnakeNode psHead) {
// 蛇头
SetCursorPosition(psHead->x, psHead->y);
wprintf(L"%lc", SNAKE_HEAD);
pSnakeNode cur = psHead->next;
// 蛇尾
while (cur) {
SetCursorPosition(cur->x, cur->y);
wprintf(L"%lc", SNAKE_BODY);
cur = cur->next;
}
}
// 食物
void CreateFood(pSnake ps) {
// 随机产生食物
short foodPosX = 0;
short foodPosY = 0;
generatePosAgain:
do {
// 食物坐标必须在墙内:x>=2&&x<=76 y>=1&&y<=31
foodPosX = rand() % (WALL_WIDTH - 5) + 2;
foodPosY = rand() % (WALL_HEIGHT - 2) + 1;
// 食物x坐标必须是2的倍数
} while (foodPosX % 2 != 0);
// 食物坐标不能与蛇身坐标冲突
pSnakeNode cur = ps->_pSnakeHead;
while (cur) {
if (foodPosX == cur->x && foodPosY == cur->y) {
goto generatePosAgain;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL) {
perror("CreateFood: malloc() error");
return;
}
else {
pFood->x = foodPosX;
pFood->y = foodPosY;
pFood->next = NULL;
ps->_pFood = pFood;
SetCursorPosition(foodPosX, foodPosY);
wprintf(L"%c", FOOD);
}
}
// 游戏运行
void Play(pSnake ps) {
do {
// 分数更新提示
SetCursorPosition(94, 8);
printf("当前分数:%.1lf", ps->_totalScore);
// 长度更新提示
SetCursorPosition(94, 10);
printf("当前长度:%d", ps->_length);
// 速度更新提示
SetCursorPosition(94, 12);
char* speed = NULL;
if (ps->_paceInterval == PACE_INTERVAL_GREEDY) {
speed = "疯狂";
}
else if (ps->_paceInterval == PACE_INTERVAL_EXTREME) {
speed = "极速";
} else if (ps->_paceInterval == PACE_INTERVAL_QUICK) {
speed = "快速";
}
else if (ps->_paceInterval == PACE_INTERVAL_MODERATE) {
speed = "中速";
}
else {
speed = "慢速";
}
printf("当前速度:%s", speed);
// 按键监视
if ((KEY_PRESSED(VK_UP) || KEY_PRESSED(0x57)) && ps->_direction != DOWN) { // 上
ps->_direction = UP;
}
else if ((KEY_PRESSED(VK_DOWN) || KEY_PRESSED(0x53)) && ps->_direction != UP) { // 下
ps->_direction = DOWN;
}
else if ((KEY_PRESSED(VK_LEFT) || KEY_PRESSED(0x41)) && ps->_direction != RIGHT) { // 左
ps->_direction = LEFT;
}
else if ((KEY_PRESSED(VK_RIGHT) || KEY_PRESSED(0x44)) && ps->_direction != LEFT) { // 右
ps->_direction = RIGHT;
}
else if (KEY_PRESSED(VK_SPACE)) { // 暂停
Pause();
}
else if (KEY_PRESSED(VK_ESCAPE)) { // 退出
ps->_gameState = EXIT;
break;
}
else if (KEY_PRESSED(0x4A) || KEY_PRESSED(0x31)) { // 加速
if (ps->_paceInterval >= PACE_INTERVAL_EXTREME) {
ps->_paceInterval -= 50;
}
}
else if (KEY_PRESSED(0x4B) || KEY_PRESSED(0x32)) { // 减速
if (ps->_paceInterval <= PACE_INTERVAL_MODERATE) {
ps->_paceInterval += 50;
}
}
// 不同速度下食物的分数
if (ps->_paceInterval == PACE_INTERVAL_GREEDY) {
ps->_foodScore = GREEDY_SCORE;
}
else if (ps->_paceInterval == PACE_INTERVAL_EXTREME) {
ps->_foodScore = EXTREME_SCORE;
}
else if (ps->_paceInterval == PACE_INTERVAL_QUICK) {
ps->_foodScore = QUICK_SCORE;
}
else if (ps->_paceInterval == PACE_INTERVAL_MODERATE) {
ps->_foodScore = MODERATE_SCORE;
}
else {
ps->_foodScore = SLOW_SCORE;
}
// 休眠间隔
Sleep(ps->_paceInterval);
// 蛇移动
Move(ps);
// 移动约束:撞墙、或吃到自己
CheckMove(ps);
} while (ps->_gameState == OK);
}
// 暂停游戏
void Pause() {
while (true) {
if (KEY_PRESSED(VK_SPACE)) {
break;
}
Sleep(100);
}
}
// 蛇移动
void Move(pSnake ps) {
pSnakeNode pNext = (pSnakeNode)calloc(1, sizeof(SnakeNode));
if (pNext == NULL) {
perror("Move(pSnake) malloc error");
return;
}
// 四个方向
switch (ps->_direction)
{
case UP:
pNext->x = ps->_pSnakeHead->x;
pNext->y = ps->_pSnakeHead->y - 1;
break;
case DOWN:
pNext->x = ps->_pSnakeHead->x;
pNext->y = ps->_pSnakeHead->y + 1;
break;
case LEFT:
pNext->x = ps->_pSnakeHead->x - 2;
pNext->y = ps->_pSnakeHead->y;
break;
case RIGHT:
pNext->x = ps->_pSnakeHead->x + 2;
pNext->y = ps->_pSnakeHead->y;
break;
default:
break;
}
// 是否吃到食物
if (IsFood(pNext, ps->_pFood)) {
EatFood(ps, pNext);
// 产生新的食物
CreateFood(ps);
}
else {
NoFood(ps, pNext);
}
ShowSnake(ps->_pSnakeHead);
}
// 是否有食物
bool IsFood(pSnakeNode pNext, pSnakeNode pFood) {
if (pNext->x == pFood->x && pNext->y == pFood->y) {
return true;
}
return false;
}
// 吃食物
void EatFood(pSnake ps, pSnakeNode pNext) {
// 头插
pNext->next = ps->_pSnakeHead;
ps->_pSnakeHead = pNext;
// 删除食物节点
free(ps->_pFood);
ps->_pFood = NULL;
// 增加分数
ps->_totalScore += ps->_foodScore;
// 增加长度
ps->_length += 1;
}
// 不吃食物
void NoFood(pSnake ps, pSnakeNode pNext) {
// 头插
pNext->next = ps->_pSnakeHead;
ps->_pSnakeHead = pNext;
// 尾删
pSnakeNode cur = ps->_pSnakeHead;
while (cur->next->next) {
cur = cur->next;
}
SetCursorPosition(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
// 移动约束
void CheckMove(pSnake ps) {
short headX = ps->_pSnakeHead->x;
short headY = ps->_pSnakeHead->y;
// 是否撞墙
if (headX < 2 || headX > WALL_WIDTH - 4 || headY < 1 || headY > WALL_HEIGHT - 1) {
ps->_gameState = KILL_BY_WALL;
}
// 是否吃到自己
pSnakeNode snakeBody = ps->_pSnakeHead->next;
while (snakeBody) {
short bodyX = snakeBody->x;
short bodyY = snakeBody->y;
if ((headX == bodyX && headY == bodyY)) {
ps->_gameState = KILL_BY_SELF;
}
snakeBody = snakeBody->next;
}
}
// 游戏结束(释放资源等)
void GameOver(pSnake ps) {
switch (ps->_gameState)
{
case EXIT:
SetCursorPosition(32, 15);
printf("退出游戏...");
Sleep(1000);
system("cls");
Sleep(1000);
printf("已退出游戏.");
break;
case KILL_BY_WALL:
SetCursorPosition(32, 12);
printf("你被墙杀死了...");
break;
case KILL_BY_SELF:
SetCursorPosition(32, 12);
printf("你被自己杀死了...");
break;
default:
break;
}
// 释放内存
pSnakeNode cur = ps->_pSnakeHead;
while (cur) {
pSnakeNode del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
到了这里,关于【C语言小游戏】贪吃蛇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!