首页 Qt 学习之路 2 Qt 学习之路 2(32):贪吃蛇游戏(2)

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

59 9

下面我们继续上一章的内容。在上一章中,我们已经完成了地图的设计,当然是相当简单的。在我们的游戏中,另外的主角便是蛇和食物。下面我们便开始这部分的开发。

我们的地图是建立在QGraphicsScene的基础之上的,所以,里面的对象应该是QGraphicsItem实例。通常,我们会把所有的图形元素(这里便是游戏中需要的对象,例如蛇、食物等)设计为QGraphicsItem的子类,在这个类中添加绘制自身的代码以及动画逻辑。这也是面向对象的开发方式:封装自己的属性和操作。在我们的游戏中,应该有三个对象:蛇 Snake、食物 Food 以及墙 Wall。

我们从食物开始。因为它是最简单的。我们将其作为一个红色的小圆饼,大小要比地图中的一个方格要小,因此我们可以将其放置在一个方格中。正如上面分析的那样,我们的Food类需要继承QGraphicsItem。按照接口约束,QGraphicsItem的子类需要重写至少两个函数:boundingRect()paint()

boundingRect()返回一个用于包裹住图形元素的矩形,也就是这个图形元素的范围。需要注意的是,这个矩形必须能够完全包含图形元素。所谓“完全包含”,意思是,在图形元素有动画的时候,这个矩形也必须将整个图形元素包含进去。如果范围矩形过小。图形会被剪切;如果范围矩形过大,就会影响性能。

paint()的作用是使用QPainter将图形元素绘制出来。

下面是 food.h 和 food.cpp 的内容:

////////// food.h //////////
#ifndef FOOD_H
#define FOOD_H

#include <QGraphicsItem>

class Food : public QGraphicsItem
{
public:
    Food(qreal x, qreal y);

    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *);

    QPainterPath shape() const;
};

#endif // FOOD_H

////////// food.cpp //////////
#include <QPainter>

#include "constants.h"
#include "food.h"

static const qreal FOOD_RADIUS = 3;

Food::Food(qreal x, qreal y)
{
    setPos(x, y);
    setData(GD_Type, GO_Food);
}

QRectF Food::boundingRect() const
{
    return QRectF(-TILE_SIZE,    -TILE_SIZE,
                   TILE_SIZE * 2, TILE_SIZE * 2 );
}

void Food::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    painter->save();

    painter->setRenderHint(QPainter::Antialiasing);
    painter->fillPath(shape(), Qt::red);

    painter->restore();
}

QPainterPath Food::shape() const
{
    QPainterPath p;
    p.addEllipse(QPointF(TILE_SIZE / 2, TILE_SIZE / 2), FOOD_RADIUS, FOOD_RADIUS);
    return p;
}

虽然这段代码很简单,我们还是有必要解释一下。构造函数接受两个参数:x 和 y,用于指定该元素的坐标。setData()函数是我们之后要用到的,这里简单提一句,它的作用为该图形元素添加额外的数据信息,类似于散列一样的键值对的形式。boundingRect()简单地返回一个QRect对象。由于我们的元素就是一个圆形,所以我们返回的是一个简单的矩形。注意,这个矩形的范围实际是四倍于实际区域的:以元素坐标 (x, y) 为中心,边长为TILE_SIZE * 2的正方形。我们还重写了shape()函数。这也是一个虚函数,但是并不是必须覆盖的。这个函数返回的是元素实际的路径。所谓路径,可以理解成元素的矢量轮廓线,就是QPainterPath所表示的。我们使用addEllipse()函数,添加了一个圆心为 (TILE_SIZE / 2, TILE_SIZE / 2),半径 FOOD_RADIUS 的圆,其范围是左上角为 (x, y) 的矩形。由于设置了shape()函数,paint()反而更简单。我们所要做的,就是把shape()函数定义的路径绘制出来。注意,我们使用了QPainter::save()QPainter::restore()两个函数,用于保存画笔状态。

现在我们有了第一个图形元素,那么,就让我们把它添加到场景中吧!对于一个游戏,通常需要有一个中心控制的类,用于控制所有游戏相关的行为。我们将其取名为GameController

