贪食蛇的设计&实践——初级

Github上给出的例子过于详细(啰嗦),很多功能暂时只会干扰我的学习。故从最基本的开始一步步的进行分析。

最简单的贪吃蛇
代码已经上传GitHub:https://github.com/Xieyuschen/Greedy-Snake-simple

需要完成的功能

1.控制贪吃蛇的移动
2.食物的生成
3.贪吃蛇吃到食物变长
4.最外圈墙壁的生成
5.贪食蛇碰到自己或墙壁撞死,程序结束

以上是最简单贪吃蛇的一些需求。下面比照着较为先进的代码,通过模仿来实现一步一步的功能.

点类与输出类的完成。

首先在最开始直接建立了空程序,然后自己添加头文件和源文件进行书写。但在引用外部依赖项的时候,发现自己写的程序中的外部依赖项较少,而导入的外部依赖项较多。这个问题是为什么呢?
——一个外部依赖项并不是独立的,很多时候一个外部依赖项还包含了很多其他的外部依赖项。所以当我们引入一个的时候,它会自动添加和此外部依赖项相关的其他文件。
我们现在需要wincontypes.h这个头文件,但是外部依赖项中不存在,所以我们需要引入包含此文件的头文件。即windows.h.在引入此文件之后,打开外部依赖项下拉菜单,可以发现又有很多文件被添加进来。

地图的初始化与地图的输出

  • 地图的初始化

【难点】:如何在输出边框的时候利用容器设置边框元素的坐标。
其实这个蛮简单的,首先我是这么写的:

//错误代码,dort(i,1)是不合法的
for (int i = 1; i < 10; ++i) {
  vector<Point> dort(i,1);
  dort[i].Print();
 }

这个题的难点就在于如何使用容器存储点对象,而点对象有两个数据成员,x与y。
我的代码问题在于,没有理解清楚容器存储的内容,现在容器存储的是点对象,而不是数据成员。对象的数据成员在存入容器之前就已经被定义好了
所以应该创建一个容器,再用emplace_back()来加入新的点单位。

那我们应该这么写:

class Map
{
public:
    Map()//默认构造函数,将正方形各点压入initmap
    {
        initmap.emplace_back(Point(1, 1));
        //省略若干行
        initmap.emplace_back(Point(3030));
 void PrintInitmap();
private:
 std::vector<Point> initmap;
};

这里emplace_back 的初始化方式是新接触的,很好用
容器定义为私有成员,储存点类
重要的一点: 如何初始化点并压入容器中。我们需要借助函数来完成,而不是在public域中直接一个个的完成定义必须要借助函数来完成!!所以构造函数来初始化地图更为合适,我们就选择了构造函数来完成。

  • 地图的输出:
    地图的输出主要依靠main()函数调用使用map类中的打印函数来完成。简单试用的代码如下:
int main() {
 Map map1;
 map1.PrintInitmap();
 return 0;
}

这样我们可以完成地图的打印,在之后还可以定义多套地图进行选择

蛇的定义与打印

  • 静态的蛇
    首先我们需要定义一条蛇,然后把他打印出来,之后再考虑如何让蛇动起来。
    仿照map的编写思路,我们很快就定义好了一条初始的小蛇。输出省略。代码如下:
class Snake {//class后public前默认为private!!
public:
 Snake(){
  Initsnake.emplace_back(Point(3, 3));
  Initsnake.emplace_back(Point(4, 3));
  Initsnake.emplace_back(Point(5, 3));
 }
 void PrintSnake();
 void PrintSnakeHead();
private:
 std::vector<Point> Initsnake;
};

好了,蛇打印了出来,我们怎么让它动起来呢?首先要分析下我们的需求。

首先要分析以下蛇动的时候我们需要控制的变量

  • 蛇头的位置,怎么确定哪边是蛇头
  • 怎么让蛇通过键盘控制来拐弯
  • 怎么完成蛇的移动

那么问题来了,程序是如何调用运行的呢??

贪食蛇的游戏进程

  • main() 函数的调用
    main()函数调用controller.h,使用类controller
  • controller对象调用成员函数Game();
  • Game的流程:
    1.输出开始界面
    2.绘制游戏界面
    3.开启游戏循环界面(此步真正开始进入贪吃蛇的操控)。此步关键语句int tmp=PlayGame(),调用成员函数PlayGame()
  • PlayGame() 函数的调用:
    1.首先初始化蛇和食物,然后随机数设置食物的位置
    2.函数的循环体内
    大条件蛇存活时(没有撞墙&&撞到自身,由之前定义的函数实现判断)
    吃到食物变长的if函数体、如果吃到食物调用move()函数增长蛇,如果没有吃到就使用NormalMove()函数完成正常移动。
    3.之后是一些食物参数的设定(在进阶阶段会用到不同食物的设置,初级暂时只需要设置一种食物)

大致的运行流程就是这样。接下来我们来分析以上我们分析的几个需要用来解决的问题。

