个人简介:双非大二学生
个人博客:Monodye
今日鸡汤:人生就像一盒巧克力,你永远不知道下一块是什么味的
C语言基础刷题:牛客网在线编程_语法篇_基础语法 (nowcoder.com)
一.贪吃蛇游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。
在编程语⾔的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学⽣的编程能⼒和逻辑能⼒。
二.游戏实现过程
大致分为三个大模块:
- GameStart完成游戏的初始化打印
- GameRun游戏运行时各个功能的实现
- GameEnd游戏结束以后的一些善后工作
2.1游戏功能
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
2.2需要掌握的知识
C语⾔函数、枚举、结构体、动态
内存管理、预处理指令、链表、Win32 API等。
三.Win32API
3.1Win32API介绍
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便 称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。
3.2控制台程序
平常我们运⾏起来的⿊框程序其实就是控制台程序 我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
mode con cols=100 lines=30
也可以通过命令设置控制台窗口的名字
title 贪吃蛇
这些命令我们使用C语言的system便可以实现:
#include <stdio.h>
int main()
{
//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
system("mode con cols=100 lines=30");
//设置cmd窗⼝名称
system("title 贪吃蛇");
return 0;
}
这里我们可以在后面实现的时候加一个getchar(),防止程序运行结束无法确定窗口是否设置成功。
这样就设置好了。
3.3控制台屏幕上的坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
通过这样一个结构体我们就可以给得到控制台上的坐标了。
这里我们可以设置一个坐标
COORD pos = { 10, 15 };
3.4GetStdHandle
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标
准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
它就像炒菜的手柄,你想获得API函数的操作权就得有一个可以获得他们的手柄。
举个例子:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
3.5GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息 ,其实就是获得光标的操作权。
举个例子:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
3.6CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息,可以通过它来得到光标的两个参数:可见性,光标所占的百分比,
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
3.7SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
举个例子:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
3.8 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调 ⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
这里我们可以利用上面讲到的知识,实现一个设置光标位置的函数:
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
3.9GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬
起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
我们可以将 GetAsyncKeyState的结果与0x1进行&运算这样1代表按键,0代表没按键。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
四.GameStart
在这个函数里我们主要完成这样几个功能的实现:
设置控制台的信息,窗口的大小,窗口名
隐藏光标
打印欢迎信息
绘制地图
初始化蛇
创建食物
void GameStart(pSnake ps)
{
//设置控制台的信息,窗口的大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
//打印欢迎信息
WelcomeToGame();
//绘制地图
GetMap();
//初始化蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
}
这里需要特殊说明一下这里我们打印蛇身的时候使用的是中文符号,可以下载搜狗输入法特殊字符获得,并且,这些中文字符,是普通字符 的二倍大。
五.GameRun
用来实现这样几个功能:
打印欢迎信息PrintfHelpInfo();
按键的实现switch语句
蛇的移动SnakeMove();sleep()一下移动一下
void GameRun(pSnake ps)
{
PrintfHelpInfo();
do
{
SetPos(62,10);
printf("总分:%5d\n",ps->Score);
SetPos(62,11);
printf("食物的分值:%02d\n",ps->FoodWeight);
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;
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_F3))
{
if (ps->SleepTime >= 80)
{
ps->SleepTime -= 30;
ps->FoodWeight+= 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->FoodWeight > 2)
{
ps->SleepTime += 30;
ps->FoodWeight -= 2;
}
}
SnakeMove(ps);
Sleep(ps->SleepTime);
} while (ps->status==OK);
//getchar();
}
这里是GameRun的基本框架,一些具体的函数嵌套,下文会有详细源码。
六.GameEnd
这个函数主要用来进行游戏结束时的一些善后工作,
打印退出游戏的信息
逐个销毁创建的贪吃蛇蛇身节点
释放食物节点指针
void GameEnd(pSnake ps)
{
SetPos(15,12);
switch (ps->status)
{
case ESC:
printf("主动退出游戏,正常退出\n");
break;
case KILL_BY_WALL:
printf("很遗憾,撞墙了,游戏结束\n");
break;
case KILL_BY_SELF:
printf("很遗憾,咬到自己了,游戏结束了\n");
break;
}
PSnakeNode cur = ps->pSnake;
PSnakeNode del = NULL;
while (cur)
{
del = cur;
cur - cur->next;
free(del);
}
free(ps->pFood);
ps->pFood = NULL;
}
七.贪吃蛇小项目源码
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
void test()
{
//创建蛇
Snake snake = {0};
GameStart(&snake);
//GameRun(&snake);
//GameEnd(&snake);
}
int main()
{
//修改适配为中文环境
setlocale(LC_ALL,"");
test();
return 0;
}
Snake.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>
#include<stdio.h>
#include"stdlib.h"
#include<windows.h>
#include<stdbool.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define Food L'★'
#define POS_X 24
#define POS_Y 5
enum GAME_STATUS
{
OK = 1,
ESC,
KILL_BY_WALL,
KILL_BY_SELF
};
enum DIRECTION
{
UP=1,
DOWN,
LEFT,
RIGHT
};
//蛇身节点的定义
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, *PSnakeNode;
//贪吃蛇
typedef struct Snake
{
PSnakeNode pSnake;
PSnakeNode pFood;
int Score;
int FoodWeight;
int SleepTime;
enum GAME_STATUS status;
enum DIRECTION dir;
}Snake,*pSnake;
void GameStart(pSnake ps);
void WelcomeToGame();
void GetMap();
void InitSnake(pSnake ps);
void CreateFood(pSnake ps);
void GameRun(pSnake ps);
void PrintfHelpInfo();
void SnakeMove(pSnake ps);
int NextIsFood(pSnake ps,PSnakeNode pNext);
void EatFood(pSnake ps, PSnakeNode pNext);
void NotFood(pSnake ps, PSnakeNode pNext);
void killByWall(pSnake ps);
void KillBySelf(pSnake ps);
void GameEnd(pSnake ps);
Snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
void SetPos(int x,int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(handle,pos);
}
void WelcomeToGame()
{
SetPos(35,10);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(38,20);
system("pause");
system("cls");
//功能介绍界面
SetPos(15,10);
printf("用↑.↓.←.→来控制蛇的移动,F3是加速,F4是减速\n");
SetPos(15,11);
printf("加速得到更高的分数\n");
SetPos(38, 20);
system("pause");
system("cls");
}
void GetMap()
{
//上
SetPos(0,0);
int i = 0;
for ( i = 0;i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0,26);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i ++)
{
SetPos(0,i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56,i);
wprintf(L"%lc", WALL);
}
}
void InitSnake(pSnake ps)
{
PSnakeNode cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (PSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake malloc!");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//头插法
if (ps->pSnake == NULL)
{
ps->pSnake = cur;
}
else
{
cur->next = ps->pSnake;
ps->pSnake = cur;
}
}
cur = ps->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->dir = RIGHT;
ps->FoodWeight = 10;
ps->pFood = NULL;
ps->Score = 0;
ps->SleepTime = 200;
ps->status = OK;
}
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 24 + 1;
} while (x%2!=0);
PSnakeNode cur = ps->pSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
PSnakeNode pFood = (PSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood():malloc");
return;
}
pFood->x=x;
pFood->y=y;
ps->pFood = pFood;
SetPos(x,y);
wprintf(L"%lc",Food);
}
void GameStart(pSnake ps)
{
//设置控制台的信息,窗口的大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
//打印欢迎信息
WelcomeToGame();
//绘制地图
GetMap();
//初始化蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
}
void PrintfHelpInfo()
{
SetPos(62,15);
printf("1.不能穿墙,不能咬到自己\n");
SetPos(62,16);
printf("用↑.↓.←.→来控制蛇的移动\n");
SetPos(62,17);
printf("F3是加速,F4是减速\n");
SetPos(62,19);
printf("版权@Monodye\n");
}
void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
int NextIsFood(pSnake ps,PSnakeNode pNext)
{
if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
{
return 1;
}
else
return 0;
}
void EatFood(pSnake ps, PSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
PSnakeNode cur = ps->pSnake;
while (cur)
{
SetPos(cur->x,cur->y);
wprintf(L"%lc",BODY);
cur = cur->next;
}
ps->Score += ps->FoodWeight;
free(ps->pFood);
CreateFood(ps);
}
void NotFood(pSnake ps, PSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
PSnakeNode cur = ps->pSnake;
while (cur->next->next )
{
SetPos(cur->x,cur->y);
wprintf(L"%lc",BODY);
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
void killByWall(pSnake ps)
{
if (ps->pSnake->x == 0 ||
ps->pSnake->x == 56 ||
ps->pSnake->y == 0 ||
ps->pSnake->y == 26)
{
ps->status = KILL_BY_SELF;
}
}
void KillBySelf(pSnake ps)
{
PSnakeNode cur = ps->pSnake->next;
while (cur)
{
if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
{
ps->status = KILL_BY_SELF;
return 0;
}
cur = cur->next;
}
}
void SnakeMove(pSnake ps)
{
PSnakeNode pNext=(PSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL)
{
perror("SnakeMove():malloc()");
return;
}
pNext->next = NULL;
switch (ps->dir)
{
case UP:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y-1;
break;
case DOWN:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y +1;
break;
case LEFT:
pNext->x = ps->pSnake->x-2;
pNext->y = ps->pSnake->y ;
break;
case RIGHT:
pNext->x = ps->pSnake->x+2;
pNext->y = ps->pSnake->y;
break;
}
if (NextIsFood(ps,pNext))
{
EatFood(ps,pNext);
}
else {
NotFood(ps,pNext);
}
killByWall(ps);
KillBySelf(ps);
}
void GameRun(pSnake ps)
{
PrintfHelpInfo();
do
{
SetPos(62,10);
printf("总分:%5d\n",ps->Score);
SetPos(62,11);
printf("食物的分值:%02d\n",ps->FoodWeight);
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;
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_F3))
{
if (ps->SleepTime >= 80)
{
ps->SleepTime -= 30;
ps->FoodWeight+= 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->FoodWeight > 2)
{
ps->SleepTime += 30;
ps->FoodWeight -= 2;
}
}
SnakeMove(ps);
Sleep(ps->SleepTime);
} while (ps->status==OK);
//getchar();
}
void GameEnd(pSnake ps)
{
SetPos(15,12);
switch (ps->status)
{
case ESC:
printf("主动退出游戏,正常退出\n");
break;
case KILL_BY_WALL:
printf("很遗憾,撞墙了,游戏结束\n");
break;
case KILL_BY_SELF:
printf("很遗憾,咬到自己了,游戏结束了\n");
break;
}
PSnakeNode cur = ps->pSnake;
PSnakeNode del = NULL;
while (cur)
{
del = cur;
cur - cur->next;
free(del);
}
free(ps->pFood);
ps->pFood = NULL;
}