GameController的工作是,初始化场景中的游戏对象,开始游戏循环。每一个游戏都需要有一个游戏循环,类型于事件循环。想象一个每秒滴答 30 次的表。每次响起滴答声,游戏对象才有机会执行相应的动作:移动、检查碰撞、攻击或者其它一些游戏相关的活动。为方便起见,我们将这一次滴答成为一帧,那么,每秒 30 次滴答,就是每秒 30 帧。游戏循环通常使用定时器实现,因为应用程序不仅仅是一个游戏循环,还需要响应其它事件,比如游戏者的鼠标键盘操作。正因为如此,我们不能简单地使用无限的 for 循环作为游戏循环。

在 Graphics View Framework 中,每一帧都应该调用一个称为advance()的函数。QGraphicsScene::advance()会调用场景中每一个元素自己的advance()函数。所以,如果图形元素需要做什么事,必须重写QGraphicsItemadvance(),然后在游戏循环中调用这个函数。

GameController创建并开始游戏循环。当然,我们也可以加入pause()resume()函数。现在,我们来看看它的实现:

GameController::GameController(QGraphicsScene *scene, QObject *parent) :
    QObject(parent),
    scene(scene),
    snake(new Snake(this))
{
    timer.start(1000/33);

    Food *a1 = new Food(0, -50);
    scene->addItem(a1);

    scene->addItem(snake);

    scene->installEventFilter(this);

    resume();
}

GameController的构造函数。首先开启充当游戏循环的定时器,定时间隔是 1000 / 33 毫秒,也就是每秒 30(1000 / 33 = 30)帧。GameController有两个成员变量:scene 和 snake,我们将第一个食物和蛇都加入到场景中。同时,我们为GameController添加了事件过滤器,以便监听键盘事件。这里我们先不管这个事件过滤器,直接看看后面的代码:

void GameController::pause()
{
    disconnect(&timer, SIGNAL(timeout()),
               scene,  SLOT(advance()));
}

void GameController::resume()
{
    connect(&timer, SIGNAL(timeout()),
            scene,  SLOT(advance()));
}

pause()resume()函数很简答:我们只是连接或者断开定时器的信号。当我们把这一切都准备好之后,我们把GameController添加到MainWindow中:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
      game(new GameController(scene, this))
{
    ...
}

由于GameController在构造时已经开始游戏循环,因此我们不需要另外调用一个所谓的“start”函数。这样,我们就把第一个食物添加到了游戏场景:

snake 第一步

接下来是有关蛇的处理。

蛇要更复杂一些。在我们的游戏中,蛇是由黄色的小方块组成,这是最简单的实现方式了。第一个是蛇的头部,紧接着是它的身体。对此,我们有两个必须面对的困难:

  1. 蛇具有复杂得多的形状。因为蛇的形状随着游戏者的控制而不同,因此,我们必须找出一个能够恰好包含蛇头和所有身体块的矩形。这也是 boundingRect() 函数所要解决的问题。
  2. 蛇会长大(比如吃了食物之后)。因此,我们需要在蛇对象中增加一个用于代表蛇身体长度的growing变量:当growing为正数时,蛇的身体增加一格;当growing为负数时,蛇的身体减少一格。
  3. advance()函数用于编码移动部分,这个函数会在一秒内调用 30 次(这是我们在GameController的定时器中决定的)。

我们首先从boundingRect()开始看起:

QRectF Snake::boundingRect() const
{
    qreal minX = head.x();
    qreal minY = head.y();
    qreal maxX = head.x();
    qreal maxY = head.y();

    foreach (QPointF p, tail) {
        maxX = p.x() > maxX ? p.x() : maxX;
        maxY = p.y() > maxY ? p.y() : maxY;
        minX = p.x() < minX ? p.x() : minX;
        minY = p.y() < minY ? p.y() : minY;
    }

    QPointF tl = mapFromScene(QPointF(minX, minY));
    QPointF br = mapFromScene(QPointF(maxX, maxY));

    QRectF bound = QRectF(tl.x(),  // x
                          tl.y(),  // y
                          br.x() - tl.x() + SNAKE_SIZE,      // width
                          br.y() - tl.y() + SNAKE_SIZE       //height
                          );
    return bound;
}

这个函数的算法是:遍历蛇身体的每一个方块,找出所有部分的最大的 x 坐标和 y 坐标,以及最小的 x 坐标和 y 坐标。这样,夹在其中的便是蛇身体的外围区域。

