Qt Creator Colliding Mice碰撞老鼠例程解析【1.5W字数长文!详细!】

工程效果在这里插入图片描述

可以看到,小老鼠碰撞后耳朵会变红。具体完整代码可在示例里面找到。
在这里插入图片描述
在这里插入图片描述
工程总体就是多了一个mouse的源文件和头文件,即Mouse类相关文件。在Graphics View框架结构主要包含三个类:场景类(QGraphicsScene)、视图类(QGraphicsView)和图元类(QGraphicsItem),统称为“三要素”。而Mouse类就是继承自QGraphicsItem类的自定义类。

Scene类管理数量众多的Item类,并组织起他们的信息交流。但是它只是起后台管理的功能,并不会让这些Item类展现出现。View类起到将Item类可视化的功能,支持旋转和缩放。有点类似于浏览器的后端与前端,后端负责流程交互,前端负责展示效果。

mouse.h

其实QT本身就提供了Item类,包括矩形、椭圆形、线型等,但是我们一般使用的图案都是几种类型的结合,所以需要自定义类。以下为官方提供的Item类。
在这里插入图片描述
在这里插入图片描述

class Mouse : public QGraphicsItem
{
public:
    Mouse();

    QRectF boundingRect() const override;
    QPainterPath shape() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
               QWidget *widget) override;

protected:
    void advance(int step) override;

private:
    qreal angle = 0;
    qreal speed = 0;
    qreal mouseEyeDirection = 0;
    QColor color;
};

Mouse类是继承自QGraphicsItem类,所以我们在工程里面见到很多并未定义的函数,就是来自QGraphicsItem类。在Graphics View框架当中,QGraphicsItem是所有item的基础类。后三个函数是自定义Item类必须重新定义的纯虚函数。
在这里插入图片描述
paint函数是实际的绘制函数,包含老鼠的身体、眼镜等部分都是需要在paint函数里面绘制。

boundingRect 函数返回的是Item重新绘制的区域大小,相当于老鼠的平面面积。因为实际上动画的实现,就是擦除旧的,再画新的,所以好像移动了一样。这个擦除区域的大小就是由boundingRect() 来返回的。

Shape函数主要用于碰撞检测,它会返回老鼠的精确形状,默认情况下它返回的就是boundingRect的值,但有时候我们想要靠近5cm就认为碰撞,就需要重写该函数。

advance函数用来推进Scene,可以很容易实现Item的动画效果。调用Scene的advance函数就会自动调用Scene中所有Item的advance函数,而且Item的advane函数会被分为两个阶段调用两次。第一次phase为0,告诉所有的Items:Scene将要改变;第二次phase为1,在这时才进行具体的操作。
在这里插入图片描述
qreal是double数据类型,QColor是Qt的颜色变量,这几个定义的向量在后面会用到。

mouse.cpp

一、paint函数

先来看看它是怎么通过paint函数画出老鼠的。Graphics View框架调用paint函数绘制item的内容,而且是以本地坐标系为基准。

1.身体、眼睛、鼻子

在这里插入图片描述
setBrush函数是设置笔刷的函数,主要就是内部填充的颜色,这里的color变量是Mouse类的私有变量,主要是想实现随机颜色的老鼠。

drawEllipse函数是paninter类的画椭圆的函数,共有四个参数,前两个代表对应矩形的左上角的坐标,后两个代表宽度和高度。
在这里插入图片描述
在这里插入图片描述

Mouse类在构造函数中对color变量进行初始化。构造函数完成了两件事情,一个是通过随机数确定随机颜色,而是通过setRotation函数完成初始化随机角度的设定。
在这里插入图片描述
随机颜色的设定:color是Qt类Qcolor的实例化对象,三个参数代表RGB的值。bounded函数主要用于产生0-256的随机数。帮助手册说明如下:
在这里插入图片描述
随机角度的确定:setRotation函数是继承自QGraphicsItem类的一个函数,主要用于设定item出现在sence坐标系时的角度。默认情况下,item一般是以sence的中心(0,0)为自身局部坐标系的中心。通过setRotation函数可以改变老鼠开始移动时的方向。

接下来画白色的眼睛和黑色的鼻子,如果drawEllipse的后两个参数是相同的,画出来就是圆形。至于这个QRectF是强制转换函数,就是将里面四个参数先转为QRectF,然后再作为drawEllipse函数的参数。这个叫函数的重载。不管是直接四个参数,还是传入QRectF参数都是可以的,QT都有提供对应的函数。

painter->setBrush(Qt::black);
painter->drawEllipse(QRectF(-2, -22, 4, 4));

在这里插入图片描述

2.瞳孔

