C++编写贪吃蛇(使用类与对象)
完成了一个学期的C++学习后我决定用C++编写一个贪吃蛇。本以为是一个十分基础的问题,网上应该有很多代码教程,但找了好久都没有合适的。要么没有使用类和对象来编写,要么连编译都通不过。无奈只能从头开始自己摸索着写。本人水平有限,代码算法可能存在着一些问题,欢迎读到这篇文章有其他想法的人在评论区理性讨论,大家互相学习。
思路
贪吃蛇游戏主要有这样三个部分:地图、蛇和食物。地图是蛇运动和食物生成的地方且在地图周围存在着墙作为边界。游戏运行的过程大致如下:
1.游戏开始,生成地图并生成墙; 2.生成蛇,包括初始化蛇位置、长度和初始运动方向; 3.生成食物; 4.蛇朝着当前方向移动,直到接收到玩家改变运动方向的指令或蛇撞到墙或自身使游戏结束; 5.当蛇头下一步就要吃到食物时,蛇的长度加一,同时生成新的食物; 6.当蛇的长度达到最大值或死亡时游戏结束。
代码实现
点类
在贪吃蛇中,不论是蛇本身还是食物都是由点组成的,在代码编写过程中,需要进行大量的对点的操作。 在编写游戏本体之前,我首先声明定义了点(Point)类,并编写了一些函数以便后面使用。
class Point
{
private:
COORD pos;
public:
Point () { pos.X = 0; pos.Y = 0; }
Point (UINT x , UINT y)
{
pos.X = x;
pos.Y = y;
}
Point (const Point &p)
:pos (p.pos)
{
}
void setPos (UINT x , UINT y)
{
pos.X = x;
pos.Y = y;
}
void setPosPoint (const Point &p) { pos = p.pos; }
void setPosRandom (int isHead = 0)
{
if (isHead == 1)
{
srand (time (NULL));
pos.X = rand () % 18 + 1;
pos.Y = rand () % 10 + 7;
}
else
{
srand (time (NULL));
pos.X = rand () % 18 + 1;
pos.Y = rand () % 18 + 1;
}
}
void movePoint (int d)
{
if (d == 1)
{
pos.Y--;
}
else if (d == 2)
{
pos.Y++;
}
else if (d == 3)
{
pos.X--;
}
else if (d == 4)
{
pos.X++;
}
}
COORD getPos (void)
{
return pos;
}
void printPoint (char c)
{
HANDLE hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
SetConsoleCursorPosition (hOutput , pos);
cout << c;
}
};
这个类实际上十分简单,它只包含自身位置这一个成员变量。成员函数的功能也大多为对点进行初始化或再赋值。这里面涉及到一些控制台的操作,不明白的可以去百度一下。这里只是简单涉及到,还是很好理解的。 有了这个工具后就可以开始游戏本体的编写了。
蛇
class Snake
{
private:
Point p[400];
int dirction;
int lenth;
friend class Game;
public:
Snake ()
{
lenth = 3;
p[0].setPosRandom (1);
p[1].setPos (p[0].getPos ().X , p[0].getPos ().Y + 1);
p[2].setPos (p[1].getPos ().X , p[1].getPos ().Y + 1);
dirction = 1;
}
};
蛇这个类本身十分简单,成员函数只有一个无参构造函数。设计这个类的目的主要是储存与蛇本身有关的一些变量,并对蛇进行初始化。为了方便后面的操作,我将另一个Game类声明为Snake类的友元,这样Game中的函数也可以对Snake类中的变量直接进行操作了。
游戏功能
class Game
{
private:
Snake s;
int map[20][20] = {0};
Point food;
int score;
public:
Game ()
{
for (int i = 0; i < 20; i++)
{
Point p1 (UINT (i) , UINT (0));
map[i][0] = -1;
p1.printPoint ('=');
Point p2 (UINT (i) , UINT (19));
map[i][19] = -1;
p2.printPoint ('=');
Point p3 (UINT (0) , UINT (i));
map[0][i] = -1;
p3.printPoint ('=');
Point p4 (UINT (19) , UINT (i));
map[19][i] = -1;
p4.printPoint ('=');
Sleep (50);
}
for (int i = 0; i < s.lenth; i++)
{
map[s.p[i].getPos ().X][s.p[i].getPos ().Y] = 1;
}
for (int i = 0; i < 3; i++)
{
s.p[i].printPoint ('O');
}
score = 0;
}
void creatFood (void)
{
Point food;
while (map[food.getPos ().X][food.getPos ().Y] != 0)
{
food.setPosRandom ();
}
map[food.getPos ().X][food.getPos ().Y] = 2;
food.printPoint ('$');
}
void eatFood (void)
{
score = score + 100;
Point temph (s.p[0]);
Point tempt (s.p[s.lenth - 1]);
s.p[0].movePoint (s.dirction);
map[s.p[0].getPos ().X][s.p[0].getPos ().Y] = 1;
s.p[0].printPoint ('O');
for (int i = s.lenth - 1; i > 1; i--)
{
s.p[i].setPosPoint (s.p[i - 1]);
}
s.p[1].setPosPoint (temph);
s.lenth++;
s.p[s.lenth - 1].setPosPoint (tempt);
Sleep (500);
}
void snakeGo (void)
{
Point temp (s.p[0]);
s.p[0].movePoint (s.dirction);
map[s.p[0].getPos ().X][s.p[0].getPos ().Y] = 1;
s.p[0].printPoint ('O');
map[s.p[s.lenth - 1].getPos ().X][s.p[s.lenth - 1].getPos ().Y] = 0;
s.p[s.lenth - 1].printPoint (' ');
for (int i = s.lenth - 1; i > 1; i--)
{
s.p[i].setPosPoint (s.p[i - 1]);
}
s.p[1].setPosPoint (temp);
Sleep (500);
}
int isEatorEnd (Point q , int dirc)
{
q.movePoint (dirc);
if (map[q.getPos ().X][q.getPos ().Y] == -1 || map[q.getPos ().X][q.getPos ().Y] == 1)
{
return -1;
}
else if (map[q.getPos ().X][q.getPos ().Y] == 2)
{
return 1;
}
else
{
return 0;
}
}
void setDirection (char d)
{
if (d == 'w' && s.dirction != 2)
{
s.dirction = 1;
}
else if (d == 's' && s.dirction != 1)
{
s.dirction = 2;
}
else if (d == 'a' && s.dirction != 4)
{
s.dirction = 3;
}
else if (d == 'd' && s.dirction != 3)
{
s.dirction = 4;
}
else
{
return;
}
}
Point &getHead (void) { return s.p[0]; }
int getDirc (void) { return s.dirction; }
int getPoint (void) { return score; }
int getLenth (void) { return s.lenth; }
};
贪吃蛇游戏的主要功能都是在Game类中定义的。Game类中有一个int型的二维数组,里面储存着整个地图中各个位置目前的状态,方便后面条件判断使用。 一些简单的函数的说明都以注释的方式写到代码里了,这里主要说一下snakeGo和eatFood这两个函数。
snakeGo函数
snakeGo函数可以说是整个游戏的核心所在。这里简单示意一下蛇运动一次需要经历的步骤。 这里首先要说明一下,游戏中一共有三个地方储存蛇的位置:蛇类中Point类里储存的控制台坐标,变量map中相应位置储存的“1”和控制台窗口上实际打印出的蛇。蛇要完成一次移动,就要将这三个都进行改变。 第一步要先将表示蛇头点的控制台坐标暂时储存下来;第二步要将储存蛇头位置的三个地方进行相应的改动;第三步将蛇尾从map和控制台窗口中抹去;最后一步将中间蛇身的所有点的坐标变为其前一个点的坐标。 这里可以注意到在移动过程中控制台窗口只需关注蛇头和蛇尾的变化情况即可。 函数最后的Sleep函数实现了蛇移动后的短暂停顿。
eatFood函数
eatFood函数用来控制当蛇吃到食物时的运动状况。当判断到蛇在下一次运动时会吃到食物时(通过主函数调用isEatorEnd函数实现),主函数会调用eatFood函数进行蛇的运动而不是snakeGo并在执行完成后直接进入下一循环。 eatFood函数与snakeGo基本相同,不同点在于要在snake类中p数组中增加蛇尾位置的信息,lenth变量中增加蛇的长度。
主循环
接下来就是程序的执行部分,欢迎界面后便是主循环,游戏结束前的部分都将在这里完成。主循环内的二层循环以kbhit函数作为判断条件,函数的功能可以自行搜索,在用户改变蛇前进方向之前的部分都在这里进行。在每次循环进行时,会首先判断游戏是否结束或蛇是否将要吃到食物,然后再进行蛇的移动操作。
int main ()
{
cout << " 您好,欢迎来到贪吃蛇 " << endl;
cout << "您可以通过'w''a''s''d'来控制蛇的上下左右移动" << endl;
system ("pause");
system ("cls");
Game g;
g.creatFood ();
while (1)
{
while (!_kbhit ())
{
int i = g.isEatorEnd (g.getHead () , g.getDirc ());
if (i == 1)
{
g.eatFood ();
if (g.getLenth () == 361)
{
goto L;
}
g.creatFood ();
continue;
}
else if (i == -1)
{
goto L;
}
g.snakeGo ();
}
char d = _getch ();
g.setDirection (d);
}
L:system ("cls");
cout << "游戏结束!您的得分为" << g.getPoint () << endl;
return 0;
}
|