简单的思路分析
首先我们需要考虑的是蛇的属性和食物。
食物的生成很简单,通过random产生一个随机坐标。
这里的坐标只的是在生成的命令行窗口 windows下命令台的坐标类似第四象限坐标值。
即初始位置的坐标为(0,0) 右下角的位置是(x,y)。大概就是下图这样。我们可以通过确定我们想要的位置,然后在对应位置打印我们需要的字符。 具体x,y最大值是多少大家可以自己也试一试。
这里改变光标所在位置用到了windows里的一个叫做“句柄”的概念。句柄是windows内存管理内的一个很重要的概念,在虚拟地址和实际的物理地址中的管理对应有很重要的作用。当然,在这里我们只是简单的使用到了改变命令行光标位置而已。所以我们暂且可以不用深究,就当做简单的使用windows提供好的接口用来改变光标位置就好。所以我们的代码就需要包含“windows.h”的头文件。
在我们控制蛇的移动时,可以通过使蛇身体最后一节消失,在最前面增加一节。就可以看上去让蛇向前移动。当判断到“吃到食物”时,使最后一节不消失。就可以让蛇边长。
void MoveCursor(int x,int y)
{
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);
}
蛇属性的设置
一开始我们需要考虑蛇的状态。我们需要知道蛇的长度,蛇的移动速度,蛇每一节身体的坐标。 所以,我们可以为“蛇”定义一个结构体。而保存蛇的身体坐标。有两种方法。
- 用链表 ,将每一节的坐标值分别储存在链表中。所以链表的每一个节点需要有三个内容
struct snakelistnode
{
int _snakex;
int _snakey;
struct snakelistnode* next;
}
这样做的好处呢,就是只要可以。蛇的长度是可以无限增加的。
- 用数组, 定义两个数组,分别保存每一节身体对应的横坐标和纵坐标。因为C语言没有现成的库,这样就比较简单省事。但是相比使用链表,蛇的长度会受到数组大小的约束。如果将数组定义的很大的话又会造成空间浪费。但是写起来简单。所以我采用了这样使用两个数组的方式来保存坐标。
蛇需要的属性就是这些:
struct
{
int snake_len;
int snake_speed;
int x[SNAKESIZE];
int y[SNAKESIZE];
}snake;
void InitSnake()
{
snake.snake_len = 3;
snake.snake_speed = SPEED;
snake.x[0] = MAXWEIGHT / 2 + 1;
snake.y[0] = MAXHIGH / 2;
for (int i = 1; i < snake.snake_len; i++)
{
snake.x[i] = snake.x[i - 1] - 2;
snake.y[i] = snake.y[i - 1];
}
for (int i = 0; i < snake.snake_len; i++)
{
MoveCursor(snake.x[i], snake.y[i]);
printf(SNAKE);
}
}
生成食物
生成食物就比较简单。我们只需要通过产生两个随机数来对应食物的坐标。然后在屏幕打印出来就好了。
值得注意的是,我们需要判断不能让食物和蛇的身体或者边界障碍重合。如果生成的随机地址发生了冲突,就需要重新生成一个新的位置。
struct //食物的坐标
{
int food_x;
int food_y;
}food;
void CreatFood()
{
int coincide = 0;
do
{
srand((unsigned int)time(NULL));
food.food_x = rand() % (MAXWEIGHT - 2) + 2;
food.food_y = rand() % (MAXHIGH - 1) + 1;
for (int i = 0; i < snake.snake_len; i++)
{
if (food.food_x == snake.x[i] && food.food_y == snake.y[i])
{
coincide = 1;
break;
}
}
} while (food.food_x % 2 != 0||coincide);
MoveCursor(food.food_x, food.food_y);
printf(FOOD);
}
让蛇动起来
前面我们已经说了能让蛇动起来的的大体思路。通过让蛇身体的最后一节消失,在蛇头前增加一节。来达到让蛇看起来在向前移动的效果。
同时,每当蛇向前移动一个单位时,我们需要改变蛇每一节社体的横纵坐标。(当蛇移动后,每一节身体的当前坐标等于移动前的前一节身体的坐标。)
同时,为了让蛇在我们不操作时,能自动向前。我们不能使用阻塞型的函数来读取改变移动方向的键值。因为如果我们采用阻塞型函数,当程序运行到需要读取键值时,我们不对其进行操作。程序就不会继续向下进行。例如我们用scanf时,如果不输入相关元素。程序就会一直等待我们输入。
这里我们使用了——kbhit()函数。用来捕获我们的键盘敲击值。这是一个非阻塞型函数。即使当程序运行到这个函数时我们没有对键盘有任何操作。程序依然会向下进行。
void SnakeMove()
{
int pre_move = move;
if (_kbhit())
{
fflush(stdin);
move = _getch();
if (move == 0 || move == 224)
move = _getch();
}
if (!chkeating) //擦除尾巴
{
MoveCursor(snake.x[snake.snake_len-1], snake.y[snake.snake_len-1]);
printf(" ");
}
// 蛇不能反向
if ((pre_move == 72 && move == 80)||(pre_move==80&&move==72)
||(pre_move==75&&move==77)||(pre_move==77&&move==75))
move = pre_move;
for (int i = snake.snake_len-1; i >0; i--)
{
snake.x[i] = snake.x[i - 1];
snake.y[i] = snake.y[i - 1];
}
switch (move) //判断头向哪移动
{
case 72: // 上
snake.y[0] -= 1;
break;
case 80: // 下
snake.y[0] += 1;
break;
case 75: // 左
snake.x[0] -= 2;
break;
case 77: // 右
snake.x[0] += 2;
break;
}
MoveCursor(snake.x[0],snake.y[0]);
printf(SNAKE);
chkeating = 0;
}
同时我们需要每次判断一下我们从键盘获得的键值是否和当前运动方向相反,如果和当前运动方向相反,则让蛇保持当前运动方向。
当我们移动到食物时,则不消除尾巴即可。我们通过设定一个chkeating来判断当前移动后是否有遇到食物,如果遇到食物则将chkeating置为1;否则置为0。
void EatFood()
{
if (snake.x[0] == food.food_x && snake.y[0] == food.food_y)
{
snake.snake_len++;
chkeating = 1; // 当前吃到食物
score += 10;
MoveCursor(20, MAXHIGH + 1);
printf("%d",score);
CreatFood();
}
}
接着还有判断当前蛇移动的位置是否有障碍物或者撞到自身。这里只需要判断蛇头的位置是否有重合就好。
int ChkSnake()
{
if (snake.x[0] == 0 || snake.x[0] == MAXWEIGHT || snake.y[0] == 0 || snake.y[0] == MAXHIGH)
return FALSE;
for (int i = 3; i < snake.snake_len; i++)
if (snake.x[0] == snake.x[i] && snake.y[0] == snake.y[i])
return FALSE;
return TRUE;
}
总结
没有什么总结的。我们还需要画出地图。同样这里可以通过随机数再地图中间随机生成一些不能移动的位置。来增加游戏难度。 然后完整的地图:
void ShowMap()
{
for (int i = 0; i <= MAXWEIGHT; i += 2)
{
MoveCursor(i, 0);
printf("□");
MoveCursor(i,MAXHIGH);
printf("□");
}
for (int i = 1; i <= MAXHIGH; i++)
{
MoveCursor(0, i);
printf("□");
MoveCursor(MAXWEIGHT, i);
printf("□");
}
InitSnake();
CreatFood();
MoveCursor(0,MAXHIGH+1);
printf("当前得分:");
MoveCursor(20, MAXHIGH + 1);
printf("%d", score);
}
这里会有一个点需要注意 ,我们使用的用来表示墙的“□”或者蛇的“■”在横坐标需要站两个单位。需要注意。
剩下的就没有啦。只需要在主函数按顺序调用就算完成游戏了:
void Gaming()
{
ShowMap();
while (TRUE)
{
if (ChkSnake())
{
EatFood();
SnakeMove();
Sleep(200);
}
else
{
system("cls");
MoveCursor(MAXWEIGHT / 2 + 1, MAXHIGH / 2);
printf("游戏失败");
break;
}
}
}
完整的代码点这里:
完整代码资源