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. ubuntu网速慢解决方法

    ubuntu网速慢解决方法 (2011-04-02 09:58:21)         本人在Window7下装ubuntu10.10双系统,在window7下速度挺快的,到了ubuntu速度就慢了很 ...

  3. Windows 环境下于虚拟环境中源码安装 cx_oracle

    安装前提条件: (1).安装 instantclient-basic-nt (2).安装 instantclient-sdk-nt (3).安装 Microsoft Visual C++ Compil ...

  4. ubuntu 编译并安装resin3.1.12+nginx1.2.6

    一.先装jdk 先建立如下两个目录: mkdir /usr/lib/jvm mkdir /usr/lib/jvm/java 把jdk-6u26-linux-x64.bin文件传到上面目录下: chmo ...

  5. linq 条件查询与分页

    <div>姓名:<asp:TextBox ID="T1" runat="server"></asp:TextBox>< ...

  6. opennebula 模板参数说明

    两种模板配置方式一.光驱引导启动需要配置:disk1:磁盘类型:cdrom      驱动类型:file      磁盘标记:hd      是否只读:yesDisk2:磁盘类型:DATABLOCK驱 ...

  7. Inception安装

    前言: MySQL语句需要审核,这一点每个DBA及开发人员都懂,但介于语句及环境的复杂性,大部分人都是望而却步,对其都是采取妥协的态度,从而每个公司都有自己的方法. 大多数公司基本都是半自动化(脚本+ ...

  8. jQuery中关于toggle的使用

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>t ...

  9. css总结13:CSS 伪类(Pseudo-classes)

    1 伪类作用:CSS伪类是用来添加一些选择器的特殊效果. 2 常用示例: 2.1anchor伪类:代码:   正常语法: a{color:#FF0000;}/* 文字颜色 */   伪类语法: a:l ...

  10. (转)常见存储过程分页PK赛——简单测试分析常见存储过程分页速度

    原文地址:http://www.cnblogs.com/yangyy753/archive/2013/01/23/2872753.html 数据的分页是我们再熟悉不过的功能了,各种各样的分页方式层出不 ...