在这里插入图片描述
这里存在一个变量就是矩形左上角的x,这个mouseEyeDirection变量是在advance函数里面进行更新的,主要实现的效果就是,当老鼠往左,那瞳孔就左移一点;当老鼠往右,那瞳孔就右移一点。

3.耳朵

耳朵是用来表示碰撞的,正常情况下耳朵是灰黄色,当老鼠之间发生碰撞,耳朵会变成红色。这里使用了scene类的collidingItems函数进行判断是否发生碰撞。这个this代表的就是类本身,就是某一个对象(老鼠),函数会返回与该老鼠碰撞的其他item的清单。因为此工程只需要判断碰撞与否,而不需要判断与谁发生碰撞,所以用isEmpty函数判断是否为空就行了。
在这里插入图片描述
使用手册相关说明如下:
在这里插入图片描述
By default, all items whose shape intersects item or is contained inside item's shape are returned.可以得知,碰撞的检测是以shape交叉或者包含为准的,这就是shape函数实现的。

4.尾巴

在这里插入图片描述
drawPath函数用于绘制给定的路径曲线。
在这里插入图片描述
cubicTo函数是拿来画曲线的。使用c1和c2指定的控制点在当前位置和给定端点之间添加三次Bezier曲线。添加曲线后,将当前位置更新为曲线的端点。所以例程中存在6个参数,其实对应的就是c1、c2、endPoint三个点的坐标点。而实例化对象时用的(0,20)就是起点。

因为path.cubicTo(-5, 22, -5, 22, 0, 25);中的c1和c2是一样的,相当于曲线一定要从这一点经过。而path.cubicTo(5, 27, 5, 32, 0, 30);则表示曲线得从(5,27)和(5,32)这两个点中间经过,然后到达(0,30)这个点。
在这里插入图片描述
至此,老鼠已经画完了。

二、advance函数

当Scene准备刷新场景的时候,就会调用每一个Item类的advance函数,完成场景的刷新。通过advance函数我们可以完成老鼠的移动。

  void Mouse::advance(int step)
  {
      if (!step)
          return;
      QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0));
      if (lineToCenter.length() > 150) {
          qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx());
          angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2);

          if (angleToCenter < Pi && angleToCenter > Pi / 4) {
              // Rotate left
              angle += (angle < -Pi / 2) ? 0.25 : -0.25;
          } else if (angleToCenter >= Pi && angleToCenter < (Pi + Pi / 2 + Pi / 4)) {
              // Rotate right
              angle += (angle < Pi / 2) ? 0.25 : -0.25;
          }
      } else if (::sin(angle) < 0) {
          angle += 0.25;
      } else if (::sin(angle) > 0) {
          angle -= 0.25;
      }

首先,如果step为0,那老鼠不做任何的前进,直接return。因为scene会调用两次advance函数,第一次调用step为0,表明item即将更新;第二次调用step等于1,这次是实际的更新。所以我们要保证step等于0时,item不发生更改。

GraphicsView坐标系统

在这里插入图片描述
Item坐标系是自身的局部坐标系,Scene坐标系是全局坐标系。之前我们通过paint画图就是在Item类坐标系上画的,但老鼠的移动时是需要在Scene类坐标系中移动的,所以存在一个坐标映射的问题,也可以称之为坐标转化。比如图中Item类坐标系的原点(0,0)转换为Scene类坐标系就是大概为(15,-10)这样。

QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0));

QLineF是表示一条线,两个参数,一个起点和一个终点。mapFromScene函数是继承自QGraphicsItem类的函数,主要是将Scene的坐标转换为Item类的坐标。这句代码其实就是将Scene类坐标(0,0)转换为Item坐标系中的点。这条线就是如下图所示的红色直线。
在这里插入图片描述
如果这条直线的长度length大于150,就进行方向变化,这是为了保证老鼠待在一个150像素的圆内。如果超过150,就掉头。通过QLineF类的dx函数和dy函数,结合atan2函数可以算出这条直线的角度,公式为float angle = atan2( y2-y1, x2-x1 ),即终点减去起点。atan2函数与atan函数的取值范围不同,是-π到π。
在这里插入图片描述

          qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx());
          angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2);

例程中通过atan2函数计算得出方位角后,还经过了normalizeAngle函数去调整角度,而这个normalizeAngle函数是例程自定义的角度处理函数。函数功能不难理解,就是将角度范围限定在0-2π之间。

static qreal normalizeAngle(qreal angle)
{
    while (angle < 0)
        angle += TwoPi;
    while (angle > TwoPi)
        angle -= TwoPi;
    return angle;
}