shape()函数决定了蛇身体的形状,我们遍历蛇身体的每一个方块向路径中添加:

QPainterPath Snake::shape() const
{
    QPainterPath path;
    path.setFillRule(Qt::WindingFill);

    path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE));

    foreach (QPointF p, tail) {
        QPointF itemp = mapFromScene(p);
        path.addRect(QRectF(itemp.x(), itemp.y(), SNAKE_SIZE, SNAKE_SIZE));
    }

    return path;
}

在我们实现了shape()函数的基础之上,paint()函数就很简单了:

void Snake::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    painter->save();
    painter->fillPath(shape(), Qt::yellow);
    painter->restore();
}

现在我们已经把蛇“画”出来。下一章中,我们将让它“动”起来,从而完成我们的贪吃蛇游戏。

59 评论

khedive 2012年12月28日 - 10:03

你好,有个疑问,Food的boundingRect()中为什么是4倍于实际区域,而不是9倍于实际区域的?按照我的理解,Food应该是可以上下左右移动,这样可能的区域应该9倍于本身啊?谢谢!

回复
DevBean 2012年12月28日 - 15:18

食物只是占有一个方格。之所以是位于四个方格的右下角,是因为这样程序写起来更方便,而不是上下左右移动的原因。

回复
Fuatnow 2016年8月28日 - 16:36

Hi,我发现boundingRect必须要重写。把food带入snake的boundingRect就是4个方格这样更容易理解对吧。
但是为什么加上一个SNAKE_SIZE,是不是因为第28节的梗啊?
```当绘制大于1个像素时,情况比较复杂:如果绘制像素是偶数,则实际绘制会包裹住逻辑坐标值;如果是奇数,则是包裹住逻辑坐标值,再加上右下角一个像素的偏移。```

回复
bobo 2013年1月17日 - 11:06

不是左上角吗?

回复
豆子 2013年1月18日 - 09:56

什么意思?

回复
msccreater 2013年3月8日 - 22:54

不是左上角,确实是右下角,Food中的boundingRect是这样定义的QRectF(-TILE_SIZE, -TILE_SIZE, TILE_SIZE * 2, TILE_SIZE * 2 ); 而Food中的shape是这样写的p.addEllipse(QPointF(TILE_SIZE / 2, TILE_SIZE / 2), FOOD_RADIUS, FOOD_RADIUS);也就是说在以x,y为左上角的顶点,TILE_SIZE*TILE_SIZE的矩形(坐标范围是从-TILE_SIZE到TILESIZE)中添加了一个圆,圆的坐标是(TILE_SIZE/2,TILE_SIZE/2)正好是矩形的右下角的中心点

回复
gavin 2013年3月30日 - 13:02

给GameController安装过滤器时为什么调用scene的函数?过滤器到底是装在scene上的还是装在GameController上的?

scene->installEventFilter(scene);
// 或者
this->installEventFilter(this);

这样行不行?

回复
豆子 2013年4月1日 - 09:01

首先,注意到我们的程序,过滤器是要作为 sence 的控制,监视的是 sence 接收到的键盘事件,因此过滤器是要安装在 sence 上面(这是过滤器安装的对象);第二,过滤器本身是由 controller 实现的,因此函数的参数是 this。总之,这是为一个对象(sence)安装另外一个对象(controller)实现的过滤器。

回复
Jakes 2013年5月4日 - 21:21

编译的时候提示了几个错误:
\moc_food.cpp:63: error: 'staticMetaObject' is not a member of 'QGraphicsItem'
moc_food.cpp:78: error: 'qt_metacast' is not a member of 'QGraphicsItem'
moc_food.cpp:83: error: 'qt_metacall' is not a member of 'QGraphicsItem'
这到底是什么原因呢?

回复
Jakes 2013年5月4日 - 21:23

晕,知道原因了。因为在类定义体内增加了Q_OBJECT

回复
hqwhqwhq 2015年12月1日 - 22:48

求问为什么加了Q_OBJECT就会编译错误啊!!!

回复
豆子 2015年12月6日 - 19:47

不知道什么错误?

回复
hqwhqwhq 2015年12月6日 - 20:56