  • 蛇头位置的确定
    我们使用了双向容器deque,这种容器首、尾均可以操作,所以deque的front就是蛇头。
  • 怎么让蛇通过键盘控制来拐弯
    我们定义了ChangeDirection() 函数来完成对方向的控制。此函数根据输入的键盘按键进行修改,可以修改头部点的坐标,完成改变蛇的运动方向。
  • 如何完成蛇的移动
    通过成员函数Move() 与 NormalMove() 来完成蛇的移动,每次更新蛇身每个点的坐标,之后进行打印。

那么蛇的运动清楚之后,通过main()完成蛇的运动。

int main() {
 unsigned long speed = 300;
 Map map1;
 Snake* snake1 = new Snake();
 for(int i=1;i<100;++i) {
 snake1->ChangeDirection();
 snake1->NormalMove();
 Sleep(speed);
}
return 0}

食物的生成与贪食蛇吃食物

  • 食物的生成
    定义食物类之后,关注它的坐标,使用SetCursorPosition() 定位,然后输出食物代表的字符。
void Food::DrawFood(Snake& csnake) {
 while (true)//有东西
 {
  int tmp_x = rand() % 30;
  int tmp_y = rand() % 30;
  x = tmp_x;
  y = tmp_y;
  SetCursorPosition(x, y);
  std::cout << "★";
 }
}

但是这个输出之后的代码框有★在快速闪动,可能需要我们设置生成的速度。

贪食蛇吃食物

设置函数GetFood() ,完成贪食蛇吃食物的过程。->我认为的函数应该满足的功能。
SetFood() 要实现以下几个基本需求。

  • 吃食物后食物被消除
  • 吃食物之后可以变长

实际上:这个函数只需要判断蛇是否吃到食物,其他功能例如食物被消除,贪食蛇吃完可以变长都可以通过之前已经定义的函数来完成。

bool Snake::GetFood(const Food& nfood){
 if (Initsnake.back().GetX() == nfood.x && Initsnake.back().GetY() == nfood.y)
      return true;
 else
      return false;}
  

判断是否撞到墙上&&自身

首先我们判断是否撞上,要么是要么否。故我们选择bool类型。

  • 撞墙
    我们将蛇身压入deque中去,边界我们之前设置时已经明确了x,y坐标,那么可以直接通过比较坐标来判断是否撞墙。
  • 撞上自身
    先取出蛇头的地址,然后再遍历与之后的进行比较,如果不一样就+1,一样不加。最后和蛇长比较,如果正好等于自身就说明没有重复。
for (auto point : Initsnake) {
  if (!(point == *head)) {
   ++cnt;
  }
  else
    break;}

但是需要注意的是,point 和*head 都是类类型,而关系运算符“==”只针对内置类型。所以我们需要使其重载。
重载代码如下:

bool operator==(Point& p1) {
  if (x == p1.x && y == p1.y)
  	 return true;
  else
  	return false;
 }
  • 关于运算符的重载:
    例如class Point{…}
    之后要对两个Point对象是否相等进行运算,那么我们仿照==的意义进行函数重载。
    首先要写明函数返回类型,然后operator关键字说明现在写的是一个运算符的重载,然后把运算符写到operator后面
    需要注意的是此类重载括号只允许一个参数,所以我们就引用一个,然后一个直接用数据成员,另一个用.运算符来判断位置。

贪食蛇整个程序的组装

组装程序时

int speed = 200;
 Snake* csnake = new Snake();
 Food* cfood = new Food();
 Map map1;
 map1.PrintInitmap();
 csnake->PrintSnake();
 srand((unsigned)time(NULL));
 (*cfood).DrawFood(csnake);
 while (csnake->OverEdge() && csnake->HitItself()) {
  if (csnake->ChangeDirection()) {
   if(csnake->GetFood(*cfood))
   {
    csnake->Move();//蛇增长
    cfood->DrawFood(csnake);
    }
   else
   {
    csnake->NormalMove();
    Sleep(speed); }}}
 delete csnake;//释放分配的内存空间
 delete cfood;
 return 0;}
  • 初始化蛇和食物
  • 定义地图,利用成员函数绘制地图边界
  • 设置时间窗口
  • 设置循环条件,不撞墙不撞到自己身上时一直循环运行
  • 下面就是if的使用了。在程序中首先我们想要控制蛇的前进方向,所以我们使用函数ChangeDirection()。
    每次改变方向只能改变一次,同时需要检查是否吃到食物。所以我们可以先改变方向,然后在每次改变方向之后检查是否吃到食物,然后继续前进。
  • 如果退出循环,释放分配的内存空间。

你可能感兴趣的:(C/C++,pratice)