为什么通过atan2角度之后,还需要经过(Pi - angleToCenter) + Pi / 2计算呢?这是我思考很久才弄明白的,因为说明手册也没说。

以下图为例,角度不代表真实角度,就是为了表示他们之间的换算关系。老鼠的头部是朝上的,我们习惯以老鼠头部为基准,然后去判断方向,如下图我们可以认为Scene坐标系原点在老鼠头部的左前方。

但是tan2函数算出来的角度是以Item坐标系为准的,比如下图的-120°就是通过atan2函数计算得出,这代表直线角度从x正方向逆时针转动了120°。通过(Pi - angleToCenter) + Pi / 2计算,得到的角度是390°,以这个参数代入normalizeAngle函数,得到30°,这个就是我们想要的角度。

所以我们可以推断出这个angleToCenter角度是以y负方向为基准,然后逆时针旋转的角度。
在这里插入图片描述
再看一个例子:
在这里插入图片描述
当然在程序中得到的数值和计算的数值都是以弧度制表示的,上面为了讲解用了角度值。原理是一样的。

在这里插入图片描述
如果angleToCenter处于左下角区域,老鼠可能往左调头返回,也可能往右调头返回,这个由angle决定。angleToCenter表示的是Scenne坐标系原点和Item坐标系原点的角度,即确定老鼠中心所处的位置,那angle又是表示什么?

angle能推测出是老鼠前进的方向,通过说明手册能得知,这个角度是Item沿Z轴旋转的方向,以顺时针为正,默认情况下为0.也就是两个坐标系x、y轴方向一致的时候是0.
在这里插入图片描述
下面这张图,此时老鼠的angle就是0度,没有发生旋转。
在这里插入图片描述

