最近女朋友挺无聊的(不是左手,也不是右手),决定用C++写一个贪吃蛇的游戏给他玩玩。顺便更新一下博客,把思路记下来。
相信看这篇博客的人都用过诺基亚的手机,玩过贪吃蛇(不接受反驳)。贪吃蛇的玩法就是一条小蛇在转圈圈儿,同时要去吃食物,每吃一个,自身增加1节长度,直到撞到自己或者撞到墙才算失败。游戏规则是非常的简单。相信各位早就看懂了,那接下来就开始敲代码吧:
首先在这个游戏中有蛇,有食物还有墙。因此我们需要设计三个类来依次表示他们。
首先我们先在excel中模拟一下贪吃蛇的游戏过程如下:
我们可以假定用*表示墙,用=表示蛇身,用@表示蛇头,用#表示食物。因此,整个过程我们可以用一个二维数组来维护整个蛇,墙以及食物的状态。先建立一个墙的类,内容如下:
#pragma once
#include
class Wall {
public:
enum
{
ROW = 25, COLUMN = 25
};
//构造函数
Wall();
//根据条件设置点的元素,可能为食物,可能为蛇身可能为蛇头
void setPoint(int x, int y, char ele);
//获取某一坐标的元素,用于给需要移动到的下一点判断是否会装上
char getPoint(int x, int y)const;
//在屏幕上输出当前状态
void drawWall();
//重载绘制方法,此时传入一个参数为得分,并写在第一行
void drawWall(unsigned int score);
private:
char gameArr[ROW][COLUMN]; //维护一个二维数组
};
wall.h
然后对墙这个类里面的基本方法进行实现,内容如下:
#include"Wall.h"
//构造函数,先把墙搭建好,即i=0或i=ROW - 1或j=0或j=COLUMN - 1的地方就为墙用'*'表示,其他地方为待使用区域用' '表示
Wall::Wall() {
for (int i = 0; i < ROW; ++i)
for (int j = 0; j < COLUMN; ++j)
if (i == 0 || j == 0 || i == ROW - 1 || j == COLUMN - 1)
this->gameArr[i][j] = '*';
else
this->gameArr[i][j] = ' ';
}
//根据传入的元素,设置点位的状态,可能是食物'#',也可能是蛇头'@'或蛇身'='
void Wall::setPoint(int x, int y, char ele) {
this->gameArr[x][y] = ele;
}
//返回点位的元素,用来判断是否会撞上,或者是否吃到了食物
char Wall::getPoint(int x, int y)const {
return this->gameArr[x][y];
}
//将二维数组中维护的状态输出在屏幕上,同时在右侧加入开发者,玩法等信息
void Wall::drawWall() {
for (int i = 0; i < ROW; ++i) {
for (int j = 0; j < COLUMN; j++)
std::cout << this->gameArr[i][j] << " ";
if (i == 6)
std::cout << "开发者: 方人也WJ";
if (i == 8)
std::cout << "玩法——a: left | w: up | s:down | d: right";
std::cout << std::endl;
}
}
//重载的输出信息,并且通过传入的分数参数计算当前的得分
void Wall::drawWall(unsigned int score) {
for (int i = 0; i < ROW; ++i) {
for (int j = 0; j < COLUMN; j++)
std::cout << this->gameArr[i][j] << " ";
if (i == 1)
std::cout << "当前得分:" << (score - 3) * 10; //要减去一开始的时候蛇就有3段即减掉30分
if (i == 6)
std::cout << "开发者: 方人也WJ";
if (i == 8)
std::cout << "玩法——a: left | w: up | s:down | d: right";
std::cout << std::endl;
}
}
Wall.cpp
整个墙实现了之后基本框架就搭好了,演示图如下:
接下来就是构造蛇的类了,在初始化一条蛇时需要考虑到给它一定的长度(假设是三段即”==@“),接下来就是把蛇放到维护数据的二维数组中,因此在蛇这个类中需要保存一个wall的引用,用以修改二维数组中的数据。
此外,蛇本身的移动即为对二维数组中内容的修改,假设蛇一开始在([5,4] (蛇身),[5,5] (蛇身),[5,6] (蛇头)),现在蛇向右移动后就会变成([5,5] (蛇身),[5,6] (蛇身),[5,7] (蛇头)),观察这两组数据可以发现,当蛇移动时,只需要把蛇尾剔除,把原来的蛇头变成蛇身,把移动的地方变成蛇头就可以了。因此可采用一个list来维护蛇,在这个list中,每次移动时就把蛇尾pop掉(即pop_front),同时在list末端添加一个蛇头(push_back)。同理,当吃到了一个食物时,就直接在list末尾push一个蛇头即可。因此蛇类设计如下:
#pragma once
#include
#include
#include "Wall.h"
#include "Food.h"
class Snake {
public:
enum {
LEFT = 'a',
RIGHT = 'd',
UP = 'w',
DOWN = 's'
};
//构造函数,初始化时需要将wall对象传入,构造出的蛇需要存入wall中的二维数组
Snake(Wall& wall);
//重新写一个构造函数,需要将food类传入作为内部类
Snake(Wall& wall, Food& food);
//弹出当前的蛇尾
void popSnakeTail();
//插入新蛇头的位置,传入参数为新蛇头的坐标
void insertSnakeHead(int x, int y);
//通过wasd操纵移动蛇,传入的参数为蛇的移动方向,如果可以成功移动,则返回true,否则false
bool moveSnake(char direction);
//获取得分接口,用来提供得分
unsigned int getScore()const;
private:
Wall& wall;
//维护一个蛇列表,其中蛇身或蛇头的坐标用pair形式保存
std::list<std::pair<int, int>> snakeList;
Food& food;
};
Snake.h
相应的接口实现如下:
#include "Snake.h"
#include
//初始化蛇,假设一开始蛇为3段,2段蛇身,1段蛇头
Snake::Snake(Wall& wall, Food& food) : wall(wall), food(food) {
this->snakeList.push_back(std::make_pair<int, int>(5, 4)); //蛇尾
this->snakeList.push_back(std::make_pair<int, int>(5, 5)); //蛇身
this->snakeList.push_back(std::make_pair<int, int>(5, 6)); //蛇头
this->wall.setPoint(5, 4, '=');
this->wall.setPoint(5, 5, '=');
this->wall.setPoint(5, 6, '@');
}
//弹出蛇尾
void Snake::popSnakeTail() {
std::pair<int, int> snakeTail = this->snakeList.front(); //记录下蛇尾的坐标,用于清除墙中二维数组中的当前蛇尾
this->snakeList.pop_front();
this->wall.setPoint(snakeTail.first, snakeTail.second, ' '); //重新写入空格
}
//插入蛇头,再插入蛇头之前,需要将原蛇头位置变为蛇身
void Snake::insertSnakeHead(int x, int y) {
std::pair<int, int> snakeHead = this->snakeList.back();
this->wall.setPoint(snakeHead.first, snakeHead.second, '='); //更新为蛇身
this->snakeList.push_back(std::pair<int, int>(x, y)); //将新的蛇头坐标保存
this->wall.setPoint(x, y, '@');
}
bool Snake::moveSnake(char direction) {
std::pair<int, int> snakeHead = this->snakeList.back(); //先取出蛇头的坐标
int headX = snakeHead.first;
int headY = snakeHead.second;
switch (direction) //根据方向来判断需要如何修改
{
case UP: //如果向上移动了,那么新的蛇头坐标应该为--headX, headY
--headX;
break;
case DOWN:
++headX; //向下移动,则新蛇头坐标为++headX, headY
break;
case LEFT:
--headY;
break;
case RIGHT:
++headY;
break;
default:
break;
}
//接下来就需要能否成功移动一步
char space = this->wall.getPoint(headX, headY);
if (space == ' ') { //如果要移动到的点为空格,则该空间无任何东西,可正常移动,则需要掐尾改头
this->popSnakeTail();
this->insertSnakeHead(headX, headY);
return true;
}
//再加一个判断,如果此时蛇尾正好需要移动,同时蛇头会在这个位置出现,并不会死亡
/*
=
@ = 在这种情况下,蛇头往上,蛇尾向右,这种情况不会死亡,但是程序会判定为死亡,因为先撞上,再修改
=
*/
else if (space == '=') { //如果下一个要走的点还是蛇身
this->popSnakeTail(); //先将末尾弹出,再判断,要走的位置是否位空,如果还不是空,则是撞上了
space = this->wall.getPoint(headX, headY);
if (space == ' ') {
this->insertSnakeHead(headX, headY);
return true;
}
else {
this->insertSnakeHead(headX, headY);
return false;
}
}
else if (space == '#') { //如果是食物,那么蛇身需要加长1,因此不需要再将蛇尾弹出,只需改掉头即可
this->insertSnakeHead(headX, headY);
//食物被吃掉之后需要生成新的食物
this->food.generateFood();
return true;
}
else { //否则撞墙了或撞到蛇身了
this->popSnakeTail();
this->insertSnakeHead(headX, headY);
return false;
}
}
//以蛇的长度代替得分,每增加一段即吃掉一个食物,则增加10分
unsigned int Snake::getScore()const {
return this->snakeList.size(); //每增加一段就加10分并在绘制时输出
}
Snake.cpp
到目前为止,蛇的类基本已经实现,可以先看一下效果图:
现在还差一个食物类需要去实现,食物类非常简单,只需要提供一个依靠随机数随机生成一个食物的方法即可,这里不做过多的赘述,直接上代码:
#pragma once
#include
#include "Wall.h"
class Food {
public:
//构造时需要将墙传入,因为每当生成一个食物时,需要更新到墙中
Food(Wall& wall);
//生成食物
void generateFood();
private:
Wall& wall;
};
Food.h
上面的generateFood即实现了食物的更新,具体实现方法如下:
#include "Food.h"
Food::Food(Wall& wall) : wall(wall) {}
void Food::generateFood() {
int foodX = 0;
int foodY = 0;
while (true) {
foodX = rand() % Wall::COLUMN + 1; //食物的x坐标限制在墙内
foodY = rand() % Wall::ROW + 1; //同理,食物的y坐标也应限制在墙内
if (this->wall.getPoint(foodX, foodY) == ' ') { // 只有食物的坐标处无任何东西时,食物生成才算成功,否则重新生成
this->wall.setPoint(foodX, foodY, '#');
break;
}
}
}
Food.h
到目前为止,整个贪吃蛇游戏基本就可以跑起来了。静态图如下:
整个贪吃蛇的所有要素都构建完成了,现在唯一需要的就是能够通过用户按键使得整个蛇跑起来,同时得分能够更新起来。最好还可以设置难度使蛇越跑越快。为了实现上述功能,可以再增加一个玩家类,用来实现上述过程:
#pragma once
#include
#include
#include "Snake.h"
//引入玩家类
class Player {
public:
//snake用于移动蛇,wall用于更新画面
Player(Snake& snake, Wall& wall);
//玩起来
void play();
private:
Snake& snake;
Wall& wall;
};
Player.h
接下来最主要的就是实现上面的play接口了:
#include "Player.h"
Player::Player(Snake& snake, Wall& wall) : snake(snake), wall(wall) {}
void Player::play() {
//判断蛇是否还存活,即游戏是否要结束游戏
bool isAlive = true;
//放置一个激活变量,判断游戏是否激活,只有第一次按键为wsd中的一个则激活游戏
bool isActive = false;
//用来记录上一次的按键,如果上一次按键和本次按键正好相反(如上一次d,这一次是a,则认为蛇需要旋转180°,然而这是不可能实现的。
//并且刚开始激活游戏时不能按a,否则开局就死
char preKey = Snake::RIGHT;
while (true) {
char ch = _getch(); //通过键盘获取一个按键值
do {
if (ch != Snake::LEFT && ch != Snake::UP && ch != Snake::RIGHT && ch != Snake::DOWN && !isActive) //如果玩家按的不是asdw键则直接忽略
break;
else if (ch != Snake::LEFT && ch != Snake::UP && ch != Snake::RIGHT && ch != Snake::DOWN && isActive) //如果激活了,但是按键不是wasd其中一个,则认为按键无效
ch = preKey; //将之前的按键赋值给当前按键
if (preKey == Snake::RIGHT && ch == Snake::LEFT ||
preKey == Snake::LEFT && ch == Snake::RIGHT ||
preKey == Snake::DOWN && ch == Snake::UP ||
preKey == Snake::UP && ch == Snake::DOWN)
ch = preKey; //如果按了反方向,则认为没有按。
isActive = true; //游戏已激活
isAlive = snake.moveSnake(ch); //如果游戏失败则游戏终止
system("cls"); //清屏用于下一次绘制
wall.drawWall(snake.getScore()); //重新绘制当前状态
if (!isAlive) {
std::cout << "GAME OVER!!!" << std::endl;
break;
}
//设置难度,根据蛇的长度来设置难度
unsigned int difficultLevel = this->snake.getScore();
if (difficultLevel < 8) //如果蛇小于8段,则两帧画面刷新时间为800ms
Sleep(800);
else if (difficultLevel < 15 && difficultLevel >= 8) //如果蛇小于15段但大于8段则刷新时间为500ms
Sleep(500);
else if (difficultLevel < 20 && difficultLevel >= 15) //继续加快速度
Sleep(200);
else if (difficultLevel < 30 && difficultLevel >= 20)
Sleep(100);
else
Sleep(50);
preKey = ch; //记住当前的按键,用于和下一次的按键作对比
} while (!_kbhit()); //如果没有按键,则认为和上次按键一样
if (!isAlive)
break;
}
}
Player.cpp
到目前为止,整个贪吃蛇代码就开发完了,接下来就是要初始化一个测试用户了。假设现在有个叫王二蛋的人想玩这个贪吃蛇游戏,因此只需要在main函数中初始化一个叫作王二蛋的变量即可,如下:
#include
#include
#include "Wall.h"
#include "Snake.h"
#include "Food.h"
#include "Player.h"
int main()
{
srand(time(NULL)); //随机数种子
Wall wall; //初始化一个墙对象
Food food(wall); //初始化一个食物对象
Snake snake(wall, food); //初始化一个蛇对象
Player wangErdan(snake, wall); //玩家王二蛋闪亮登场
food.generateFood(); //第一次玩,需要有一个初始化的食物
wall.drawWall(snake.getScore()); //绘制第一屏,此时蛇并未激活
wangErdan.play(); //王二蛋开始玩游戏了
return 0;
}
main.cpp