Qt 学习之路 2(34):贪吃蛇游戏(4)

这将是我们这个稍大一些的示例程序的最后一部分。在本章中,我们将完成GameController中有关用户控制的相关代码。

首先,我们来给GameController添加一个事件过滤器:

 
 
bool GameController::eventFilter(QObject *object, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
handleKeyPressed((QKeyEvent *)event);
return true;
} else {
return QObject::eventFilter(object, event);
}
}
1
2
3
4
5
6
7
8
9
bool GameController::eventFilter(QObject *object, QEvent *event)
{
    if (event->type() == QEvent::KeyPress) {
        handleKeyPressed((QKeyEvent *)event);
        return true;
    } else {
        return QObject::eventFilter(object, event);
    }
}

回忆一下,我们使用QGraphicsScene作为游戏场景。为什么不直接继承QGprahicsScene,重写其keyPressEvent()函数呢?这里的考虑是:第一,我们不想只为重写一个键盘事件而继承QGraphicScene。这不符合面向对象设计的要求。继承首先应该有“是一个(is-a)”的关系。我们将游戏场景继承QGraphcisScene当然满足这个关系,无可厚非。但是,继承还有一个“特化”的含义,我们只想控制键盘事件,并没有添加其它额外的代码,因此感觉并不应该作此继承。第二,我们希望将表示层与控制层分离:明明已经有了GameController,显然,这是一个用于控制游戏的类,那么,为什么键盘控制还要放在场景中呢?这岂不将控制与表现层耦合起来了吗?基于以上两点考虑,我们选择不继承QGraphicsScene,而是在GameController中为场景添加事件过滤器,从而完成键盘事件的处理。下面我们看看这个handleKeyPressed()函数是怎样的:

 
 
