字符界面贪吃蛇的简单实现
环境:Windows
语言:C
要求制作一个蛇的可活动范围为10*10大小的平面,食物随机产生,可读取W,A,S,D字符并让蛇做出相应移动,当蛇头吃到食物时
先说一下设计思路:
关于贪吃蛇游戏:
对象有蛇,有食物,有墙面,其中蛇分为蛇头和蛇身;
对于蛇,它可以移动,当头部与食物重合,即吃掉食物的时候身体长长,而对于食物,当其与蛇头接触之后就消失,当蛇与墙面接触游戏结束。
我们先大体构建一下main函数框架
int main() {
//打印初始字符界面
printGrid();
//gameSystem代表的是游戏状况,GAME_CONTINIUE为正常,GAME_OVER为游戏失败,GAME_WIN为游戏胜利
while (gameSystem == GAME_CONTINIUE) {
char ch = readInput();
//snakeMove 是蛇移动的函数,里面的UP,LEFT等指蛇移动的方向
switch (ch) {
case 'W':
snakeMove(UP);
break;
case 'A':
snakeMove(LEFT);
break;
case 'S':
snakeMove(DOWN);
break;
case 'D':
snakeMove(RIGHT);
break;
}
if(snakeLength>=SNAKE_MAX_LENGTH){
gameSystem = GAME_WIN;
break;
}//此处是游戏胜利条件,当蛇的长度达到设定的上限时,游戏胜利
placeFood();//放置食物
printGrid();//再次打印界面(此时打印是为了打印出做出新行为的蛇已经新生成的失物)
}
clearScreen();//清屏,以便打印游戏结束or胜利信息
if(gameSystem == GAME_WIN){
printf("YOU WIN!!!");
}
if(gameSystem == GAME_OVER){
printf("GAME OVER");
}
getchar();//为了让程序停留在展示游戏结束胜利信息的状态,避免所谓的“闪退”现象,这里加个读取键盘输入的函数来等待用户键入以推出程序
return 0;
}
为了实现程序,我们现将对象的基本属性通过#define 或定义全局变量设置好:
//对于蛇的移动,我们采用二维数的方式来表示,方便刻画蛇的移动
#define UP 0,-1
#define DOWN 0,1
#define LEFT -1,0
#define RIGHT 1,0
//界面基本属性
#define GRID_WIDTH 12
#define GRID_HEIGHT 12
#define CHAR_GRID_BRICK '*'
#define CHAR_GRID_BLANK ' '
//蛇的基本属性
#define SNAKE_MAX_LENGTH 6
#define CHAR_SNAKE_BODY 'X'
#define CHAR_SNAKE_HEAD 'H'
//食物的基本属性
#define CHAR_GRID_FOOD '$'
#define FOOD_MAX_NUMBER 5
int foodNumber = 0;
//表示游戏状态的变量
#define GAME_CONTINIUE 0
#define GAME_OVER 1
#define GAME_WIN 2
int gameSystem = GAME_CONTINIUE;//初始化游戏状态
//蛇的移动界面,为了更加方便描述蛇的各个身体部位(蛇头,蛇身)的位置,这里使用多维数组来做出可以用类似数学里边的坐标来表示位置的界面
char grid[GRID_HEIGHT][GRID_WIDTH] = {
"************",
"*XXXXH *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"* *",
"************"
};
剩下的就是一些基本函数的实现了
如读取键盘键入的函数:
/*读取字符,每次只读取一个字符,无字符时返回空*/
char readInput() {
char input;
if (scanf("%c", &input)) {
while (getchar() != '\n') continue;
return input;
} else {
return ' ';
}
}
如清屏函数
/* 清空函数 */
void clearScreen() {
system("CLS");//windows命令符程序下的清屏指令,需要用到头文件<stdlib.h>
}
制图函数:
/* 刷新屏幕 */
void printGrid() {
clearScreen();
int x, y;
for (y = 0; y < GRID_HEIGHT; ++y) {
for (x = 0; x < GRID_WIDTH; ++x) {
putchar(grid[y][x]);
}
putchar('\n');
}
}
移动蛇的函数:
/* 移动蛇*/
void snakeMove(int dx, int dy) {
//判断下一步会不会导致游戏结束
if (!judgeMove(dx, dy)) {
gameSystem = GAME_OVER;
return;
}
//若没吃到食物
if (!eatFood(dx, dy)) {
int i;
/* 清空原来的蛇 */
clearSnake();
/* 身体移动 */
for (i = 0; i < snakeLength - 1; ++i) {
snakeX[i] = snakeX[i + 1];
snakeY[i] = snakeY[i + 1];
}
/* 头部确定 */
snakeX[snakeLength - 1] = snakeX[snakeLength - 1] + dx;
snakeY[snakeLength - 1] = snakeY[snakeLength - 1] + dy;
/* 画蛇 */
drawSnake();
}
//若吃到了食物
else{
int i;
/* 清空原来的蛇 */
clearSnake();
/* 头部确定 */
snakeX[snakeLength] = snakeX[snakeLength - 1] + dx;
snakeY[snakeLength] = snakeY[snakeLength - 1] + dy;
/* 画蛇 */
drawSnake();
}
}
可以发现在吃到食物和未吃到食物两种状态,对蛇的各个部位的处理有些不同,若没吃到蛇,蛇的各个身体的坐标就往蛇移动的方向逐个传递,若吃到了食物,仅仅往移动方向位移蛇头,再在蛇头原来的位置添加蛇身即可,这样就能实现蛇身体的长长。
其中我们还需要细化里面定义的画蛇函数drawSnake(),清除蛇的函数 clearSnake(),判断下一步游戏状态的函数judgeMove(),以及判断有没有吃到食物的函数eatFood();这个过程中可以体会到自顶向下方式的逻辑性与对程序设计思维的简化。
//画蛇函数
void drawSnake() {
int i;
for (i = 0; i < snakeLength; ++i) {
int x = snakeX[i];
int y = snakeY[i];
grid[y][x] = (snakeLength - 1) == i ? CHAR_SNAKE_HEAD : CHAR_SNAKE_BODY;
}
}
//清除蛇函数
void clearSnake() {
int i;
for (i = 0; i < snakeLength; ++i) {
int x = snakeX[i];
int y = snakeY[i];
grid[y][x] = CHAR_GRID_BLANK;
}
}
在判断下一步的状态的时候,其实这一步已经发生了,不过是发生在储存蛇的坐标的信息中,此时并为将此状态打印出来,故要注意自己的思考方向,注意将字符界面与多维字符组的状态分开思考(当初就是因为混淆了两种状态,导致不知道如何处理判断下一步是否吃到食物的函数,其实只要在蛇头还未打印出来之前,先在数组中改变蛇头的位置,再判断蛇头马上要打印出来的位置原来是不是食物即可,对于障碍物的判断同理。)
根据这种思考后我们很容易可以写出eatFood()函数,以及judgeMove()函数
//判断游戏下一步状态的函数,游戏结束返回0,游戏继续返回1。
int judgeMove(int dx, int dy) {
/* 蛇头坐标位置确定 */
int headX = snakeX[snakeLength - 1] + dx;
int headY = snakeY[snakeLength - 1] + dy;
int i;
/* 判断是否越出打表范围 */
if (headX <= 0 || headX >= GRID_WIDTH - 1 || \
headY <= 0 || headY >= GRID_HEIGHT - 1) {
return 0;
}
/* 判断是否撞到障碍物*/
if (grid[headY][headX] == CHAR_GRID_BRICK) {
return 0;
}
/*判断是否吃到自己 */
for (i = 0; i < snakeLength; ++i) {
int bodyX = snakeX[i];
int bodyY = snakeY[i];
if (headX == bodyX && headY == bodyY) {
return 0;
}
}
return 1;
}
//判断是否吃到蛇的函数,吃到返回 1,没吃到返回0;
int eatFood(int dx, int dy) {
/* 蛇头坐标位置确定 */
int headX = snakeX[snakeLength - 1] + dx;
int headY = snakeY[snakeLength - 1] + dy;
/*通过判断蛇头位置是否为食物来吃食物*/
if (grid[headY][headX] == CHAR_GRID_FOOD) {
if (snakeLength + 1 > SNAKE_MAX_LENGTH) return 0;
snakeLength++;
snakeX[snakeLength - 1] = headX;
snakeY[snakeLength - 1] = headY;
/*食物变量改变*/
foodNumber--;
int i;
for (i = 0; i < foodNumber; ++i) {
if (headX == foodX[i] && headY == foodY[i]) {
foodX[i] = foodX[foodNumber];
foodY[i] = foodY[foodNumber];
break;
}
}
return 1;
}
return 0;
}
其中涉及了食物的数量,有关食物数量可以简单的用吃到后-1来表示数量的改变。
但是食物呢?食物要放在哪?这就是我们接下来需要实现的函数。
食物位置的确定函数placeFood(),以及将食物打印出来的函数printFood()。
/*确定食物位置,为了做到随机放置食物,若我们直接用rand()的话,多次运行程序,刷出来的食物的位置都是固定的,所以这里用time()作为随机数的种子,来产生随机数,这样就可以确保每次运行程序所产生的食物的位置都不会一模一样,而由于食物只在x[1,10],y[1,10]的位置出现,所以只要将随机数%12 并去掉值为0的情况就可以随机出一组食物坐标了!故这里要用到头文件<time.h>*/
void placeFood() {
int x, y;
for (;;){
srand((unsigned int)time(0));
if (foodNumber + 1 > FOOD_MAX_NUMBER) return;
do {
x = (int)(rand()%12);
y = (int)(rand()%12);
if(x == 0 || y == 0)continue;
} while (grid[y][x] != CHAR_GRID_BLANK);
foodNumber++;
foodX[foodNumber - 1] = x;
foodY[foodNumber - 1] = y;
printFood();
}
}
/* 画出食物*/
void printFood() {
int i;
for (i = 0; i < foodNumber; ++i) {
int y = foodY[i];
int x = foodX[i];
grid[y][x] = CHAR_GRID_FOOD;
}
}
最后将以上函数合在一起就可以整合出简单的字符贪吃蛇了!
而由于分了这么多代码块,我们也可以通过改变,蛇的最大长度,地图上会同时出现的食物的数量,以及人为地在地图中加入多的障碍物“*”。从而增加游戏乐趣!