04 贪吃蛇类
定义贪吃蛇的移动,打印,吃食物等等。这节课我们暂时不讨论AI功能,先把手动操作的贪吃蛇做了跑起来,下节课再做AI功能的介绍。该类大体如下:
4.1 成员变量
成员变量m_direction记录每次移动的方向。m_is_alive记录贪吃蛇是否还活着。m_coordinate则是贪吃蛇身体坐标的记录。贪吃蛇是一节一节的,整条蛇必然是由许多节组成的。因此用了一个vector来存储蛇身,每节类型是COORDINATE结构体的。
4.2 默认构造函数
默认构造函数Snake()里面主要是做了初始贪吃蛇的生成,以及移动方向的定义等。初始的蛇为3节。在中间位置,向上移动。代码如下:
1 //移动方向向上 2 m_direction = 1; 3 m_is_alive = true; 4 COORDINATE snake_head; 5 //蛇头生成位置 6 snake_head.x = GameSetting::window_width / 2 - 15; 7 snake_head.y = GameSetting::window_height / 2; 8 9 this->m_coordinate.push_back(snake_head); 10 snake_head.y++; 11 this->m_coordinate.push_back(snake_head); 12 snake_head.y++; 13 this->m_coordinate.push_back(snake_head); //初始蛇身长度三节
4.3 监听键盘
监听键盘用了C里面的一个库函数。_kbhit()非阻塞函数,可以不断监听键盘的情况从而不产生阻塞。有键盘按下的时候,就获取按下的键盘是哪个。然后做出相应的变化,其实是方向的调整。需要注意的是,当我们的蛇往上走的时候,按下方向的键,我们是不做处理的。其它方向一样。还有一个调整游戏速度的,speed是休眠时间,speed越小,速度越快。反之速度越慢。
1 //监听键盘 2void listen_key_borad() 3{ 4 char ch; 5 6 if (_kbhit()) //kbhit 非阻塞函数 7 { 8 ch = _getch(); //使用 getch 函数获取键盘输入 9 switch (ch) 10 { 11 case 'w': 12 case 'W': 13 if (this->m_direction == DOWN) 14 break; 15 this->m_direction = UP; 16 break; 17 case 's': 18 case 'S': 19 if (this->m_direction == UP) 20 break; 21 this->m_direction = DOWN; 22 break; 23 case 'a': 24 case 'A': 25 if (this->m_direction == RIGHT) 26 break; 27 this->m_direction = LEFT; 28 break; 29 case 'd': 30 case 'D': 31 if (this->m_direction == LEFT) 32 break; 33 this->m_direction = RIGHT; 34 break; 35 case '+': 36 if (speed >= 25) 37 { 38 speed -= 25; 39 } 40 break; 41 case '-': 42 if (speed < 250) 43 { 44 speed += 25; 45 } 46 break; 47 } 48 } 49}
4.4 移动贪吃蛇
移动贪吃蛇,我们用了一个方向变量,在监听键盘的时候获取移动的方向,然后在根据方向移动贪吃蛇的蛇头。这里的移动我们是这样处理的,首先,贪吃蛇每移动一次,需要改变的只有蛇头和蛇尾两节。我们只需要把新的蛇头插进去,最后再画出来就可以了。至于蛇尾,如果我们不删除蛇尾的话,蛇会不断变长的。因此我们的做法是:吃到食物的时候插入蛇头而不删除蛇尾,没有吃到食物的时候插入蛇头同时删除蛇尾。这样就完美搞定了。
1 //移动贪吃蛇 2void move_snake() 3{ 4 //监听键盘 5 listen_key_borad(); 6 //蛇头 7 COORDINATE head = m_coordinate[0]; 8 //direction方向:1 上 2 下 3 左 4 右 9 switch (this->m_direction) 10 { 11 case UP: 12 head.y--; 13 break; 14 case DOWN: 15 head.y++; 16 break; 17 case LEFT: 18 head.x--; 19 break; 20 case RIGHT: 21 head.x++; 22 break; 23 } 24 //插入移动后新的蛇头。是否删除蛇尾,在后续吃到食物判断那里做 25 m_coordinate.insert(m_coordinate.begin(), head); 26}
4.5 是否吃到食物
判断是否吃到食物,就是看看蛇头的坐标等不等于食物的坐标。如果等于,就重新生成食物,不删除蛇尾,蛇变长一节。不等于,就删除蛇尾,蛇长不变。
1bool is_eat_food(Food & f) 2{ 3 //获取食物坐标 4 COORDINATE food_coordinate = f.GetFoodCoordinate(); 5 //吃到食物,食物重新生成,不删除蛇尾 6 if (m_coordinate[HEAD].x == food_coordinate.x && m_coordinate[HEAD].y == food_coordinate.y) 7 { 8 f.RandomXY(m_coordinate); 9 return true; 10 } 11 else 12 { 13 //没有吃到食物,删除蛇尾 14 m_coordinate.erase(m_coordinate.end() - 1); 15 return false; 16 } 17}
4.6判断蛇是否还存活
判断蛇是否GG,主要是看是否超出边界,是否碰到自己身体其他部分。
1//判断贪吃蛇死了没 2bool snake_is_alive() 3{ 4 if (m_coordinate[HEAD].x <= 0 || 5 m_coordinate[HEAD].x >= GameSetting::window_width - 29 || 6 m_coordinate[HEAD].y <= 0 || 7 m_coordinate[HEAD].y >= GameSetting::window_height - 1) 8 { 9 //超出边界 10 m_is_alive = false; 11 return m_is_alive; 12 } 13 //和自己碰到一起 14 for (unsigned int i = 1; i < m_coordinate.size(); i++) 15 { 16 if (m_coordinate[i].x == m_coordinate[HEAD].x && m_coordinate[i].y == m_coordinate[HEAD].y) 17 { 18 m_is_alive = false; 19 return m_is_alive; 20 } 21 } 22 m_is_alive = true; 23 24 return m_is_alive; 25}
4.7 画出贪吃蛇
画出贪吃蛇比较简单,gotoxy到身体的每一节,然后cout就行。在此之前设置了颜色为浅绿色。
1//画出贪吃蛇 2void draw_snake() 3{ 4 //设置颜色为浅绿色 5 setColor(10, 0); 6 for (unsigned int i = 0; i < this->m_coordinate.size(); i++) 7 { 8 gotoxy(m_coordinate[i].x, m_coordinate[i].y); 9 cout << "*"; 10 } 11 //恢复原来的颜色 12 setColor(7, 0); 13}
4.8 清除屏幕上的贪吃蛇
我们是死循环不断刷新打印贪吃蛇的,因此每移动一次,必然会在屏幕上留下上一次贪吃蛇的痕迹。因此我们每次在画蛇之前,不是添足,而是清理一下上次遗留的蛇身。我们知道,蛇每次移动,变的只有蛇头和蛇尾,因此该函数我们只需要清理蛇尾就行。gotoxy到蛇尾的坐标,cout<<" ";就行。
1gotoxy(m_coordinate[this->m_coordinate.size()-1].x, m_coordinate[this->m_coordinate.size() - 1].y); 2cout << " ";
05 主函数,组装我们的游戏
我们的游戏在主函数里面进行组装。然后开始运行。
首先我们做游戏相关的初始化和模式选择。
1GameSetting setting; 2PrintInfo print_info; 3Snake snake; 4//初始化游戏 5setting.GameInit(); 6//游戏模式选择 7print_info.DrawChoiceInfo(); 8 9char ch = _getch(); 10switch (ch) 11{ 12case '1': 13 snake.set_model(true); 14 speed = 50; 15 break; 16case '2': 17 snake.set_model(false); 18 break; 19default: 20 gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3); 21 cout << "输入错误,Bye!" << endl; 22 cin.get(); 23 cin.get(); 24 return 0; 25} 26gotoxy(GameSetting::window_width / 2 - 10, GameSetting::window_height / 2 + 3); 27system("pause");
然后就是画地图边框,打印游戏相关信息和说明。生成食物了。
1//画地图 2print_info.DrawMap(); 3print_info.DrawGameInfo(snake.GetModel()); 4//生成食物 5Food food(snake.m_coordinate);
最后就是游戏死循环,在死循环里面,我们需要不断移动蛇,画蛇,判断蛇的状态,判断食物的状态,是否吃到食物等等。具体代码:
1//游戏死循环 2while (true) 3{ 4 //打印成绩 5 print_info.DrawScore(snake.GetSnakeSize()); 6 //画出食物 7 food.DrawFood(); 8 //清理蛇尾,每次画蛇前必做 9 snake.ClearSnake(); 10 //判断是否吃到食物 11 snake.is_eat_food(food); 12 //根据用户模式选择不同的调度方式 13 if (snake.GetModel() == true) 14 { 15 snake.move_snake(); 16 } 17 else 18 { 19 snake.AI_find_path(food); 20 snake.AI_move_snake(); 21 } 22 //画蛇 23 snake.draw_snake(); 24 //判断蛇是否还活着 25 if (!snake.snake_is_alive()) 26 { 27 print_info.GameOver(snake.GetSnakeSize()); 28 break; 29 } 30 //控制游戏速度 31 Sleep(speed); 32}
最终,我们的代码就可以Run起来了。具体效果放在开头了。界面算不上好看,但是整个程序向大家展示了最基本最核心的功能和代码,大家可以在这个基础上开发自己喜欢的各种美丽的界面哦。
06 AI部分和完善
代码是画了几天间间断断写出来的,水平不算很高,代码也写得乱七八糟的。不过代码会在后期不断优化,尽量做到精简优美。至于AI功能,等下一篇博文写吧。