提示:阅读本文需要一定的C++基础,此思路只用于实现贪吃蛇的基本功能,没有图形化,没有使用EasyX库。(尊重原创,转载本文请指明出处)
此思路对于锦城的学生很适用,这就是一学期的一个小项目。
(如果对你有帮助的话,希望能点个赞或者一键三连)
前言
提示:全部代码请点击–>https://github.com/qing-qing-mei/Snake
了解代码框架戳这儿:https://blog.csdn.net/weixin_43929310/article/details/112667309
前一篇食物类和地图类:https://blog.csdn.net/weixin_43929310/article/details/112787936
下一篇 控制类:https://blog.csdn.net/weixin_43929310/article/details/112878920
如何代码有任何不清楚或者疑问的地方欢迎在评论区留言或者私信我,我看到一定会回复。(虽然不一定及时)
一、代码
.h文件#pragma once
#include"CFood.h"
#include<iostream>
#include<vector>
#include<iostream>
#include<Windows.h>
using namespace std;
typedef enum {
w,s,a,d
}Directions; //定义枚举类型,上下左右四个方向
class CSnake
{
public:
CSnake(int x = 40, int y = 15, int length = 4, Directions direction = d, int speed = 400, char pic = '*'); //默认构造
virtual~CSnake();
bool move(); //蛇移动
void eraseSnake(); //擦除蛇
void eraseSnake(int flag); //擦除蛇,用于蛇死亡之后的擦除
bool changeDirections(char vkValue); //用于键盘输入从而改变方向
void changeDirections(Directions dir); //无输入保持方向不变
bool quitGame(); //退出游戏
bool eatFood(CFood* pfood); //是否吃到食物
bool checkFoodPos(CFood *pFood); //检查食物位置
void movePos(); //存储蛇的移动坐标
void growup(); //蛇变长
void showSnake(); //显示蛇
public:
int m_iLength; //总长度(包含蛇头)
int m_iHeadX; //蛇头坐标
int m_iHeadY;
int m_iSpeed; //蛇当前速度
char m_pic;
Directions m_enumCurrentDirection; //蛇当前方向
vector<Cunit> m_vecBody; //存储蛇的一个一位数组
};
.cpp
#include<iostream>
#include<vector>
#include<conio.h>
#include<Windows.h>
#include"CSnake.h"
#include"Cunit.h"
#include"CMap.h"
#include"CFood.h"
#include"CMenu.h"
using namespace std;
CSnake::CSnake(int x, int y, int length, Directions direct, int speed, char pic)
{
m_iLength = length;
m_iHeadX = x;
m_iHeadY = y;
m_iSpeed = speed;
m_pic = pic;
m_enumCurrentDirection = direct;
Cunit c(0, 0, '@');
m_vecBody.push_back(c);
m_vecBody[0].m_ix = m_iHeadX;
m_vecBody[0].m_iy = m_iHeadY;
for (int i = 1; i < m_iLength; i++)
{
Cunit c;
m_vecBody.push_back(c);
switch (m_enumCurrentDirection)
{
case w:
m_vecBody[i].m_ix = m_iHeadX;
m_vecBody[i].m_iy = m_iHeadY + i;
break;
case s:
m_vecBody[i].m_ix = m_iHeadX;
m_vecBody[i].m_iy = m_iHeadY - i;
break;
case a:
m_vecBody[i].m_ix = m_iHeadX + i;
m_vecBody[i].m_iy = m_iHeadY;
break;
case d:
m_vecBody[i].m_ix = m_iHeadX - i;
m_vecBody[i].m_iy = m_iHeadY;
break;
}
}
}
CSnake::~CSnake()
{
}
//显示蛇
void CSnake::showSnake()
{
for (int i = 0; i < m_iLength; i++)
{
m_vecBody[i].show();
}
}
//擦除
void CSnake::eraseSnake()
{
/*for (int i = 0; i < m_iLength; i++)
{
m_vecBody[i].erase();
}*/
int i;
for (i = 0; i < m_iLength - 1; i++);
m_vecBody[i].erase();
}
void CSnake::eraseSnake(int flag)
{
for (int i = 0; i < m_iLength; i++)
m_vecBody[i].erase();
}
//改变方向
bool CSnake::changeDirections(char vkValue)
{
switch (vkValue)
{
case'w':
m_vecBody[0].m_iy--;
m_enumCurrentDirection = w;
return false;
case's':
m_vecBody[0].m_iy++;
m_enumCurrentDirection = s;
return false;
case'a':
m_vecBody[0].m_ix--;
m_enumCurrentDirection = a;
return false;
case'd':
m_vecBody[0].m_ix++;
m_enumCurrentDirection = d;
return false;
case 'q': //代表quit
changeDirections(m_enumCurrentDirection);
showSnake();
if (quitGame())
{
return true;
}
return false;
case '+':
changeDirections(m_enumCurrentDirection);
showSnake();
m_iSpeed -= 50;
return false;
case ' ':
changeDirections(m_enumCurrentDirection);
showSnake();
Cunit::gotoxy(CMap::KLEFT, CMap::KHEIGHT + 1);
cout << "游戏暂停,请按任意键继续...";
_getch(); //游戏暂停
Cunit::gotoxy(CMap::KLEFT, CMap::KHEIGHT + 1);
for (int i = 0; i < 27; i++)
cout << ' ';
return false;
default:
changeDirections(m_enumCurrentDirection);
showSnake();
return false;
}
}
void CSnake::changeDirections(Directions dir)
{
switch (dir)
{
case w:
m_vecBody[0].m_iy--;
break;
case s:
m_vecBody[0].m_iy++;
break;
case a:
m_vecBody[0].m_ix--;
break;
case d:
m_vecBody[0].m_ix++;
break;
default:
break;
}
}
bool CSnake::quitGame() //退出游戏
{
Cunit::gotoxy(CMap::KLEFT, CMap::KHEIGHT + 1);
cout << "退出游戏?(y/n):";
char ch;
cin >> ch;
Cunit::gotoxy(CMap::KLEFT, CMap::KHEIGHT + 1);
for (int i = 0; i < 17; i++)
cout << ' ';
if (ch == 'y')
{
return true;
}
else return false;
}
//存储移动坐标
void CSnake::movePos()
{
for (int i = m_iLength - 1; i > 0; i--)
{
m_vecBody[i].m_iy = m_vecBody[i - 1].m_iy;
m_vecBody[i].m_ix = m_vecBody[i - 1].m_ix;
}
}
//移动
bool CSnake::move()
{
bool flag = false;
movePos();
if (_kbhit())
{
char key;
key = _getch();
if (key == 'a' && m_enumCurrentDirection == d)
{
key = 'd';
flag=changeDirections(key);
}
else if (key == 'd' && m_enumCurrentDirection == a)
{
key = 'a';
flag=changeDirections(key);
}
else if (key == 'w' && m_enumCurrentDirection == s)
{
key = 's';
flag=changeDirections(key);
}
else if (key == 's' && m_enumCurrentDirection == w)
{
key = 'w';
flag=changeDirections(key);
}
else flag=changeDirections(key);
}
else changeDirections(m_enumCurrentDirection);
showSnake();
Sleep(m_iSpeed);
eraseSnake();
return flag;
}
//判断是否吃到食物
bool CSnake::eatFood(CFood* pfood)
{
if (pfood->m_ix == m_vecBody[0].m_ix && pfood->m_iy == m_vecBody[0].m_iy)
return true;
else
return false;
}
//检查食物位置是否在蛇身上
bool CSnake::checkFoodPos(CFood *pFood)
{
for (int i = 0; i < m_iLength; i++)
{
if (pFood->m_ix == m_vecBody[i].m_ix &&
pFood->m_iy == m_vecBody[i].m_iy)
return true;
}
return false;
}
//蛇吃到食物变长
void CSnake::growup()
{
Cunit c;
m_vecBody.push_back(c);
m_iLength++;
}
二、代码解析
先介绍一下贪吃蛇移动的原理:控制台的贪吃蛇实际上是由一系列的点所构成,贪吃蛇之所以能够移动,实际上就是不断擦除原来的点然后又不断显示新的点过程,我们并看不出来这些是因为计算机运行速度太快一下就能全部显示所以我们看起来是一条蛇在移动。
介绍一下预备知识:vector,getch, kbhit, sleep ,除了vector(因为它可以用指针代替),其他三个我们能够写出来贪吃蛇必不可少的要了解的知识。
事先声明:我本人之前没有学过这些知识,对于这些知识的了解也就仅限于本项目中和网上的一些必要资料,如有说得不对的地方欢迎指出但不要喷我。
vector:它是STL中的一个容器,可以理解为能够存放任意类型的动态数组,也就是说,可以向对待一个数组一样对待它,例如用下标访问数组元素。需要注意的是,需要添加vector头文件。
这个项目中涉及到的一些vector操作很简单,就一个push_back,它的作用是往数组末尾添加一个元素。更多了解请点击:https://www.runoob.com/w3cnote/cpp-vector-container-analysis.html
getch():用到它需要添加 conio.h 头文件,作用是读取一个字符的输入,并且不会在屏幕上显示,如没有输入将会等待输入,类似于getchar()。
kbhit(): 用到它也需要添加 conio.h 头文件,作用是监测键盘输入,例如在本例中,当我们敲下方向键贪吃蛇能够及时转向,如果不输入则不会做任何处理。
sleep(): 用到它需要添加 windows.h 头文件,实现功能延时,单位为ms(毫秒),在本例中,我们也使用了它,否则程序会运行过快导致你没有反应时间造成游戏结束。关于sleep更多点击:https://www.cnblogs.com/ruiy/p/9699819.html
接下来我们分析代码:
首先是构造函数,我将蛇头和蛇身分开初始化便于区分,如果不想做区分直接改一下循环条件就行。然后根据初始化方向的不同所以蛇身的方向也不同,这也就是需要switch分支的原因。
showSnake():利用一个循环显示蛇
eraseSnake():这个地方需要尤其注意,不同的擦除和显示方法所造成的效果并不一样,如果是一个点一个点的擦除在一个点一个点的显示,当蛇身过长,难免会出现蛇身闪烁严重的问题,用本例中的这种擦除方法,能将闪烁情况降至最低,当然最好的方法是使用双端容器队列,能够避免蛇身闪烁的情况。
changeDirection():本例中有两个这种函数,区别只在于函数返回值或函数形参列表,bool changeDirections(char vkValue):这是当按下各种不同的键值的函数,并且判断按下的键是不是q键,会有不同的返回值,所以函数类型为bool,当按下了方向键,将蛇头方向(m_enumCurrentDirection)改变为当前输入方向并且蛇头坐标做出相应改变。
void changeDirections(Directions dir) : 这是没有键盘输入的函数,只改变相应的蛇头坐标。
quitGame():这是按下q键后调用的函数,里面的一系列操作只是为了判断你是否想要退出游戏?
movePos():这个函数很重要,是移动必不可少的函数之一,我们之前介绍了贪吃蛇的原理,里面涉及到了一个重要的问题,如何在擦除点的同时还能够保存新的点的位置从而显示?这个问题之前也困扰了我很久,后来我终于想明白了!例如一个点在m_vecBody(存储蛇坐标的数组)的下标为i,我们直接将其保存为上一个i-1的下标的坐标就行了!所以这样来一个循环我们就能记录每个点的前一个点的位置并在下一时刻显示,需要注意的是循环是从蛇尾开始,否则会出错!
经过上面这些函数的铺垫,一个蛇的移动过程就体现出来了,当蛇初始化好后,我们首先调用movePos函数记录下一时刻显示的点,然后判断是否有键盘输入并且调用相应的changeDirection函数(在这时,蛇头坐标就已经发生变化了),然后调用showSnake函数显示这些点,调用eraseSnake函数擦除蛇尾这个点,这一系列的过程其实就是move()函数,只是move函数中做了键盘有效输入的判断,例如蛇的方向向左你按下a是没用的,然后
while(true)
{
move();
}
这个蛇就完全能够动起来了。至此,贪吃蛇最重要的一步终于完成了。
bool eatFood(CFood* pfood) :是否吃到食物直接判断食物坐标是否与蛇头坐标重合。
void growup() :直接往m_vecBody添加一个元素。
bool checkFoodPos(CFood *pFood):增加此函数是为了避免随机生成的食物和蛇身重合导致地图没有新食物出现,直接利用一个循环 循环比较蛇身坐标即可。