豆子大神,就是我在类的定义里面为什么不可以加Q_OBJECT?加了以后就是和这楼的一样的编译错误。
\moc_food.cpp:63: error: ‘staticMetaObject’ is not a member of ‘QGraphicsItem’
moc_food.cpp:78: error: ‘qt_metacast’ is not a member of ‘QGraphicsItem’
moc_food.cpp:83: error: ‘qt_metacall’ is not a member of ‘QGraphicsItem’

豆子 2015年12月8日 - 16:08

加过 Q_OBJECT 之后需要重新运行 qmake 生成 makefile 才可以。

zz 2019年8月4日 - 11:34

在food 头文件 加的就出错 一样的去掉就好了 已经clean qmake过了

gameboy 2013年7月4日 - 23:44

QRectF Food::boundingRect() const
{
return QRectF(-TILE_SIZE, -TILE_SIZE,
TILE_SIZE * 2, TILE_SIZE * 2 );
}
这个返回的是个常量的QRectF,但在游戏中,随机出现的FOOD都是同样的boundingRect吗?与FOOD出现的位置没有关系?没看出boundingRect()在程序中的作用,不是有shape()来检测碰撞吗

回复
豆子 2013年7月5日 - 09:30

注意这个是 Food 类的函数,所以坐标系是 Food 为基准的,而不是使用的世界坐标。shape() 用来返回 Food 的轮廓线,boundingRect() 返回的是 Food 所占据的矩形区域,这是系统回调使用的,不是我们显式调用的。

回复
gameboy 2013年7月5日 - 12:55

意思是return QRectF(-TILE_SIZE, -TILE_SIZE, TILE_SIZE * 2, TILE_SIZE * 2 )中,左上角的点(-TILE_SIZE, -TILE_SIZE)是以food的坐标为原点(0,0)来计算的? 真是一语中的,解决了很多困惑.

回复
a9527 2014年4月7日 - 15:48

怎样看出来boundingRect()函数是系统回调使用的?谢谢

回复
9527 2014年4月7日 - 15:51

“boundingRect() 返回的是 Food 所占据的矩形区域,这是系统回调使用的,不是我们显式调用的。”哪里能看出来是系统回调的?

回复
豆子 2014年4月8日 - 10:56

表明看不出来是系统回调的,从文档和函数实现的功能以及你的经验确定吧。

回复
j 2013年7月25日 - 11:38

1,timer.start(1000/33); 每秒30次不应该是timer.start(1000/30);?

2,在 Graphics View Framework 中,每一帧都应该调用一个称为 advance() 的函数。QGraphicsScene::advance() 会调用场景中每一个元素自己的 advance() 函数。所以,如果图形元素需要做什么事,必须重写 QGraphicsItem 的 advance(),然后在游戏循环中调用这个函数。
问:Graphics View Framework每秒到底多少帧,30?还是可以自己设定.其次,蛇和食物都没有重写advance, 而controller有advance但是没加入sence,所以不会被框架调用,所以采用了定时器调用.理解正确没?

回复
豆子 2013年7月25日 - 13:04

1. 每秒 30 帧,每次就是 1000/30 毫秒,大约 33 毫秒,所以每秒 30 帧,timer 的间隔时间是 1000/33。

2. Graphics View Framework 没有帧的概念,是我们用定时器实现了帧,所以每秒多少帧取决于我们的定时器的设置,你那样理解应该也没有问题。

回复
j 2013年7月25日 - 13:13

还是关于:timer.start(1000/33);
30=1000/33;
timer.start(30)表示每30毫秒触发一次定时器事件,那么每秒共触发33次.
timer.start(33)表示每33毫秒触发一次定时器事件,那么每次共触发30次.
还是我对定时器理解有误.新手来着.

回复
Vincent 2013年8月13日 - 21:22

豆子师兄,我是刚接触C++的学生,我们要求做个大作业,师兄们都推荐来这里学QT。我想做只桌面宠物。可是不懂哪些功能要用哪些知识。(宠物基本就是能养,时不时会说点话)(如果可以还想实现监测cpu使用情况、温度,换衣服,可以唤出日历模块之类的)

回复
豆子 2013年8月14日 - 09:00

