文章目录
1. 贪吃蛇介绍
贪吃蛇游戏想必大家并不陌生,它的玩法很简单,通过上下左右控制贪吃蛇的移动,让它吃到地图上某个位置的食物,每次吃到食物,小蛇就会变长一段,看起来是不是很神奇呢!其实这个游戏的设计原理十分简单,通过本篇文章你可以学会使用C语言设计简单的贪吃蛇小游戏。在学习之前你只需要有以下知识的基础:
- C语言基础:基本数据类型、循环语句、switch 条件语句,宏定义,struct 结构体,函数的定义与使用
- DevCpp 工具的基本使用:编译 和运行 .c 文件
如果你有以上这些基础,我相信只需要10分钟你就能掌握贪吃蛇小游戏的编写技巧。
如上图所示,这个小游戏最主要的两个部分就是 画面 和 操作。
首先,小游戏绘制出了一个范围表示贪吃蛇允许的运动范围,以及不断运动着的小蛇。
其次,通过画面我们可以通过键盘来操作小蛇的方向,在这个画面中主要有 上、下、左、右 四种方向。
在写游戏代码前,我们有必要先了解一下如何用 C语言来实现小游戏的画面以及获取用户的操作。
2. 前置准备
2.1 C语言移动光标
windows.h 头文件支持许多与 Windows 系统相关的功能,这里我们主要是使用它里面当中可以获取运行的窗口,运行的坐标相关的方法。
参考:https://docs.microsoft.com/zh-cn/windows/console/setconsolecursorposition
#include <windows.h>
/*--------------------移动光标--------------------- */
void gotoxy(int x,int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); // 获取当前运行程序的窗口
COORD coord; // 获取光标
coord.X = x; // 设置坐标
coord.Y = y;
SetConsoleCursorPosition(handle,coord); // 设置指定控制台屏幕缓冲区中的光标位置。
}
测试:
#include <stdio.h>
int main()
{
gotoxy(2,2);
printf("hello");
}
运行结果:
2.2 C语言读取键盘按键
读取用户的键盘按键则通过 conio.h 这个头文件,它的 _kbhit() 方法 判断用户是否按下某个键,它的最大特点是:如果用户没有按下任何按键,则会返回 false,若按下了按键则返回 true, 同时需配合 _getch() 函数来获取到用户之前按下的键对应的 ASCII码。
#include <conio.h>
#include <stdio.h>
/* ---获取用户按键--- */
int keyDown()
{ int key = -1;
if(_kbhit())
{
fflush(stdin); // 刷新控制台输入的缓冲区
key=_getch(); // 读取键盘的按键
}
return key;
}
测试:
int main(){
int key = 0;
while(1){
key = keyDown(); // 读取用户的键盘按键, 若没有则执行下一行内容
if(key != -1)
printf("按下的按键为: %c\n", char(key));
}
}
运行结果:
2.3 C语言延迟生成随机数
在游戏过程中,我们发现食物的位置每次都是随机的,所以我们需要有生成随机数的函数。
#include <time.h>
#include <stdlib.h>
/*--------获取 [a, b) 范围的随机整数------*/
int randomIn(int a, int b){
srand((unsigned int)time(NULL));
return rand() % b + a;
}
测试:( 输出10次 [0, 10) 的随机整数 )
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
/*--------获取 [a, b) 范围的随机整数------*/
int randomIn(int a, int b){
srand((unsigned int)time(NULL));
return rand() % b + a;
}
int main(){
for(int i = 0; i < 10; i++){
Sleep(1000); // 延迟 1秒
printf("第 %d 个随机数 : %d\n" , i+1, randomIn(0, 10));
}
}
运行效果:
2.4 C语言隐藏光标
为了防止游戏不断闪烁干扰实现,我们需要调用 API 来实现控制台窗口隐藏光标
/*-------------------- 隐藏光标 -------------------- */
void hideCursor()
{
CONSOLE_CURSOR_INFO cursor;
cursor.bVisible = FALSE;
cursor.dwSize = sizeof(cursor);
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorInfo(handle, &cursor);
}
3. 实现贪吃蛇小游戏
3.1 绘制游戏边界
#define MAP_HEIGHT 25
#define MAP_WIDTH 60
#define WALL "■"
/*----------------绘制地图------------- */
void drawMap(){
// 绘制左右边界
for(int i=0;i<=MAP_HEIGHT;i++) // 遍历指定的地图高度
{
gotoxy(0,i); // 将光标移动到边界最左边的位置
printf(WALL);
gotoxy(MAP_WIDTH,i); // 将光标移动到边界最右边的位置
printf(WALL);
}
// 绘制上下边界
for(int i=0;i<=MAP_WIDTH;i+=2) // 遍历指定的地图宽度
{ // 由于 ■ 符号在水平方向上是占两个字符的, 所以地图也相应的隔两个位置
gotoxy(i,0); // 将光标移动到边界最上边的位置
printf(WALL);
gotoxy(i,MAP_HEIGHT);
printf(WALL); // 将光标移动到边界最下边的位置
}
}
运行结果:
3.2 绘制小蛇
在绘制小蛇以前我们要知道它有哪些属性:
- 蛇的长度
- 蛇的移动速度
- 蛇头和蛇身的位置(横坐标, 纵坐标)
我们可以用 struct 结构体来表示蛇的结构
#define SNAKE_SIZE 100 // 蛇头加上蛇身的最大节数
struct
{
int x[SNAKE_SIZE]; // 蛇头和蛇身的横坐标
int y[SNAKE_SIZE]; // 蛇头和蛇身的纵坐标
int len; // 蛇长
int speed; // 移动速度
int direction; // 移动方向
} snake;
接下来我们定义绘制小蛇的函数,和之前绘制边界的思路类似,先移动光标,然后再printf 打印
#define SNAKE "■"
/* ----------- 绘制小蛇 -------------*/
void drawSnake(){
for(int i=0;i < snake.len; i++)
{
gotoxy(snake.x[i],snake.y[i]);
printf(SNAKE);
}
}
3.3 擦除小蛇尾部
根据之前移动小蛇的分析,我们知道在小蛇移动后,它的尾巴是需要抹去的,否则小蛇在移动的时候身子就会越来越长,这里为方便之后调用,定义可以擦除游戏画面的函数。
/*------------------ 擦除画面 --------------------*/
void clear(int x, int y)
{
gotoxy(x, y);
printf(" ");
}
3.4 绘制食物
和绘制小蛇类似,我们先定义一个表示食物的结构体:
struct
{
int x;
int y;
}food;
在本次小游戏的设计中,食物只有一个,所以就不需要以数组的形式存储了。
#define FOOD "●"
/*----------绘制食物----------*/
void drawFood(){
gotoxy(food.x, food.y);
printf(FOOD);
}
在绘制食物时,我们需要考虑绘制的位置,食物的位置是随机的,但是不能在小蛇的蛇头或者蛇身上。
/*------------ 判断坐标是否在蛇头或蛇身上--------------*/
bool isInSnake(int x,int y){
for(int i = 0; i < snake.len; i++)
if(snake.x[i] == x && snake.y[i] == y)
return true;
return false;
}
/*------------ 随机生成食物的位置 --------*/
void randomFoodPosition(){
do {
food.x = randomIn(2, MAP_WIDTH - 4); // 食物在水平方向上必须在围墙内
food.y = randomIn(1, MAP_HEIGHT - 2); // 食物在竖直方向上必须在围墙内
} while(isInSnake(food.x, food.y) || food.x % 2 != 0); // 当坐标不合理时, 则重新生成
}
3.5 移动小蛇
移动小蛇是有规律的,我们再次观察之前的动态图:
蛇头用于控制方向,如果用户没有按下任何键,那么小蛇会一直往那个方向前进,在移动过程中,蛇身的每一节总会往它靠近蛇头的那一节蛇身移动。
通过上面的分析,我们将 蛇头 和 蛇身 分开考虑:
- 蛇头:根据移动方向移动,比如向右,那么就向右移动一格
- 蛇身:当前这节蛇身朝着移动前靠近蛇头的那一节蛇身的位置移动,直接替换位置即可
除了绘制小蛇移动后的画面以外,我们还需要擦除它之前的尾巴,这个直接移动光标到之前尾巴的部分,printf 打印两个空格即可。在控制台中,光标的移动规律是如下图所示的,通过这个规律我们能总结出对于坐标 (x, y) ,它的上下左右四个坐标的特点:
- 上 ( x, y -1 )
- 下 ( x, y + 1)
- 左 ( x - 1, y )
- 右 ( x + 1, y )
#define D_UP 'w'
#define D_RIGHT 'd'
#define D_DOWN 's'
#define D_LEFT 'a'
/*----------移动小蛇 --------*/
void move(){
clear(snake.x[snake.len - 1], snake.y[snake.len -1]);
// 先移动蛇身
for(int i = snake.len - 1; i > 0 ; i--){
snake.x[i] = snake.x[i-1] + 2;
snake.y[i] = snake.y[i-1];
}
// 控制蛇头
switch (snake.direction){
case UP:
snake.y[0]--;
break;
case DOWN:
snake.y[0]++;
break;
case LEFT:
snake.x[0] -= 2;
break;
case RIGHT:
snake.x[0] += 2;
break;
}
drawSnake();
}
3.6 小蛇吃到食物
当蛇头遇到食物时,小蛇吃到了食物,此时小蛇的身体会长一节,这里的实现比较简单,我们只要将小蛇的身体整体往后移动一节即可。
/*-------------- 判断是否吃到食物 --------------*/
bool isEating(){
return snake.x[0] == food.x && snake.y[0] == food.y;
}
/*--------------- 小蛇吃到食物 ----------------*/
bool snakeEatenFood(){
if(isEating()){
// 蛇的身体 + 1
snake.len ++;
if(snake.len < SNAKE_SIZE) { // 若蛇还未达到最大长度
for(int i = snake.len - 1; i > 0; i--){
snake.x[i] = snake.x[i-1];
snake.y[i] = snake.y[i-1];
}
// 将食物作为蛇头
snake.x[0] = food.x;
snake.y[0] = food.y;
randomFoodPosition();
drawFood();
return true;
}
}
return false;
}
3.7 判断游戏结束
当蛇头碰到边界或自己的蛇身游戏则结束,因为蛇身是跟着蛇头走的,所以我们只需要判断蛇头就行了。
bool gameover(){
for(int i = 1; i < snake.len; i++) // 蛇头碰到蛇身
if(snake.x[0] == snake.x[i] && snake.y[0] == snake.y[i])
return true;
if(snake.x[0] < 2 || snake.x[0] > MAP_WIDTH - 2) // 蛇头超出了左/右边界
return true;
if(snake.y[0] < 0 || snake.y[0] > MAP_HEIGHT - 1) // 蛇头超出了上/下边界
return true;
return false;
}
4. 整合所有部分
将上面所有的部分整合起来,我们在 main 主函数里写游戏的逻辑:
代码关键部分:
/*---------- 初始化游戏信息 ------------*/
void init(){
hideCursor(); // 隐藏光标
snake.len = 1; // 默认只有一个蛇头
snake.x[0] = MAP_WIDTH / 2;
snake.y[0] = MAP_HEIGHT / 2;
snake.speed = 1;
snake.direction = D_RIGHT; // 初始化贪吃蛇方向 向右
randomFoodPosition(); // 产生随机食物
drawMap(); // 绘制边界
drawFood(); // 绘制食物
drawSnake(); // 绘制蛇
}
int main(){
init();
while (!gameover()){
gotoxy(MAP_WIDTH +5, 0); // 显示得分
printf("得分: %d", snake.len - 1);
// 判断按键
int keydown = keyDown();
if(keydown != -1)
{
snake.direction = char(keydown);
}
snakeEatenFood();
move();
Sleep(100 / snake.speed); // 延迟
}
}
全部代码请参考 Gitee 网址:C语言实现贪吃蛇
最终运行效果:
5. 总结
相信通过本次的实践,能提升你对C语言编程的熟练度,关于这个小游戏其实还有许多可拓展的地方,比如设计一个游戏初始界面,用户可以选择"开始游戏"、“游戏说明”、“游戏设置” 等等,在游戏界面上我使用了最普通的光标移动 加 printf 打印的方式,如果你想要更好看的游戏界面,比如添加一些图片,可以了解一下 EasyX 这个图形库。
EasyX Graphics Library 是针对 Visual C++ 的免费绘图库,支持 VC6.0 ~ VC2022,简单易用,学习成本极低,应用领域广泛。目前已有许多大学将 EasyX 应用在教学当中。
不过 EasyX 是基于 C++ 语言的拓展图形库,如果你的学校是要求使用 C语言进行课程设计的话,需要考虑编程语言不同的问题哟~