根据angleToCenter值不同,angle增减值也不一样。弧度制0.25大约相当于角度制是14度。

        if (angleToCenter < Pi && angleToCenter > Pi / 4) {
            // Rotate left
            angle += (angle < -Pi / 2) ? 0.25 : -0.25;

下图以箭头方向表示老鼠方向,判断标准是-Pi/2,即原始老鼠沿逆时针旋转90°。通过下图可以看到,这个判断就是让老鼠往-Pi/2方向偏转,调头往Scene坐标系中心移动,不至于跑出圈外。
在这里插入图片描述
下面几个判断的原理差不多,都是让不同位置的老鼠掉头往中心移动。
在这里插入图片描述

避免碰撞

在这里插入图片描述
上面这一段代码是为了尝试不让老鼠撞在一起。QList是QT的链表数据结构,类似于数组。这个列表里面的数据类型是Item类指针,就是那一只只小老鼠。scene函数继承于Item类,返回这个Item目前所处的scene。items函数是QGraphicsScene类的函数,返回该scene中所有的Item的顺序列表。

mapToScene函数是把Item坐标系的点映射到Scene坐标系,是mapFromScene函数的反操作。

QPoltgonF函数是生成多边形的函数,以三个点生成多边形,就是三角形。(0,0)是老鼠的中心位置,(-30,-50)和(30,-50)是老鼠的两个耳朵左上角和右上角边缘。这三个点生成的三角形,相当于半个老鼠大小的三角形。
在这里插入图片描述
计算得到angleToMouse和angleToCenter是一样的,都是以y负半轴为0°,逆时针旋转。angle是以z轴顺时针旋转的角度,加上0.5就是顺时针旋转大约28°,减去0.5就是逆时针旋转大约28°。
在这里插入图片描述
如下图所示,蓝色老鼠就位于橙色老鼠0-Pi/2的区域内,根据判断条件,增加angle增加0.5,向右旋转28°,避开蓝色老鼠。
在这里插入图片描述

增加随机运动

在这里插入图片描述
dangerMice就是那个装着全部老鼠的队列,如果队列里面的老鼠数量size大于1,且产生的随机数等于
0。因为bounded产生的随机数位于0-9之间,所以等于0的概率是十分之一。后面又增加或者减少一个随机的角度。

小鼠移动

角度angle由三个方式确定,一是与中心点的距离和位置,二是与其他老鼠的位置关系,三是随机运动产生的角度。speed也是一个由bounded产生的随机数,结合整条式子,产生的speed值为-0.05至+0.49。dx范围为-10至10之间。
在这里插入图片描述
在这里插入图片描述
rotation函数表示的是当前item绕Z轴顺时针旋转的角度,默认值是0(如下图所示)。
在这里插入图片描述
setRotation设置的就是Item的旋转角度,范围是-360°至360°。setRotation(rotation() + dx);就是在原来的基础上,旋转dx度。但是例程通过qreal dx = ::sin(angle) * 10;把弧度制的angle转换成了角度值dx,这个转换关系我也没搞懂,可能就是大致范围的转换吧。因为正常来说弧度制转角度值,是/Pi*180。
在这里插入图片描述
setRotation决定了老鼠的方向,而setPos就是让老鼠运动起来。根据说明手册,设定item在父坐标系中的位置,如果它没有父类,那就以scene坐标系为准。这个例程的老鼠就是单独的item,它没有父类。 item的位置描述,指的是item坐标系的原点在父坐标系的位置。
在这里插入图片描述
通过mapToParent函数把坐标从Item映射到Scene,下面式子就是把Item原点移动到(0,-y)的地方,这个y是个随机数。因为老鼠头部方向为y轴负方向,所以相当于老鼠前进了距离y。

setPos(mapToParent(0, -(3 + sin(speed) * 3)));

boundingRect函数

boundingRect函数以一个矩形定义了item的外部界限。Graphice View框架使用这个矩形来决定item是否需要重新绘画,所以所有的绘制都需要在矩形框中进行。如果绘制超过了这个矩形框,Graphice View框架就不会将超出的那部分擦除。
在这里插入图片描述
通过下图就能看到上面几个数据是如何得到的,基本就是返回一个比item还要大一点的矩形。
之前提到老鼠主体部分椭圆的左上部分是

Shape函数

Graphice View框架使用shape函数返回了形状来确定两个item是否相撞,所以shape函数返回的形状会更加精确点。代码所示,这个矩形相当于就是老鼠的椭圆大小,不考虑尾巴多出来那部分。
在这里插入图片描述

Main函数

首先创建了一个scene,之前讲到的scene坐标系就是这里创建的。scene左上角在(-300,-300),长和宽都是600像素。

      QGraphicsScene scene;
      scene.setSceneRect(-300, -300, 600, 600);

QGraphicsScene类是作为QGraphicsItems类的容器,它可以高效地决定每个item的位置和哪个item是可见的。

如下所示,Scene默认情况是使用索引算法,这种算法可以加速搜索item,比较适用于静态场景。但是如果场景有很多动画,我们可以设置为NoIndex,这样会比较快点。

scene.setItemIndexMethod(QGraphicsScene::NoIndex);

在这里插入图片描述
通过addItem给scene添加老鼠。

      for (int i = 0; i < MouseCount; ++i) {
          Mouse *mouse = new Mouse;
          mouse->setPos(::sin((i * 6.28) / MouseCount) * 200,
                        ::cos((i * 6.28) / MouseCount) * 200);
          scene.addItem(mouse);
      }

scene是为了管理item,但是不能让item可视化。可视化需要新建QGraphicsView类。
setRenderHin函数:设置了反走样\反锯齿(Antialiasing),主要是让线条更柔顺,不会边缘出现锯齿。
setBackgroundBrush函数:设置了view的背景图----奶酪图。
setCacheMode函数:设置缓存模式,加速渲染背景图。
setViewportUpdateMode函数:ViewportUpdateMode属性描述了View类在场景发生变化时是如何更新它的视图的,存在五种模式。
QGraphicsView::FullViewportUpdate:更新整个视图;
QGraphicsView::MinimalViewportUpdate:这是默认模式,寻求最小化区域进行更新;
QGraphicsView::SmartViewportUpdate:分析重绘区域,然后试图寻找最佳的模式;
QGraphicsView::BoundingRectViewportUpdate:(该例程选择模式),只更改界限以内的内容。这个模式缺点是就算其他item未发生改变,它的界限内也会发生重绘。
QGraphicsView::NoViewportUpdate:不更新试图,这种用户想要自己控制更新时选择的模式;

setDragMode函数:这个DragMode属性定义了当用户点击scene背景并且拖拽鼠标时会发生什么,存在三种模式。
QGraphicsView::NoDrag :忽略鼠标事件,不可以拖动。
QGraphicsView::ScrollHandDrag :光标变为手型,可以拖动场景进行移动。(本例程采用)
QGraphicsView::RubberBandDrag :进行区域选择,可以选中一个区域内的所有图形项。

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));
    view.setCacheMode(QGraphicsView::CacheBackground);
    view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
    view.setDragMode(QGraphicsView::ScrollHandDrag);

**setWindowTitle函数:设置窗口标题;

    view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice"));
    view.resize(400, 300);
    view.show();

创建一个定时器,连接定时器的timeout信号,到scene的advance槽函数。每一次定时器到了时间,就激活scene刷新场景。定时器的实际是1000/33毫秒。所以相当于动画一秒钟会刷新30帧。对于大多数动画来说,这个帧速率足够了。

    QTimer timer;
    QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance);
    timer.start(1000 / 33);
  • 8
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值