你做的东西挺复杂的,如果刚刚接触,并且时间允许的话,先把 C++ 了解下,然后看看 Qt 的基础,比如事件、信号槽之类。基于你的项目,个人感觉应该使用 Graphics View Framework 去实现。如果不限定一定使用 C++,QML 做起动画来会更简单一些(如果你的桌面宠物需要动画的话)。说话之类可以使用 Qt Multimedia 模块解决,这点不用担心。至于监测系统状况,大概只能调用系统 API 然后将数据显示出来,只要数据格式转换好,应该也不是很麻烦。

回复
Vincent 2013年8月14日 - 10:47

太棒了~~~回复很及时详细 😀 💡

回复
疯子 2013年8月28日 - 11:23

void Food::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)函数是重载的么,这个项目中没有调用这个函数,是怎么一个实现机理,本人是初学者...

回复
疯子 2013年8月28日 - 11:38

又仔细看了下文章的前部分,明白是重写,以后有问题一定好好想想再问...assistant上说是被QGraphicsView调用的,我想是在QGraphicsItem实例加入到QGraphicsSence中时调用的,不知理解是否正确。

回复
豆子 2013年8月29日 - 08:58

大致是这个样子,不过这个函数并不一定只调用一次,只要有需要就会被系统调用(这种不是有开发者主动调用的方式就叫“回调”),例如窗口大小改变、位置移动等

回复
疯子 2013年8月29日 - 11:53

嗯,谢谢豆子先生 😛

回复
NashLegend 2013年9月16日 - 18:11

运行了一下,boundingRect()是每次绘制都自动执行了。但是我看文档上说要改变bounding rectangle必须要先调用 prepareGeometryChange(),但是这里没调用。有什么区别吗

回复
NashLegend 2013年9月16日 - 23:32

boundingRect在每次setPos的时候都会自动调用一下。不用prepareGeometryChange,那什么时候用prepareGeometryChange呢

回复
豆子 2013年9月17日 - 14:54

个人理解,这里并没有改变 bounding rect,仅仅是定义,所以不需要调用 prepareGeometryChange()。如果你需要改变 bounding rect 的话,比如需要重新计算,则需要调用 prepareGeometryChange()

回复
Linguang 2015年1月4日 - 16:14

豆子你好,十分感谢你的文章,实在是太有帮助了,但是我有一点不是很明白在写resume()的函数的时候,为什么用如下形式的connect不能成功?

connect(&timer,&timer.timeout(),scene,&scene->advance());

回复
豆子 2015年1月10日 - 17:48

注意你的 &timer.timeout() 和 &scene->advance() 并不是取的函数指针,而是取的返回值的引用。connect() 函数的新语法接受的是函数指针。

回复
一条狗 2015年4月2日 - 10:34

豆子你好,我对snake的重绘有些不大明白,shape函数里 path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE));第一次调用是在原点处绘制,然后以后每次调用shape时,又为什么都要在原点设置这个点呢?我一直搞不明白这里,shape函数是在每次重绘都调用呢,还是只调用一次?非常感谢你的文章,通俗易懂。

回复
小蛋 2015年8月10日 - 18:00

对于path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE));
我最初也有疑惑。你可以试一下把这一句注释掉,看一下有什么差别,就能知道这条语句有什么作用了。PS:感谢豆子老师。o(^▽^)o

回复
lr 2019年5月29日 - 20:09

我的理解,shape是每次重绘都会调用的, 每次调用path.addRect(QRectF(0, 0, SNAKE_SIZE, SNAKE_SIZE)),但并不是每次都会绘制这个点,因为boundingRect 会根据tail里的所有点限定绘画的范围。

回复
bird 2015年4月20日 - 21:19

大神,能不能把initscenebackground()这个函数的每行详细解释下,不是很明白呀。。

回复
豆子 2015年4月21日 - 13:23

这段代码并不是很复杂。首先创建一个QPixmap对象,然后在其上面进行绘制,绘制的内容是边长为TILE_SIZE的灰色正方形,然后以此图像创建一个画刷QBrush,并将该画刷作为背景图案进行平铺。

回复
A 2015年6月3日 - 09:26

豆子哥您好,请问boundingRectF返回的矩形到底怎么理解?
您之前关于food的返回解释的是:以(x,y)为圆心。
但是从这次的参数来说 您使用了-TILE_SIZE, -TILE_SIZE, 作为左上角 也就是-10,-10为左上角。 20为长和宽 这样画出来 中心是0,0啊?

而且关于snake的boundingRectF的返回参数值也不是了解很清楚。
拜托讲解
感谢