void GameController::handleKeyPressed(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Left:
snake->setMoveDirection(Snake::MoveLeft);
break;
case Qt::Key_Right:
snake->setMoveDirection(Snake::MoveRight);
break;
case Qt::Key_Up:
snake->setMoveDirection(Snake::MoveUp);
break;
case Qt::Key_Down:
snake->setMoveDirection(Snake::MoveDown);
break;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GameController::handleKeyPressed(QKeyEvent *event)
{
    switch (event->key()) {
        case Qt::Key_Left:
            snake->setMoveDirection(Snake::MoveLeft);
            break;
        case Qt::Key_Right:
            snake->setMoveDirection(Snake::MoveRight);
            break;
        case Qt::Key_Up:
            snake->setMoveDirection(Snake::MoveUp);
            break;
        case Qt::Key_Down:
            snake->setMoveDirection(Snake::MoveDown);
            break;
    }
}

这段代码并不复杂:只是设置蛇的运动方向。记得我们在前面的代码中,已经为蛇添加了运动方向的控制,因此,我们只需要修改这个状态,即可完成对蛇的控制。由于前面我们已经在蛇的对象中完成了相应控制的代码,因此这里的游戏控制就是这么简单。接下来,我们要完成游戏逻辑:吃食物、生成新的食物以及咬到自己这三个逻辑:

 
 
void GameController::snakeAteFood(Snake *snake, Food *food)
{
scene.removeItem(food);
delete food;

addNewFood();
}

1
2
3
4
5
6
7
void GameController::snakeAteFood(Snake *snake, Food *food)
{
    scene.removeItem(food);
    delete food;
 
    addNewFood();
}

首先是蛇吃到食物。如果蛇吃到了食物,那么,我们将食物从场景中移除,然后添加新的食物。为了避免内存泄露,我们需要在这里 delete 食物,以释放占用的空间。当然,你应该想到,我们肯定会在addNewFood()函数中使用 new 运算符重新生成新的食物。

 
 
void GameController::addNewFood()
{
int x, y;

do {
x = (int) (qrand() % 100) / 10;
y = (int) (qrand() % 100) / 10;

x *= 10;
y *= 10;
} while (snake->shape().contains(snake->mapFromScene(QPointF(x + 5, y + 5))));

Food *food = new Food(x , y);
scene.addItem(food);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void GameController::addNewFood()
{
    int x, y;
 
    do {
        x = (int) (qrand() % 100) / 10;
        y = (int) (qrand() % 100) / 10;
 
        x *= 10;
        y *= 10;
    } while (snake->shape().contains(snake->mapFromScene(QPointF(x + 5, y + 5))));
 
    Food *food = new Food(x , y);
    scene.addItem(food);
}

addNewFood()代码中,我们首先计算新的食物的坐标:使用一个循环,直到找到一个不在蛇身体中的坐标。为了判断一个坐标是不是位于蛇的身体上,我们利用蛇的shape()函数。需要注意的是,shape()返回元素坐标系中的坐标,而我们计算而得的 x,y 坐标位于场景坐标系,因此我们必须利用QGraphicsItem::mapFromScene()将场景坐标系映射为元素坐标系。当我们计算出食物坐标后,我们在堆上重新创建这个食物,并将其添加到游戏场景。

 
 
void GameController::snakeAteItself(Snake *snake)
{
QTimer::singleShot(0, this, SLOT(gameOver()));
}

void GameController::gameOver()
{
scene.clear();

snake = new Snake(*this);
scene.addItem(snake);
addNewFood();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
void GameController::snakeAteItself(Snake *snake)
{
    QTimer::singleShot(0, this, SLOT(gameOver()));
}
 
void GameController::gameOver()
{
    scene.clear();
 
    snake = new Snake(*this);
    scene.addItem(snake);
    addNewFood();
}

如果蛇咬到了它自己,游戏即宣告结束。因此,我们直接调用gameOver()函数。这个函数将场景清空,然后重新创建蛇并增加第一个食物。为什么我们不直接调用gameOver()函数,而是利用QTimer调用呢(希望你没有忘记QTimer::singleShot(0, ...)的用法)?这是因为,我们不应该在一个 update 操作中去清空整个场景。因此我们使用QTimer,在 update 事件之后完成这个操作。

至此,我们已经把这个简单的贪吃蛇游戏全部完成。最后我们来看一下运行结果:

文末的附件中是我们当前的全部代码。如果你检查下这部分代码,会发现我们其实还没有完成整个游戏:Wall对象完全没有实现,难度控制也没有完成。当然,通过我们的讲解,希望你已经理解了我们设计的原则以及各部分代码之间的关系。如果感兴趣,可以继续完成这部分代码。豆子在 github 上面创建了一个代码库,如果你感觉自己的改进比较成功,或者希望与大家分享,欢迎 clone 仓库提交代码!

附件:snake
git:git@github.com:devbean/snake-game.git

WIN1064BIT QTCREATOR  QT5.5.1 需要delete

    1. 斯啦丝拉 2013年2月26日

      我的环境是Ubuntu 12.10 64bit/gcc 2.7.2

      void GameController::snakeAteFood(Snake *snake, Food *food)
      {
      scene.removeItem(food);
      delete food;

      addNewFood();
      }中,
      delete food;这句代码会导致程序报错退出。注释掉程序可以正常使用,但是内存会泄露。如果在一个函数体内new/delete Food,程序不会崩溃。
      还望答疑。

      • 豆子 2013年2月27日

        会不会是 gcc 版本太低?2.7.2?

        • 斯啦丝拉 2013年2月28日

          呃……写错了,4.7.2

          • 豆子 2013年3月4日

            这个还没有测试过,暂不知是哪里的问题…不好意思哦

            • qingxp9 2013年5月26日

              我在ubuntu12.04 gcc编译后,游戏开始直接向上吃food,程序也报错退出。 win下mingw编译正常

               

            • 豆子 2013年5月27日

              这个我没有在 ubuntu 上面测试过,可以的话帮忙检查下哪里问题哦~

               

      • devnull 2013年9月22日

        我也遇到这个问题了,有人知道怎么解决吗?

        • 豆子 2013年9月22日

          你的环境是怎样的?我在 openSUSE 12.3 上面使用 gcc 4.7.2 20130108,Qt 4.8.4 64bits 测试是正常的

          • devnull 2013年9月22日

            我是在Ubuntu 12.04 TLS 64bit,Qt-5.1.1, gcc 4.6.3测试的,现象和@qingxp9,@斯啦丝拉的一样,注释掉delete food;就能正常运行。

            • 豆子 2013年9月23日

              的确有这个问题,不过现在也没有弄清楚怎么回事,稍等一段时间研究研究

               

            • sxy 2015年6月25日

              我看第一个food是在controller构造函数中new的,名字叫a1,把这个删掉调用addnewfood创建food就不会delete出错了,不过每次第一个food都在蛇右边第一个格子,不知道怎么回事

               

      • newbie 2014年3月22日

        ”在构造函数中使用new来分配内存时,必须在相应析构函数中使用delete来释放内存。“
        引自《C++ Prime Plus》(第五版 )P381

        • sunskybrave 2017年7月31日

          我感觉newbie兄说的有点有点接近,但是有疏漏。”在构造函数中使用new来分配内存时,必须在相应析构函数中使用delete来释放内存。“中的分配内存指的是用new运算符为对象中的成员数据动态分配内存,而此处的a1并不是类的成员数据,所以你不应该在析构函数中去delete。之所会崩溃我认为是不应该像原程序那样直接去析构a1,而应该区别对待下a1,具体的原因不是很清楚。
          一种解决办法是在gamecontroller.cpp 中添加静态全局变量static int num=0;
          static Food *a1=NULL;然后修改函数snakeate函数为
          void GameController::snakeAteFood(Snake *snake, Food *food)
          {
          num++;
          if(num==1)
          {
          scene.removeItem(food);
          delete a1;
          }
          else
          {
          scene.removeItem(food);
          delete food;

          addNewFood();
          }
          }

          • sunskybrave 2017年8月1日

            delete a1后再加个addNewFood();

             

Qt 学习之路 2(34):贪吃蛇游戏(4)的更多相关文章

  1. Qt 学习之路 2(72):线程和事件循环

    Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻>  --  有需求的话还需要进行专题学习  豆子  2013年11月24日  Qt 学习之路 2  34条评论 前面一章我 ...

  2. Qt 学习之路 2(47):视图选择

    Qt 学习之路 2(47):视图选择 豆子 2013年3月28日 Qt 学习之路 2 34条评论 选择是视图中常用的一个操作.在列表.树或者表格中,通过鼠标点击可以选中某一项,被选中项会变成高亮或者反 ...

  3. Qt 学习之路 2(33):贪吃蛇游戏(3)

    Qt 学习之路 2(33):贪吃蛇游戏(3) 豆子 2012年12月29日 Qt 学习之路 2 16条评论 继续前面一章的内容.上次我们讲完了有关蛇的静态部分,也就是绘制部分.现在,我们开始添加游戏控 ...

  4. Qt 学习之路 2(32):贪吃蛇游戏(2)

    Qt 学习之路 2(32):贪吃蛇游戏(2) 豆子 2012年12月27日 Qt 学习之路 2 55条评论 下面我们继续上一章的内容.在上一章中,我们已经完成了地图的设计,当然是相当简单的.在我们的游 ...

  5. Qt 学习之路 2(31):贪吃蛇游戏(1)

    Qt 学习之路 2(31):贪吃蛇游戏(1) 豆子 2012年12月18日 Qt 学习之路 2 41条评论 经过前面一段时间的学习,我们已经了解到有关 Qt 相当多的知识.现在,我们将把前面所讲过的知 ...

  6. 《Qt 学习之路 2》目录

    <Qt 学习之路 2>目录 <Qt 学习之路 2>目录  豆子  2012年8月23日  Qt 学习之路 2  177条评论 <Qt 学习之路 2>目录 序 Qt ...

  7. Qt 学习之路 2(71):线程简介

    Qt 学习之路 2(71):线程简介 豆子 2013年11月18日 Qt 学习之路 2 30条评论 前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程.事实上,现代的程序中,使用线程 ...

  8. Qt 学习之路 2(67):访问网络(3)

    Qt 学习之路 2(67):访问网络(3) 豆子 2013年11月5日 Qt 学习之路 2 16条评论 上一章我们了解了如何使用我们设计的NetWorker类实现我们所需要的网络操作.本章我们将继续完 ...

  9. Qt 学习之路 2(66):访问网络(2)

    Home / Qt 学习之路 2 / Qt 学习之路 2(66):访问网络(2) Qt 学习之路 2(66):访问网络(2)  豆子  2013年10月31日  Qt 学习之路 2  27条评论 上一 ...

随机推荐

  1. PHP数据结构之一:PHP数据结构基本概念—数据结构

    学习任何一种技术都应该先清楚它的基本概念,这是学习任何知识的起点!本文是讲述数据结构的基本概念,适合对数据结构已经有一定基础的程序员,更是适合想要学习数据结构的code一族!让我们开始PHP数据结构的 ...

  2. windows下安装ubuntu 12.04---利用ubuntu的iso包中的wubi.exe工具安装

    一.下载ubuntu-12.04-desktop-amd64.iso后,用winrar打开,提取出wubi.exe这个文件.把ubuntu-12.04-desktop-amd64.iso和wubi.e ...

  3. 主机不能访问虚拟机中的web服务【解决方案】

    百度了其它一些方法都不行,最后实在没辙,关了windows防火墙和Linux防火墙,居然能够访问了,我服. 总结一下,原来是Red Hat Linux 6.0防火墙没有开启端口80,开启的方法为(老版 ...

  4. zend studio 字体大小修改,默认编码设置

    zend studio的字体感觉很小,很多用户不是很适应,修改方法如下: 第一步:进入设置窗口    windows -> preferences 第二步:进入修改字体的选项卡.    Gene ...

  5. PrimeNG01 angular集成PrimeNG

    1 开发环境 本博文基于angular5 2 步骤 2.1 创建angular5项目 详情参见百度 2.2 下载PrimeNG依赖 npm install primeng --save npm ins ...

  6. ubuntu 16.04 ARM glog移植

    1. 下载源文件https://github.com/google/glog 2. 源文件有CMakeLists.txt, 直接使用toolchain.cmake 直接编译就可以了,详情参考我的随笔  ...

  7. Installing XGBoost on Mac OSX

      0. Get gcc with open mp.  Just paste and execute the following command in your terminal, once Home ...

  8. css总结9:内边距(padding)和外边距(margin)

    1 css总结9:内边距和外边距 通过css总结8:盒子模型可知:内边距(padding),外边距(margin).可以影响盒子在浏览器的位置. 1.1 padding使用:{padding:上 右 ...

  9. Linq 左连接 left join

    Suppose you have a tblRoom and tblUserInfo. Now, you need to select all the rooms regardless of whet ...

  10. c++基础之向量Vector

    首先和string一样要在开头 #include <vector> #include <string> 和string一样,也算是一种容器,而且同属于STL(standard ...