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(30,30));
void PrintInitmap();
private:
std::vector<Point> initmap;
};
这里emplace_back 的初始化方式是新接触的,很好用
容器定义为私有成员,储存点类
重要的一点: 如何初始化点并压入容器中。我们需要借助函数来完成,而不是在public域中直接一个个的完成定义。 必须要借助函数来完成!!所以构造函数来初始化地图更为合适,我们就选择了构造函数来完成。
int main() {
Map map1;
map1.PrintInitmap();
return 0;
}
这样我们可以完成地图的打印,在之后还可以定义多套地图进行选择
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.之后是一些食物参数的设定(在进阶阶段会用到不同食物的设置,初级暂时只需要设置一种食物)
大致的运行流程就是这样。接下来我们来分析以上我们分析的几个需要用来解决的问题。
那么蛇的运动清楚之后,通过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;}
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类型。
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()。
每次改变方向只能改变一次,同时需要检查是否吃到食物。所以我们可以先改变方向,然后在每次改变方向之后检查是否吃到食物,然后继续前进。- 如果退出循环,释放分配的内存空间。