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

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

我们的地图是建立在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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
////////// 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();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()));
}

1
2
3
4
5
6
7
8
9
10
11
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))
{
...
}
1
2
3
4
5
6
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
      game(new GameController(scene, this))
{
    ...
}

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

接下来是有关蛇的处理。

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

  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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}

    foreach (QPointF p, tail) {   这里怎么理解 p从哪里来?
        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;
    }

这个函数的算法是:遍历蛇身体的每一个方块,找出所有部分的最大的 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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
1
2
3
4
5
6
void Snake::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    painter->save();
    painter->fillPath(shape(), Qt::yellow);
    painter->restore();
}

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

gameboy  2013年7月4日 

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日 

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

      • gameboy  2013年7月5日 

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

      • a9527  2014年4月7日 

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

      • 9527  2014年4月7日 

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

        • 豆子  2014年4月8日 

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

           

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

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

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

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

    Qt 学习之路 2(34):贪吃蛇游戏(4) 豆子 2012年12月30日 Qt 学习之路 2 73条评论 这将是我们这个稍大一些的示例程序的最后一部分.在本章中,我们将完成GameControlle ...

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

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

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

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

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

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

  6. Qt 学习之路 2(69):进程

    Qt 学习之路 2(69):进程 豆子 2013年11月9日 Qt 学习之路 2 15条评论 进程是操作系统的基础之一.一个进程可以认为是一个正在执行的程序.我们可以把进程当做计算机运行时的一个基础单 ...

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

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

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

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

  9. Qt 学习之路 2(63):使用 QJson 处理 JSON

    Home / Qt 学习之路 2 / Qt 学习之路 2(63):使用 QJson 处理 JSON Qt 学习之路 2(63):使用 QJson 处理 JSON  豆子  2013年9月9日  Qt ...

随机推荐

  1. SpringMVC Controller 介绍(详细深刻)

    一.简介 在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Mo ...

  2. 面试题:try,catch,finally都有return语句时执行哪个 已看1

    1.不管有木有出现异常,finally块中代码都会执行: return 先执行 把值临时存储起来, 执行完finally之后再取出来 值是不会改变的2.当try和catch中有return时,fina ...

  3. 38.NOW() 函数

    NOW 函数返回当前的日期和时间. 提示:如果您在使用 Sql Server 数据库,请使用 getdate() 函数来获得当前的日期时间. SQL NOW() 语法 SELECT NOW() FRO ...

  4. 解决selenium与firefox版本不兼容问题

    Python环境下类比 个人使用 32位环境 Python 2.7.12 Selenium 2.53.6 Firefox 47.01 安装selenium可用pip选择对应版本,参考另一教程. 因为在 ...

  5. .html与.text的异同

    .html与.text的方法操作是一样,只是在具体针对处理对象不同 .html处理的是元素内容,.text处理的是文本内容 .html只能使用在HTML文档中,.text 在XML 和 HTML 文档 ...

  6. 解析json的方法

    解析json的两种方法:JS中的eval().JSON.parse eval不仅解析内容还会解析其中的方法,JSON.parse解析更安全.JSONLint可校验json的错误.

  7. 基于IKAnalyzer搭建分词服务

    背景 前端高亮需要分词服务,nlp团队提供的分词服务需要跨域调用,而且后台数据索引使用的IK分词.综合评价,前端分词也需要基于IK分词器. IKAnalyzer服务已经停止更新,且对Lucene支持仅 ...

  8. SqueezeNet:AlexNet-level Accuracy with 50x fewer parameters and less than 0.5Mb model size

    - Fire modules consisting of a 'squeeze' layer with 1*1 filters feeding an 'expand' layer with 1*1 a ...

  9. POJ3295 Tautology(栈+枚举)

    Description WFF 'N PROOF is a logic game played with dice. Each die has six faces representing some ...

  10. [转]-webkit-overflow-scrolling:touch的应用

    -webkit-overflow-scrolling 用来控制元素在移动设备上是否使用滚动回弹效果. 在移动端上,在你用overflow-y:scorll属性的时候,你会发现滚动的效果很木,很慢,这时 ...