回复
豆子 2015年6月3日 - 16:12

前一个以 (x, y) 为圆心,指的是场景坐标,也就是在 scene 中的位置。后面的以 (0, 0) 为中心,指的是在元素自己的坐标系下。
snake 的 boundingRectF() 函数中,通过遍历每一个组成方块,找到最上、最下、最左、最右这四个边界值,返回的就是由这四个边界值组成的矩形。

回复
唯儿 2015年6月21日 - 13:38

豆子老师,初学者表示很痛苦,能把类的声明也帖出来吗,tail是什么类型?第31讲中给的链接也打不开

回复
hlx1996 2015年11月28日 - 00:19

首先开启充当游戏循环的定时器,定时间隔是 1000 / 33 毫秒,也就是每秒 30(1000 / 33 = 30)帧。

时间间隔为1000/33毫秒 = 1/33秒,每秒不应该是33帧吗?

回复
hlx1996 2015年12月1日 - 21:28

博主在吗?求回答一下我的问题T_T

回复
dong 2015年12月7日 - 10:11

/home/tool/qt_pro/game/mainwindow.cpp:8: error: invalid use of incomplete type 'struct GameController',这个类的头文件是啥?

回复
豆子 2015年12月8日 - 16:09

GameController 在源代码包是应该有的,完整的代码在最后一篇。

回复
动感超人 2016年4月8日 - 23:00

请问为什么要先painter->save()? 我为什么之前都没有弄就直接保存一下。。。,,还有就是注释掉painter->save() 和painter->restore() 发现程序仍然运行正常,那要这两个函数何用?

回复
豆子 2016年4月11日 - 21:48

注意这里的 painter 指针是参数传进来的,也就是 painter 是系统回调时创建的,可能会被其它组件共用。这里是为了防止影响到其它组件的使用(考虑到 QPainter 是一个状态机),因此需要添加这两个语句。当然,因为我们的程序比较简单,没有类似情况,所以不会有影响,但是不能保证复杂情况下也没有影响。为谨慎起见,这里添加了保存状态的代码。

回复
joker 2016年11月16日 - 15:28

不知道别人看起来怎么样 反正我看了三十章是死撑过来的,很多东西没讲清楚啊!!!比如shape 函数 使用 QPainterPath 设置一个椭圆轮廓,然后再把这个轮廓传递给QPainter画出来,可是文章就一句话带过,完全没讲清楚它具体是怎样画出来的,怎样传递的参数,各个参数的意思。好吧 我比较菜

回复
joker 2016年11月16日 - 15:29

我是反复看了好多遍才理解的,看您的文章要一边看一边查手册

回复
豆子 2016年11月19日 - 10:44

具体的函数参数需要自己去看文档吧,文中只是说了一个大概。我的看法是,这样的教程类文章只需要讲清楚实现的思路,具体的函数参数这样的细节需要自己查阅相关文档。

回复
alan 2017年12月28日 - 17:33

请问Snake::paint()函数在哪里调用??
我的理解是snake每走一步就应该调用一次paint()函数绘制啊,可是一直没看到在哪里调用。

回复
豆子 2017年12月28日 - 21:36

paint() 函数是系统回调函数,会在系统“需要”的时候自动调用——这正是“回调”的含义——可能是在窗口大小改变的时候,可能是在最大化、最小化的时候。对于此处,paint() 函数只是给出了 Snake 如何绘制自身,具体的绘制(也就是实际调用),是在窗口显示的时候调用的。

回复
alan 2017年12月29日 - 10:16

你好,我还有个问题,pause()函数和resume()函数在例程里面好像没怎么用,我在
void GameController::handleKeyPressed(QKeyEvent *event)函数中添加了:
case Qt::Key_A:
pause();
case Qt::Key_B:
resume();
为何我按A时,蛇没有暂停,而是恢复到原来的速度,我按B时,蛇会加快速度。

回复
yang 2018年12月12日 - 11:17

connect(&timer, SIGNAL(timeout()),
scene, SLOT(advance()));

这里scene前面要加&。

回复
Jimmy 2020年9月2日 - 21:32

请问博主为什么碰撞区域是四倍于实际区域呢?为什么不是本身(1倍区域)进行判定呢?是和背后的碰撞判定实现有关吗?

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2