为了能够最简单地完成程序,所以没有用类的继承等知识。感兴趣的朋友可以改写一下。
const int FILE_SIZE = 30; //地图方格大小
const int FPS = 5000 / 33; //游戏运行帧率
enum Item{empty, wall, food, snake}; //方格的类型
//地图大小
const int mapWidth = 20;
const int mapHeight = 20;
为了存储一条蛇, 我们需要的变量有:
下面是Snake类的定义:
class Snake
{
public:
Snake(QObject* parent, iItem ** _map, int x, int y);
void setDirection(const int x, const int y);
void move();
void grow();
bool eatSelf();
int length()const{return tail.size();}
QPoint getPos()const{return head;}
private:
bool isMoving();
QPoint head;
QList<QPoint> tail;
int moveX = 0;
int moveY = 0;
iItem **map;
};
贪吃蛇中的唯一难点, 就是蛇移动的逻辑了. 蛇移动不必更新身体每一个点的位置. 相反, 只要移动头, 并把头原来的位置加入身子, 并且去掉身体的最后就可以了.
函数的实现:
Snake::Snake(QObject *parent, iItem **_map, int x, int y) : Item(parent), map(_map)
{
head = QPoint(x, y);
map[x][y] = snake;
}
void Snake::setDirection(const int x, const int y)
{
moveX = x;
moveY = y;
}
void Snake::move()
{
if(!isMoving()) return;
QPoint newHead = head;
//更新head的位置
newHead.setX((newHead.x() + moveX) % mapWidth);
newHead.setY((newHead.y() + moveY) % mapHeight);
if(!tail.empty()){
//如果有尾巴的话, 将原来head作为tail的头
tail.push_front(head);
//剁掉尾巴的最后
QPoint t = tail.back();
map[t.x()][t.y()] = empty;
tail.pop_back();
}
else{
//没有尾巴, 则把原来的地图位置置空
map[head.x()][head.y()] = empty;
}
head = newHead; //更新head
map[head.x()][head.y()] = snake; //更新地图
}
bool Snake::isMoving()
{
return moveX != 0 || moveY != 0;
}
bool Snake::eatSelf()
{
// 如果tail链表中包含与head坐标一样的点, 说明蛇头撞到身子了
// contains是QList类内置的函数
return tail.contains(head);
}
void Snake::grow() //变长
{
QPoint t = head;
//尾巴变长的方向和移动方向相反
t.setX((t.x() - moveX) % mapWidth);
t.setY((t.y() - moveY) % mapHeight);
tail.append(t);
map[t.x()][t.y()] = snake;
}
class GameController : public QObject
{
Q_OBJECT
public:
Item** map;
GameController(QObject *parent);
~GameController();
protected:
//这个函数由于把QMainWindow收到的键盘事件在handlerKeyPressed里处理
virtual bool eventFilter(QObject *watched, QEvent *event) override;
void handleKeyPressed(QKeyEvent* event);
private:
QTimer timer;
bool paused = false; //游戏暂停
Snake *snake;
QPoint foodPos;
void checkCollision();
bool snakeEatFood();
void snakeEatSelf();
void snakeHitWall();
void addFood();
void initMap(); //初始化地图
signals:
void updateView(); //通知更新显示
void over(); //通知游戏终止
public slots:
void updateGame(); //更新游戏状态
void gameover(); //游戏结束
void stop(); //游戏暂停
void resume(); //游戏恢复进行
void newGame(); //再开一局
};
void GameController::initMap()
{
for(int i = 0; i < mapWidth; ++i){
for(int j = 0; j < mapHeight; ++j){
map[i][j] = empty;
}
}
}
GameController::GameController(QObject *parent) : QObject(parent)
{
//创建动态数组
map = new Item*[mapWidth];
for(int i = 0; i < mapWidth; ++i){
map[i] = new iItem[mapHeight];
}
initMap(); //初始化地图
//连接信号
connect(&timer, &QTimer::timeout, this, &GameController::updateGame);
timer.start(FPS); //开始计时
//蛇初始化到地图中央
snake = new Snake(this, map, mapWidth/2, mapHeight/2);
addFood(); //生成食物
}
GameController::~GameController()
{
for(int i = 0; i < mapWidth; ++i){
delete []map[i];
}
delete []map;
delete snake;
}
bool GameController::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
handleKeyPressed((QKeyEvent *)event); //自定义的按键处理函数
return true; //返回已处理
} else {
return QObject::eventFilter(watched, event); //不处理
}
}
void GameController::handleKeyPressed(QKeyEvent *event)
{
if(paused){
if(event->key() == Qt::Key_Space){
resume();
}
return;
}
//设置方向
if(event->key() == Qt::Key_Up){
snake->setDirection(0, -1);
}
else if(event->key() == Qt::Key_Down){
snake->setDirection(0, 1);
}
else if(event->key() == Qt::Key_Left){
snake->setDirection(-1, 0);
}
else if(event->key() == Qt::Key_Right){
snake->setDirection(1, 0);
}
else if(event->key() == Qt::Key_Space){
stop();
}
}
void GameController::updateGame()
{
snake->move(); //蛇移动
checkCollision(); //检查碰撞
emit updateView(); //更新显示
}
void GameController::checkCollision()
{
if(snake->eatSelf()){ // head撞到身体
gameover();
}
if(snakeEatFood()){ //吃食物
addFood(); //生成下一个食物
snake->grow(); //蛇生长
}
}
void GameController::addFood()
{
int n, x, y;
do{ //随机在不是蛇的地方生成食物
n = rand() % (mapWidth * mapHeight);
y = n / mapWidth;
x = n % mapWidth;
}
while(map[x][y] == snake);
map[x][y] = food; //更新地图
foodPos = QPoint(x, y); //存储食物的位置
}
void GameController::gameover()
{
stop(); //游戏暂停
emit over(); //更新UI
}
void GameController::stop()
{
timer.stop();
paused = true;
}
void GameController::resume()
{
timer.start(FPS);
paused = false;
}
void GameController::newGame()
{
delete snake;
initMap();
snake = new Snake(this, map, mapWidth/2, mapHeight/2);
addFood();
resume();
}
bool GameController::snakeEatFood()
{
return snake->getPos() == foodPos;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
protected:
virtual void paintEvent(QPaintEvent* event); //绘制地图
private:
GameController *game;
public slots:
void showMessageBox(); //死亡时显示的提示框
void updateView(); //更新游戏显示
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setFixedSize(FILE_SIZE * mapWidth, FILE_SIZE * mapHeight); //设置窗口大小
game = new GameController(this); //新开始游戏
this->installEventFilter(game); //对收到的事件不接收,让game接收
//信号连接
connect(game, &GameController::updateView, this, &MainWindow::updateView);
connect(game, &GameController::over, this, &MainWindow::showMessageBox);
}
MainWindow::~MainWindow()
{
delete game;
}
void MainWindow::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
//画背景
painter.fillRect(0, 0, mapWidth * FILE_SIZE, mapHeight * FILE_SIZE, Qt::gray);
//画方格内的物体
//蛇,食物,墙分别用不同颜色表示
for(int i = 0; i < mapWidth; ++i){
for(int j = 0; j < mapHeight; ++j){
switch (game->map[i][j]) {
case empty:
break;
case food:
painter.fillRect(i * FILE_SIZE, j * FILE_SIZE, FILE_SIZE, FILE_SIZE, Qt::red);
break;
case snake:
painter.fillRect(i * FILE_SIZE, j * FILE_SIZE, FILE_SIZE, FILE_SIZE, Qt::yellow);
break;
case wall:
break;
}
}
}
}
void MainWindow::showMessageBox()
{
if (QMessageBox::Yes == QMessageBox::information(NULL,
tr("Game Over"), tr("Again?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes)) {
game->newGame(); //如果选了Yes则重新开始
}
else{
exit(0); //否则推出
}
}
void MainWindow::updateView()
{
update(); //更新, 会自动调用paintEvent函数
}