Bootstrap

Qt实现台球游戏 QGraphicsView QGraphicsScene QGraphicsItem 入门实战练习

 最终效果

github下载地址

Billiards

前话

  最近在学习Qt绘图,看了很多文章,Qt的图形视图框架,最核心的三个类为:QGraphicsScene、QGraphicsItem与QGraphicsView。 关于QGraphicsScene、QGraphicsItem与QGraphicsView的详细介绍可参考Qt图形视图框架详解

        个人理解:QGraphicsScene提供一个场景, 上面放置QGraphicsItem图元, 而QGraphicsView则是选择场景中的一块区域进行展示。

        平时也经常玩游戏, 就想着用这个绘图做一款游戏练习, 复杂的又没能力做, 思来想去, 最终选择了台球斯诺克,至于为什么不选择中式黑8, 因为它的球不是纯色的, 需要做滚动效果, 难以实现。

项目分析

图元部分

        需要自定义图元分别绘制:球台、球、球杆。

计算部分

        计算主要为:击球、球与球的碰撞、球与球台的碰撞、球自身的运动。

主要流程

        进入游戏—>鼠标左键在半圆形区域放置白球—>再次点击鼠标左键进入瞄准状态—>释放鼠标进入出杆状态—>随后球杆击中白球进入台球移动状态(进行相应碰撞检测计算处理及进球判断)—>待球全部静止能再次进行瞄准操作。

主要代码      

ItemBase

        此项目所有图元继承此类, 由于此项目的图元效果难以用qt提供的基本图元实现,需要自定义图元, 继承QGraphicsItem类, 重写boundingRect、paint函数即可。

itembase.h

#ifndef ITEMBASE_H
#define ITEMBASE_H

#include <QGraphicsItem>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QStyleOption>
#include <QList>
#include <QtMath>

class ItemBase :public QGraphicsItem
{
public:
    enum itemType
    {
        ball = 1,    //球
        cushion = 2, //桌子
        club = 3     //球杆
    };

    ItemBase();
    itemType itemtype();

public:

    virtual QRectF boundingRect() const override = 0;
    virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override = 0;

protected:
    itemType m_type;
};

#endif // ITEMBASE_H

itembase.cpp

#include "itembase.h"

ItemBase::ItemBase()
{

}

ItemBase::itemType ItemBase::itemtype()
{
    return m_type;
}

Cushion

        球台图元, 根据斯诺克球台进行缩放的, 所以看到有些奇怪的数字, 多数是斯诺克标准尺寸, 大量出现的2.6是因为项目中的台球尺寸给的是20, 实际尺寸54, 存在2.6倍关系, 导致桌面等尺寸需要除2.6。

cushion.h

#ifndef CUSHION_H
#define CUSHION_H

#include <QDebug>
#include "itembase.h"
#include "ball.h"

class Cushion: public ItemBase
{
public:
    Cushion();
    ~Cushion();

    //与球的碰撞处理(包括进球)返回true表示进球
    bool collisionWithBall(Ball *ball, float fps);
public:
    QRectF boundingRect() const override;
    QPainterPath shape() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
public:
    QPointF m_outRect = QPointF(3820, 2035);          //球桌外延尺寸
    QPointF m_inRect = QPoint(3569, 1778);            //球桌内沿尺寸
    float m_pocket_r = 45;                            //袋口半径
private:
    QPainterPath m_ellipsePath;                       //库边圆角
    QPainterPath m_cushionPath;                       //库边
    QPainterPath m_pointPath;                         //辅助点、线
    QPainterPath m_pocketPath;                        //袋口

    QVector<QPointF> m_pocketPoints;                  //袋口原点, 做碰撞检测时使用
    QVector<QPointF> m_filletedCornerPoints;          //库边圆角, 做碰撞检测时使用
};

#endif // CUSHION_H

cushion.cpp

#include "cushion.h"

Cushion::Cushion()
{
    m_type = cushion;
    m_ellipsePath.addEllipse((-m_inRect.x())/2.6/2 + 8, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((-m_pocket_r*2*3.0/2)/2.6, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((m_pocket_r*2/2)/2.6, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((m_inRect.x() - m_pocket_r*2*2)/2.6/2 - 8, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((-m_inRect.x() - m_pocket_r*2*2)/2.6/2, (-m_inRect.y())/2.6/2 + 8, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((m_inRect.x())/2.6/2, (-m_inRect.y())/2.6/2 + 8, m_pocket_r*2/2.6, m_pocket_r*2/2.6);

    m_ellipsePath.addEllipse((-m_inRect.x())/2.6/2 + 8, (m_inRect.y())/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((-m_pocket_r*2*3.0/2)/2.6, (m_inRect.y())/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((m_pocket_r*2/2)/2.6, (m_inRect.y())/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((m_inRect.x() - m_pocket_r*2*2)/2.6/2 - 8, (m_inRect.y())/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((-m_inRect.x() - m_pocket_r*2*2)/2.6/2, (m_inRect.y() - m_pocket_r*2*2)/2.6/2 - 8, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_ellipsePath.addEllipse((m_inRect.x())/2.6/2, (m_inRect.y() - m_pocket_r*2*2)/2.6/2 - 8, m_pocket_r*2/2.6, m_pocket_r*2/2.6);

    m_cushionPath.addRect((-m_inRect.x()+m_pocket_r*2)/2.6/2 + 8, (-m_inRect.y()-m_pocket_r*2)/2.6/2, (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8, m_pocket_r*2/2/2.6);
    m_cushionPath.addRect(m_pocket_r*2/2.6, (-m_inRect.y()-m_pocket_r*2)/2.6/2, (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8, m_pocket_r*2/2/2.6);
    m_cushionPath.addRect((-m_inRect.x()+m_pocket_r*2)/2.6/2 + 8, (m_inRect.y())/2.6/2, (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8, m_pocket_r*2/2/2.6);
    m_cushionPath.addRect(m_pocket_r*2/2.6, (m_inRect.y())/2.6/2, (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8, m_pocket_r*2/2/2.6);
    m_cushionPath.addRect((-m_inRect.x()-m_pocket_r*2)/2.6/2, (-m_inRect.y()+m_pocket_r*2)/2.6/2 + 8, m_pocket_r*2/2/2.6, (m_inRect.y() - m_pocket_r*2)/2.6 - 16);
    m_cushionPath.addRect((m_inRect.x())/2.6/2, (-m_inRect.y()+m_pocket_r*2)/2.6/2 + 8, m_pocket_r*2/2/2.6, (m_inRect.y() - m_pocket_r*2)/2.6 - 16);

    m_pointPath.addEllipse(-m_inRect.x()/2.6/2 + 324/2.6 - 2, 0 - 2, 4, 4);
    m_pointPath.addEllipse(-m_inRect.x()/2.6/2/2 - 2, 0 - 2, 4, 4);
    m_pointPath.addEllipse(m_inRect.x()/2/2.6 - m_inRect.x()*737/3569/2.6 - 2, -m_inRect.x()*292/3569/2.6 - 2, 4, 4);
    m_pointPath.addEllipse(m_inRect.x()/2/2.6 - m_inRect.x()*737/3569/2.6 - 2, 0 - 2, 4, 4);
    m_pointPath.addEllipse(m_inRect.x()/2/2.6 - m_inRect.x()*737/3569/2.6 - 2, m_inRect.x()*292/3569/2.6 - 2, 4, 4);

    m_pocketPath.addEllipse(-m_inRect.x()/2.6/2 - m_pocket_r*2/2.6 + 10, -m_inRect.y()/2.6/2 - m_pocket_r*2/2.6 + 10, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_pocketPath.addEllipse(-m_pocket_r*2/2/2.6, -m_inRect.y()/2.6/2 - m_pocket_r*2/2.6, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_pocketPath.addEllipse(m_inRect.x()/2.6/2 - 10, -m_inRect.y()/2.6/2 - m_pocket_r*2/2.6 + 10, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_pocketPath.addEllipse(-m_inRect.x()/2.6/2 - m_pocket_r*2/2.6 + 10, m_inRect.y()/2.6/2 - 10, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_pocketPath.addEllipse(-m_pocket_r*2/2/2.6, m_inRect.y()/2.6/2, m_pocket_r*2/2.6, m_pocket_r*2/2.6);
    m_pocketPath.addEllipse(m_inRect.x()/2.6/2 - 10, m_inRect.y()/2.6/2 - 10, m_pocket_r*2/2.6, m_pocket_r*2/2.6);

    m_pocketPoints<< QPointF(-m_inRect.x()/2.6/2 - m_pocket_r*2/2.6 + 10, -m_inRect.y()/2.6/2 - m_pocket_r*2/2.6 + 10) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                  << QPointF(-m_pocket_r*2/2/2.6, -m_inRect.y()/2.6/2 - m_pocket_r*2/2.6) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                  << QPointF(m_inRect.x()/2.6/2 - 10, -m_inRect.y()/2.6/2 - m_pocket_r*2/2.6 + 10) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                  << QPointF(-m_inRect.x()/2.6/2 - m_pocket_r*2/2.6 + 10, m_inRect.y()/2.6/2 - 10) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                  << QPointF(-m_pocket_r*2/2/2.6, m_inRect.y()/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                  << QPointF(m_inRect.x()/2.6/2 - 10, m_inRect.y()/2.6/2 - 10) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2;

    m_filletedCornerPoints<< QPointF((-m_inRect.x())/2.6/2 + 8, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((-m_pocket_r*2*3.0/2)/2.6, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((m_pocket_r*2/2)/2.6, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((m_inRect.x() - m_pocket_r*2*2)/2.6/2 - 8, (-m_inRect.y()-2*m_pocket_r*2)/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((-m_inRect.x() - m_pocket_r*2*2)/2.6/2, (-m_inRect.y())/2.6/2 + 8) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((m_inRect.x())/2.6/2, (-m_inRect.y())/2.6/2 + 8) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2

                          << QPointF((-m_inRect.x())/2.6/2 + 8, (m_inRect.y())/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((-m_pocket_r*2*3.0/2)/2.6, (m_inRect.y())/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((m_pocket_r*2/2)/2.6, (m_inRect.y())/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((m_inRect.x() - m_pocket_r*2*2)/2.6/2 - 8, (m_inRect.y())/2.6/2) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((-m_inRect.x() - m_pocket_r*2*2)/2.6/2, (m_inRect.y() - m_pocket_r*2*2)/2.6/2 - 8) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2
                          << QPointF((m_inRect.x())/2.6/2, (m_inRect.y() - m_pocket_r*2*2)/2.6/2 - 8) + QPointF(m_pocket_r*2/2.6, m_pocket_r*2/2.6)/2;
}

Cushion::~Cushion()
{

}

bool Cushion::collisionWithBall(Ball *ball, float fps)
{
    if(ball == nullptr)
    {
        return false;
    }
    QPointF ballPos = mapFromItem(ball, 0, 0);

    //先与6个球袋进行判断
    for(auto point:m_pocketPoints)
    {
        //根据两球心距离与半径判断进球,若进球直接返回true, +1是在微调进球范围,与下面的+2效果相同
        if(QVector2D(ballPos - point).length() <  m_pocket_r*2/2.6/2 - ball->r() + 1)
        {
            return true;
        }
        //若还未进球但球心已经进入球袋, 则改变速度及方向,使朝着球袋中心移动
        else if(QVector2D(ballPos - point).length() <  m_pocket_r*2/2.6/2 + 2)
        {
            ball->setMoveDir(QVector2D(point - ballPos).normalized());
            ball->setSpeed(ball->speed() + 20);
        }
    }

    //若速度为0,则无需考虑碰撞
    if(ball->speed()  < 1e-6)
    {
        return false;
    }

    //与库边圆角做碰撞检测(将库边圆角当作圆形处理, 依旧根据球心距离与半径判断)
    for(auto point:m_filletedCornerPoints)
    {
        if(QVector2D(ballPos - point).length() <=  m_pocket_r*2/2.6/2 + ball->r() + 1e-6)
        {
            //计算出球沿着库边圆角中心的速度分量, 这个分量(矢量)便是球的损失速度

            //球心到库边圆角中心的方向
            QVector2D pos_dif = QVector2D(point - ballPos);

            QVector2D temp1 = (ball->moveDir()*pos_dif);
            //向量积
            float vectorValue = (temp1.x() + temp1.y());

            //(向量积/模之积)极为余弦值
            float pos_difCosBallDir = vectorValue/(pos_dif.length()*ball->moveDir().length());

            //pos_dif方向的速度损失量
            float lossSpeed = pos_difCosBallDir*ball->speed();

            //小于0即代表夹角大于90度, 不构成碰撞
            if(pos_difCosBallDir > 0)
            {
                //速度减去pos_dif方向的损失量,0.8为碰撞损耗
                ball->addSpeedVector(-0.8*lossSpeed*pos_dif.normalized());
                return false;
            }
        }
    }

    //分别与除库边圆角外的直边进行碰撞检测0.8为碰撞损耗(与水平边碰撞:x方向不变, y反向, 与竖直边碰撞:x反向, y不变)
    if(ballPos.x() < -m_inRect.x()/2.6/2 + ball->r()
            && ballPos.y() >= (-m_inRect.y()+m_pocket_r*2)/2.6/2 + 8
            && ballPos.y() <= (-m_inRect.y()+m_pocket_r*2)/2.6/2 + 8 + (m_inRect.y() - m_pocket_r*2)/2.6 - 16)
    {
        ball->setPos(-m_inRect.x()/2.6/2 + ball->r(),ball->scenePos().y());
        ball->setMoveDir(QVector2D(-ball->moveDir().x(), ball->moveDir().y()));
        ball->setSpeed(ball->speed()*0.8);
        return false;
    }
    else if(ballPos.x() > m_inRect.x()/2.6/2 - ball->r()
            && ballPos.y() >= (-m_inRect.y()+m_pocket_r*2)/2.6/2 + 8
            && ballPos.y() <= (-m_inRect.y()+m_pocket_r*2)/2.6/2 + 8 + (m_inRect.y() - m_pocket_r*2)/2.6 - 16)
    {
        ball->setPos(m_inRect.x()/2.6/2 - ball->r(),ball->scenePos().y());
        ball->setMoveDir(QVector2D(-ball->moveDir().x(), ball->moveDir().y()));
        ball->setSpeed(ball->speed()*0.6);
        return false;
    }
    else if(ballPos.y() < -m_inRect.y()/2.6/2 + ball->r()
            && ((ballPos.x() >= (-m_inRect.x()+m_pocket_r*2)/2.6/2 + 8
                 && ballPos.x() <= (-m_inRect.x()+m_pocket_r*2)/2.6/2 + 8 + (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8)
                || (ballPos.x() >= m_pocket_r*2/2.6
                    && ballPos.x() <= m_pocket_r*2/2.6 + (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8)))
    {
        ball->setPos(ball->scenePos().x(), -m_inRect.y()/2.6/2 + ball->r());
        ball->setMoveDir(QVector2D(ball->moveDir().x(), -ball->moveDir().y()));
        ball->setSpeed(ball->speed()*0.8);
        return false;
    }
    else if(ballPos.y() > m_inRect.y()/2.6/2 - ball->r()
            && ((ballPos.x() >= (-m_inRect.x()+m_pocket_r*2)/2.6/2 + 8
                 && ballPos.x() <= (-m_inRect.x()+m_pocket_r*2)/2.6/2 + 8 + (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8)
                || (ballPos.x() >= m_pocket_r*2/2.6
                    && ballPos.x() <= m_pocket_r*2/2.6 + (m_inRect.x() - m_pocket_r*2*3)/2.6/2 - 8)))
    {
        ball->setPos(ball->scenePos().x(), m_inRect.y()/2.6/2 - ball->r());
        ball->setMoveDir(QVector2D(ball->moveDir().x(), -ball->moveDir().y()));
        ball->setSpeed(ball->speed()*0.8);
        return false;
    }
    return false;
}

QRectF Cushion::boundingRect() const
{
    return QRectF(-m_outRect.x()/2.6/2, -m_outRect.y()/2.6/2, m_outRect.x()/2.6, m_outRect.y()/2.6);
}

QPainterPath Cushion::shape() const
{
    QPainterPath path;
    path.addRect(-m_outRect.x()/2.6/2, -m_outRect.y()/2.6/2, m_outRect.x()/2.6, m_outRect.y()/2.6);
    return path;
}


void Cushion::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    painter->setRenderHint(QPainter::Antialiasing,true);
    painter->setPen(Qt::NoPen);
    painter->setBrush(QColor(Qt::green).light(60));
    //绘制中间绿色运动区域
    painter->drawRect((-m_inRect.x()-m_pocket_r*2)/2.6/2, (-m_inRect.y()-m_pocket_r*2)/2.6/2, (m_inRect.x()+m_pocket_r*2)/2.6, (m_inRect.y()+m_pocket_r*2)/2.6);
    painter->setBrush(QColor(Qt::white).light(70));
    //绘制中间区域的点与线
    painter->drawPath(m_pointPath);

    painter->setPen(Qt::black);
    painter->setBrush(QColor(Qt::green).light(50));
    //绘制库边矩形与圆角
    painter->drawPath(m_cushionPath + m_ellipsePath);
    painter->setBrush(QColor(150, 75, 0).light(70));

    QPainterPath outFramePath;
    outFramePath.addRoundRect(-m_outRect.x()/2.6/2, -m_outRect.y()/2.6/2, m_outRect.x()/2.6, m_outRect.y()/2.6, 3, 5);
    QPainterPath inFramePath;
    inFramePath.addRect((-m_inRect.x()-m_pocket_r*2)/2.6/2, (-m_inRect.y()-m_pocket_r*2)/2.6/2, (m_inRect.x()+m_pocket_r*2)/2.6, (m_inRect.y()+m_pocket_r*2)/2.6);
    //    painter->drawRoundRect(-m_outRect.x()/2.6/2, -m_outRect.y()/2.6/2, m_outRect.x()/2.6, m_outRect.y()/2.6, 3, 5);
    QLinearGradient linear(QPointF(-m_outRect.x()/2.6/2, -m_outRect.y()/2.6/2)*0.2,
                           QPointF(m_outRect.x()/2.6/2, m_outRect.y()/2.6/2)*0.2);
    linear.setColorAt(0, QColor(150, 75, 0).light(70));
    linear.setColorAt(0.3, QColor(150, 75, 0).light(120));
    linear.setColorAt(1,QColor(150, 75, 0).light(70));
//    linear.setSpread(QGradient::ReflectSpread);
    painter->setBrush(linear);
    //绘制外边框(一个大的圆角矩形减去中间一个矩形)
    painter->drawPath(outFramePath - inFramePath);
    painter->setPen(QColor(Qt::white).light(70));
    painter->drawLine(QPointF(m_inRect.x()/2/2.6 - m_inRect.x()*737/3569/2.6, -m_inRect.y()/2.6/2), QPointF(m_inRect.x()/2/2.6 - m_inRect.x()*737/3569/2.6, m_inRect.y()/2.6/2));
    painter->drawArc(m_inRect.x()/2/2.6 - m_inRect.x()*737/3569/2.6 - m_inRect.x()*292/3569/2.6 , -m_inRect.x()*292/3569/2.6, m_inRect.x()*292/3569/2.6*2, m_inRect.x()*292/3569/2.6*2, -m_pocket_r*2*16, 180*16);

    painter->setPen(Qt::NoPen);
    painter->setBrush(QColor(Qt::black));
    //最后绘制球袋
    painter->drawPath(m_pocketPath);
}

Ball

        球类图元, 需要表现出光照、阴影等。

ball.h

#ifndef BALL_H
#define BALL_H

#include <QVector2D>
#include "itembase.h"

class Ball: public ItemBase
{
public:
    //将球按颜色分类
    enum BallType
    {
        red,
        green,
        coffee,
        yellow,
        blue,
        pink,
        black,
        white
    };
public:
    Ball(BallType ballType = red);

    //设置球的类型
    void setBallType(BallType ballType, bool isChangeColor = true);
    BallType ballType();

    //设置颜色
    void setColor(QColor color);

    //设置移动速度(标量)
    void setSpeed(const float &speed);
    float speed();

    //增加速度(矢量, 因碰撞会改变移动方向)
    void addSpeedVector(const QVector2D &speed);

    //设置移动方向
    void setMoveDir(const QVector2D &dir);
    QVector2D moveDir();

    //设置球体半径
    void setR(const float &r);
    float r();

    //每帧根据帧率做减速直线运动计算
    void move(float fps);

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

private:
    float m_r = 10;                         //球体半径
    float m_speed = 0;                      //移动速度
    float m_lossSpeed = 0;                  //每帧速度减少量
    QColor m_color = QColor(Qt::red);       //球体颜色
    QVector2D m_moveDir = QVector2D(0, 1);  //移动方向
    BallType m_ballType = red;              //所属类型
};

#endif // BALL_H

ball.cpp

#include "ball.h"

#include <QDebug>

Ball::Ball(BallType ballType)
{
    setBallType(ballType);
    m_type = ball;
}

void Ball::setBallType(Ball::BallType ballType, bool isChangeColor)
{
    m_ballType = ballType;
    if(isChangeColor)
    {
        switch (m_ballType) {
        case green:
            m_color = Qt::green;
            break;
        case coffee:
            m_color = QColor(150, 75, 0);
            break;
        case yellow:
            m_color = QColor(Qt::yellow).light(80);
            break;
        case blue:
            m_color = Qt::blue;
            break;
        case pink:
            m_color = QColor(255, 105, 180);
            break;
        case black:
            m_color = Qt::black;
            break;
        case white:
            m_color = QColor(Qt::white).light(80);
            break;
        default:
            m_color = Qt::red;
            break;
        }
    }
}

Ball::BallType Ball::ballType()
{
    return m_ballType;
}

void Ball::setColor(QColor color)
{
    m_color = color;
}

void Ball::setSpeed(const float &speed)
{
    m_speed = speed;
}

void Ball::addSpeedVector(const QVector2D &speed)
{
    QVector2D temp = m_moveDir*m_speed + speed;
    m_speed = temp.length();
    m_moveDir = temp.normalized();
}

void Ball::setMoveDir(const QVector2D &dir)
{
    m_moveDir = dir.normalized();
}

void Ball::setR(const float &r)
{
    m_r = r;
}

float Ball::r()
{
    return m_r;
}

QVector2D Ball::moveDir()
{
    return m_moveDir;
}

float Ball::speed()
{
    return m_speed;
}

QRectF Ball::boundingRect() const
{
    qreal adjust = 2;
    return QRectF( -m_r - adjust, -m_r - adjust, 2*m_r + adjust, 2*m_r + adjust);
}

QPainterPath Ball::shape() const
{
    QPainterPath path;
    path.addEllipse(-m_r, -m_r, 2*m_r, 2*m_r);
    return path;
}

void Ball::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option)
    Q_UNUSED(widget)

    //抗锯齿
    painter->setRenderHint(QPainter::Antialiasing,true);

    //不绘制轮廓线
    painter->setPen(Qt::NoPen);


    QColor color(Qt::black);
    color.setAlpha(90);
    painter->setBrush(color);
    //先绘制阴影部分
    painter->drawEllipse(-7, -7, 2*m_r, 2*m_r);

    //此操作可以使绘制效果为圆形渐变(表现出光照效果)
    QRadialGradient gradient(-3.0/10*m_r, -3.0/10*m_r, m_r/2);
    gradient.setColorAt(0, QColor(Qt::white).light(200));
    gradient.setColorAt(1, m_color);
    painter->setBrush(gradient);

    //绘制球体
    painter->drawEllipse(-m_r, -m_r, 2*m_r, 2*m_r);
}

void Ball::move(float fps)
{
    //fps有时会计算错误则跳过
    if(isnan(fps))
    {
        return;
    }
    if(m_speed < 1e-6)
    {
        m_speed = 0;
    }

    //速度>0, 则移动
    if(m_speed > 0)
    {
        //所谓移动,就是根据:速度、方向、帧率不断设置球的位置
        setPos(pos() + m_speed/fps*m_moveDir.normalized().toPointF());

        //随意写的一个减速规则
        m_lossSpeed = (m_speed > 200)?0.4*m_speed:40;
        m_speed -= m_lossSpeed/fps;
    }
}

GCanvasView

        由于需要用到鼠标等事件且考虑代码复用性, 实现GraphicsView类, 继承QGraphicsView;将事件分离出去用BaseOperator类实现;

grahpicsview.h

#ifndef GRAPHICSVIEW_H
#define GRAPHICSVIEW_H


#include <QObject>
#include <QGraphicsView>
#include <QTimer>


class BaseOperator;
class GraphicsView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit GraphicsView(QWidget *parent = nullptr);
    ~GraphicsView();

    void createScene();

    void setOperatorObj(QSharedPointer<BaseOperator> op);
    QSharedPointer<BaseOperator> operatorObj();

protected:
    void initialize();

    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void mouseDoubleClickEvent(QMouseEvent *event) override;
    void wheelEvent(QWheelEvent *event) override;

    void keyPressEvent(QKeyEvent *event) override;
    void keyReleaseEvent(QKeyEvent *event) override;

    void resizeEvent(QResizeEvent *event) override;
    void timerEvent(QTimerEvent *event) override;

    bool event(QEvent *event) override;
private:
    QSharedPointer<BaseOperator> m_pOperator;
    bool m_isPressed                    = false;
    bool m_isDoubleClick                = false;
};

#endif // GRAPHICSVIEW_H

grahpicsview.cpp


#include "graphicsview.h"
#include <QMouseEvent>
#include <QDebug>
#include <QVector2D>

#include "baseoperator.h"
#include "defualtoperator.h"

GraphicsView::GraphicsView(QWidget *parent)
    : QGraphicsView{parent}
{
    initialize();
}

GraphicsView::~GraphicsView()
{

}

void GraphicsView::createScene()
{
    if(this->scene() != nullptr){
        return;
    }
    auto pScene = new QGraphicsScene(this);
    this->setScene(pScene);
}

void GraphicsView::setOperatorObj(QSharedPointer<BaseOperator> op)
{
    m_pOperator = op;
}

QSharedPointer<BaseOperator> GraphicsView::operatorObj()
{
    return m_pOperator;
}

void GraphicsView::initialize()
{
    this->setMouseTracking(true);
    this->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
    auto op = QSharedPointer<DefualtOperator>(new DefualtOperator(this));
    this->setOperatorObj(op);
    startTimer(0);
}

void GraphicsView::mousePressEvent(QMouseEvent *event)
{
    m_isPressed = true;
    QPoint pos = event->pos();
    m_pOperator->mousePressEvent(event,this->mapToScene(pos));
}

void GraphicsView::mouseMoveEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();
    if(m_isPressed){
        m_pOperator->mouseMoveEvent(event,this->mapToScene(pos));
    }else{
        m_pOperator->mouseHoverEvent(event,this->mapToScene(pos));
    }
}

void GraphicsView::mouseReleaseEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();
    m_isPressed = false;
    m_pOperator->mouseReleaseEvent(event,this->mapToScene(pos));
}

void GraphicsView::mouseDoubleClickEvent(QMouseEvent *event)
{
    QPoint pos = event->pos();
    m_pOperator->mouseDoubleClickEvent(event,this->mapToScene(pos));
}

void GraphicsView::wheelEvent(QWheelEvent *event)
{
    QPointF pos = this->mapToScene(event->pos());
    m_pOperator->wheelEvent(event,pos);
}

void GraphicsView::keyPressEvent(QKeyEvent *event)
{
    m_pOperator->keyPressEvent(event);
}

void GraphicsView::keyReleaseEvent(QKeyEvent *event)
{
    m_pOperator->keyReleaseEvent(event);
}

void GraphicsView::resizeEvent(QResizeEvent *event)
{
    //    moveScene(QPoint(0,0));
    m_pOperator->resizeEvent(event);
    QGraphicsView::resizeEvent(event);
}

void GraphicsView::timerEvent(QTimerEvent *event)
{
    m_pOperator->timerEvent(event);
}

bool GraphicsView::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::Leave:
    {
        if(!m_pOperator.isNull()){
            m_pOperator->graphicsViewLeaveEvent();
        }
    }
        break;
    default:
        break;
    }
    return QGraphicsView::event(event);
}

DefualtOperator

        此类继承BaseOperator, 实现GrahpicsView中的事件(此项目主要表现为鼠标按下、释放, 窗口大小变换),实现整个游戏的逻辑。

defualtoperator.h

#ifndef DEFUALTOPERATOR_H
#define DEFUALTOPERATOR_H

#include "baseoperator.h"

#include <QObject>
#include <QUndoStack>
#include <QTime>

#include "cushion.h"
#include "ball.h"
#include "club.h"
#include "undocommands.h"

class DefualtOperator : public BaseOperator
{
    Q_OBJECT
public:
    explicit DefualtOperator(GraphicsView *parent);
    ~DefualtOperator();

    virtual void mousePressEvent(QMouseEvent *event, QPointF scenePoint) override;
    virtual void mouseMoveEvent(QMouseEvent *event, QPointF scenePoint) override;
    virtual void mouseHoverEvent(QMouseEvent *event, QPointF scenePoint) override;
    virtual void mouseReleaseEvent(QMouseEvent *event, QPointF scenePoint) override;
    virtual void mouseDoubleClickEvent(QMouseEvent *event, QPointF scenePoint) override;
    virtual void wheelEvent(QWheelEvent *event, QPointF scenePoint) override;

    virtual void keyPressEvent(QKeyEvent *event) override;
    virtual void keyReleaseEvent(QKeyEvent *event) override;

    virtual void resizeEvent(QResizeEvent *event) override;
    void timerEvent(QTimerEvent *event) override;

    //单独传入白球、球杆、球台方便直接操作
    void setWhiteBall(Ball *ball);
    void setClub(Club *club);
    void setCushion(Cushion *cushion);

    //开始
    void start();

public slots:

    //撤销
    void onUndo();

    //恢复
    void onRedo();
protected:
    void initialize();

    //计算帧率
    void calculateFps();

protected slots:

    //每帧刷新,做相应运动计算
    void updateView();


private:
    bool m_isBallsMove = false;            //球处于移动状态
    bool m_isClubMove = false;             //球杆处于移动状态
    QUndoStack *m_pUndoStack   = nullptr;  //回退处理
    float m_fps;                           //帧率
    Ball *m_pWhiteBall         = nullptr;  //白球
    Cushion *m_pCushion        = nullptr;  //球桌
    Club *m_pClub              = nullptr;  //球杆
    QGraphicsLineItem *m_pLine = nullptr;  //辅助线
    QTimer m_timer;                        //定时器
    QMap<Ball *, QPointF> m_oldBallPos;    //保存上一次球的位置
};

#endif // DEFUALTOPERATOR_H

defualtoperator.cpp

#include "defualtoperator.h"


DefualtOperator::DefualtOperator(GraphicsView *parent)
    : BaseOperator{parent}
{
    initialize();
}

DefualtOperator::~DefualtOperator()
{

}

void DefualtOperator::mousePressEvent(QMouseEvent *event, QPointF scenePoint)
{
    //此项目只考虑鼠标左键
    if(event->button() != Qt::LeftButton)
    {
        return;
    }

    //球处于移动状态 或者 在出杆状态,不允许操作
    if(m_isBallsMove || m_isClubMove)
    {
        return;
    }

    //先得到场景中的球及球台
    Cushion *pCushion;
    QList<Ball*> balls;
    for(auto item:scene()->items())
    {
        if(static_cast<ItemBase*>(item)->itemtype() == ItemBase::ball)
        {
            balls.append(static_cast<Ball*>(item));
        }
        else if(static_cast<ItemBase*>(item)->itemtype() == ItemBase::cushion)
        {
            pCushion = static_cast<Cushion*>(item);
        }
    }

    //若场景中没有白球,则需根据当前点击点放置白球
    if(!scene()->items().contains(m_pWhiteBall))
    {
        //放置白球必须在半圆形区域(游戏规则)
        if(!QGraphicsEllipseItem(pCushion->m_inRect.x()/2/2.6 - pCushion->m_inRect.x()*737/3569/2.6 - pCushion->m_inRect.x()*292/3569/2.6,
                             -pCushion->m_inRect.x()*292/3569/2.6,
                             pCushion->m_inRect.x()*292/3569/2.6*2,
                             pCushion->m_inRect.x()*292/3569/2.6*2).shape().toFillPolygon().containsPoint(scenePoint,Qt::WindingFill)
            || scenePoint.x() < pCushion->m_inRect.x()/2/2.6 - pCushion->m_inRect.x()*737/3569/2.6)
        {
            return;
        }
        //不能在已有球的位置放置白球
        for(auto ball:balls)
        {
            if(QVector2D(ball->pos() - scenePoint).length() < 2*ball->r())
            {
                return;
            }
        }
        m_pWhiteBall->setPos(scenePoint);
        if(!scene()->items().contains(m_pWhiteBall))
        {
            scene()->addItem(m_pWhiteBall);
        }
        return;
    }

    //若点击到白球, 不做瞄准处理,直接退出
    if(QVector2D(m_pWhiteBall->pos() - scenePoint).length() < m_pWhiteBall->r() + 1e-6)
    {
        return;
    }

    //以下为进入瞄准状态
    QPointF pos = scenePoint;
    //限制球杆拉伸的距离(最大击球速度)
    if(QVector2D(pos - m_pWhiteBall->pos()).length() > 500)
    {
        pos = QVector2D(QVector2D(m_pWhiteBall->pos()) + QVector2D(pos - m_pWhiteBall->pos()).normalized() * 500).toPointF();
    }
    QLineF line = QLineF(pos,m_pWhiteBall->pos());
    line.setLength(line.length() + 50);
    //设置辅助线
    m_pLine->setLine(line);
    if(!scene()->items().contains(m_pLine))
    {
        scene()->addItem(m_pLine);
    }
    //设置球杆的位置与方向
    m_pClub->setPos(pos);
    m_pClub->setDir(QVector2D(m_pWhiteBall->pos() - pos));
}

void DefualtOperator::mouseMoveEvent(QMouseEvent *event, QPointF scenePoint)
{
    //此事件为鼠标按下移动

    Q_UNUSED(event)
    if(!scene()->items().contains(m_pLine))
    {
        return;
    }

    //若鼠标移动到白球, 退出瞄准状态
    if(QVector2D(m_pWhiteBall->pos() - scenePoint).length() < m_pWhiteBall->r() + 1e-6)
    {
        //移除辅助线
        if(scene()->items().contains(m_pLine))
        {
            scene()->removeItem(m_pLine);
        }
        //球杆放置原位
        m_pClub->setDir(QVector2D(-1, 0));
        m_pClub->setPos(-m_pClub->length()/2.6/2, m_pCushion->m_outRect.y()/2.6/2 + 100/2.6);
        return;
    }

    //根据鼠标位置改变瞄准方向与力度
    QPointF pos = scenePoint;
    if(QVector2D(pos - m_pWhiteBall->pos()).length() > 500)
    {
        pos = QVector2D(QVector2D(m_pWhiteBall->pos()) + QVector2D(pos - m_pWhiteBall->pos()).normalized() * 500).toPointF();
    }
    QLineF line = QLineF(pos,m_pWhiteBall->pos());
    line.setLength(line.length() + 50);
    m_pLine->setLine(line);

    m_pClub->setPos(pos);
    m_pClub->setDir(QVector2D(m_pWhiteBall->pos() - scenePoint));

}

void DefualtOperator::mouseHoverEvent(QMouseEvent *event, QPointF scenePoint)
{
    Q_UNUSED(event)
    Q_UNUSED(scenePoint)
}

void DefualtOperator::mouseReleaseEvent(QMouseEvent *event, QPointF scenePoint)
{
    Q_UNUSED(event)
    Q_UNUSED(scenePoint)
    //判断是否处于瞄准状态(是否有辅助线)
    if(!scene()->items().contains(m_pLine))
    {
        return;
    }
    //退出瞄准状态, 进入出杆状态
    if(scene()->items().contains(m_pLine))
    {
        scene()->removeItem(m_pLine);
    }
    m_isClubMove = true;
}

void DefualtOperator::mouseDoubleClickEvent(QMouseEvent *event, QPointF scenePoint)
{
    Q_UNUSED(event)
    Q_UNUSED(scenePoint)
}

void DefualtOperator::wheelEvent(QWheelEvent *event, QPointF scenePoint)
{
    Q_UNUSED(event)
    Q_UNUSED(scenePoint)
}

void DefualtOperator::keyPressEvent(QKeyEvent *event)
{
    Q_UNUSED(event)
}

void DefualtOperator::keyReleaseEvent(QKeyEvent *event)
{
    Q_UNUSED(event)
}

void DefualtOperator::resizeEvent(QResizeEvent *event)
{
    Q_UNUSED(event)
    updateView();
}

void DefualtOperator::timerEvent(QTimerEvent *event)
{
    Q_UNUSED(event)
}

void DefualtOperator::setWhiteBall(Ball *ball)
{
    m_pWhiteBall = ball;
}

void DefualtOperator::setClub(Club *club)
{
    m_pClub = club;
}

void DefualtOperator::setCushion(Cushion *cushion)
{
    m_pCushion = cushion;
}

void DefualtOperator::start()
{
    m_pUndoStack->clear();
    m_oldBallPos.clear();
    for(auto item:scene()->items())
    {
        if(static_cast<ItemBase*>(item)->itemtype() == ItemBase::ball)
        {
            m_oldBallPos.insert(static_cast<Ball*>(item), item->pos());
        }
    }
//    m_oldBallPos.insert(m_pWhiteBall, m_pWhiteBall->pos());
    connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateView()));
    m_timer.start(1000/240.0);
}

void DefualtOperator::onUndo()
{
    if(m_isBallsMove || m_isClubMove)
    {
        return;
    }
    m_pUndoStack->undo();
}

void DefualtOperator::onRedo()
{
    if(m_isBallsMove || m_isClubMove)
    {
        return;
    }
    m_pUndoStack->redo();
}

void DefualtOperator::initialize()
{
    m_pUndoStack = new QUndoStack(this);
    m_pLine = new QGraphicsLineItem;
    m_pGView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    m_pGView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}

void DefualtOperator::calculateFps()
{
    static QTime time(QTime::currentTime());//
    double key = time.elapsed()/1000.0;
    //this->replot();
    static double lastFpsKey = 0;
    static int  frameCount;
    ++frameCount;
    if(key - lastFpsKey>1){
        lastFpsKey = key;
        frameCount = 0;
    }
    m_fps = frameCount/(key-lastFpsKey);
}

void DefualtOperator::updateView()
{
    //先计算帧率
    calculateFps();

    //判断是否所有球是否静止
    if(m_isBallsMove)
    {
        bool isStatic = true;
        QMap<Ball *, QPointF> newBallPos;
        for(auto item:scene()->items())
        {
            if(static_cast<ItemBase*>(item)->itemtype() == ItemBase::ball)
            {
                if(qAbs(static_cast<Ball*>(item)->speed()) >= 1e-6)
                {
                    isStatic = false;
                    break;
                }
                newBallPos.insert(static_cast<Ball*>(item), item->pos());
            }
        }
        //若静止则保存上一次位置到撤销栈,并记录这次位置
        if(isStatic)
        {
            m_pUndoStack->push(new UndoCommandMoveItems(m_oldBallPos, newBallPos, scene(), "balls move"));
            m_oldBallPos = newBallPos;
            m_isBallsMove = false;
        }
    }
    if(isnan(m_fps))
    {
        return;
    }

    //若处于出杆状态
    if(m_isClubMove)
    {
        //沿着辅助线计算球杆运动到的位置
        QPointF pos = m_pClub->pos() + QVector2D(m_pLine->line().p2() - m_pLine->line().p1()).toPointF()/m_fps*5;
        //判断与白球碰撞
        if(QVector2D(m_pLine->line().p1() - pos).length() >= m_pLine->line().length() - 50)
        {
            //可根据辅助线的拉伸长度决定速度大小
            float speed = m_pLine->line().length()/50*200;
            //给白球一个沿着击球方向的速度
            m_pWhiteBall->setMoveDir(QVector2D(m_pLine->line().p2() - m_pLine->line().p1()).normalized());
            m_pWhiteBall->setSpeed(speed);
            m_pClub->setDir(QVector2D(-1, 0));
            m_pClub->setPos(-m_pClub->length()/2.6/2, m_pCushion->m_outRect.y()/2.6/2 + 100/2.6);
            //出杆状态结束,球将处于运动状态
            m_isClubMove = false;
            m_isBallsMove = true;
            return;
        }
        //球杆移动
        m_pClub->setPos(pos);
        return;
    }
    //若球都处于静止状态, 则无需下列运动计算
    if(!m_isBallsMove)
    {
        return;
    }
    //先得到场景上的所有球及球台
    QList<Ball *> balls;
    Cushion* pCushion = nullptr;
    foreach (QGraphicsItem *item, scene()->items()) {
        if (static_cast<ItemBase*>(item)->itemtype() == ItemBase::ball)
        {
            balls << static_cast<Ball*>(item);
        }
        else if (static_cast<ItemBase*>(item)->itemtype() == ItemBase::cushion)
        {
            pCushion = static_cast<Cushion*>(item);
        }
    }
    //保存副本
    auto tempballs = balls;
    foreach (Ball *ball, tempballs)
    {
        //与球台进行碰撞检测并判断进球,若进球, 则移除场景
        if(pCushion->collisionWithBall(ball, m_fps))
        {
            ball->setSpeed(0);
            balls.removeOne(ball);
            if(scene()->items().contains(ball))
            {
                scene()->removeItem(ball);
            }
        }
    }
    //计算球与球之间的碰撞
    foreach (Ball *ball, balls)
    {
        foreach (Ball *ball_temp, balls)
        {
            if(ball != ball_temp)
            {
                //计算出球沿着击中球中心的速度分量, 这个分量(矢量)便是球的损失速度
                //两球的相对方向
                QVector2D pos_dif = QVector2D(ball_temp->scenePos() - ball->scenePos());
                //根据距离与半径判断碰撞
                if(pos_dif.length() < ball->r() + ball_temp->r())
                {
                    QVector2D temp1 = (ball->moveDir()*pos_dif);
                    //向量积
                    float vectorValue = (temp1.x() + temp1.y());

                    //(向量积/模之积)极为余弦值
                    float pos_difCosBallDir = vectorValue/(pos_dif.length()*ball->moveDir().length());

                    //pos_dif方向的速度损失量(被击中球的增加量)
                    float lossSpeed = pos_difCosBallDir*ball->speed();
                    if(pos_difCosBallDir > 0)
                    {
                        //被击中球增加这个方向的速度
                        ball_temp->addSpeedVector(0.8*lossSpeed*pos_dif.normalized());

                        //减少这个方向的速度
                        ball->addSpeedVector(-0.8*lossSpeed*pos_dif.normalized());
                    }
                }
            }
        }
    }

    foreach (Ball *ball, balls)
    {
        //根据球到一个偏x方向45度的斜线‘/’的距离, 设置ZValue值,保证右下方的球始终在左上方的球上层, 避免左上方球的阴影会遮挡右下方的球体
        ball->setZValue(QVector2D(ball->pos()).distanceToLine(QVector2D(-10000, -10000), QVector2D(1, 1)));

        //球体运动
        ball->move(m_fps);
    }
}


MainWin

        主窗口,初始化游戏,摆放按钮。

mainwin.h

#ifndef MAINWIN_H
#define MAINWIN_H

#include <QWidget>
#include <QPushButton>
#include "ui_mainwin.h"
#include "ball.h"
#include "cushion.h"
#include "club.h"

#ifndef ADDITEM
#define ADDITEM(item) if(!m_pGraphicsView->scene()->items().contains(item)){m_pGraphicsView->scene()->addItem(item);}
#endif

class MainWin : public QWidget, public Ui_MainWin
{
    Q_OBJECT

public:
    MainWin(QWidget *parent = nullptr);
    ~MainWin();

    void initialzed();

protected:
    bool event(QEvent *event);

protected slots:
    void rst();

private:
    QList<Ball*> m_redBalls;
    Cushion *m_pCushion     = nullptr;
    Ball *m_pWhite          = nullptr;
    Ball *m_pBlack          = nullptr;
    Ball *m_pPink           = nullptr;
    Ball *m_pBlue           = nullptr;
    Ball *m_pYellow         = nullptr;
    Ball *m_pCoffee         = nullptr;
    Ball *m_pGreen          = nullptr;
    Club *m_pClub           = nullptr;
    QPushButton *m_pBtnRst  = nullptr;
    QPushButton *m_pBtnUndo = nullptr;
    QPushButton *m_pBtnRedo = nullptr;
};
#endif // MAINWIN_H

mainwin.cpp

#include "mainwin.h"
#include "ui_mainwin.h"

#include <QtMath>
#include "defualtoperator.h"
#include "cushion.h"
#include "club.h"

MainWin::MainWin(QWidget *parent)
    : QWidget(parent)
{
    setupUi(this);
    initialzed();
}

MainWin::~MainWin()
{

}

void MainWin::initialzed()
{
    this->setWindowTitle("Billiards");
    m_pCushion = new Cushion();
    m_pGraphicsView->createScene();
    ADDITEM(m_pCushion);
    m_pClub = new Club();
    m_pWhite  = new Ball(Ball::white);
    m_pBlack  = new Ball(Ball::black);
    m_pPink   = new Ball(Ball::pink);
    m_pBlue   = new Ball(Ball::blue);
    m_pYellow = new Ball(Ball::yellow);
    m_pCoffee = new Ball(Ball::coffee);
    m_pGreen  = new Ball(Ball::green);
    for(int i = 0; i < 15; ++i)
    {
        m_redBalls.append(new Ball(Ball::red));
    }
    static_cast<DefualtOperator*>(m_pGraphicsView->operatorObj().get())->setWhiteBall(m_pWhite);
    static_cast<DefualtOperator*>(m_pGraphicsView->operatorObj().get())->setCushion(m_pCushion);
    static_cast<DefualtOperator*>(m_pGraphicsView->operatorObj().get())->setClub(m_pClub);

    m_pBtnRst = new QPushButton("重置", m_pGraphicsView);
    m_pBtnRst->setFixedSize(100, 30);
    connect(m_pBtnRst, SIGNAL(clicked(bool)), this, SLOT(rst()));

    m_pBtnUndo = new QPushButton("回退", m_pGraphicsView);
    m_pBtnUndo->setFixedSize(100, 30);
    m_pBtnUndo->move(m_pBtnRst->pos() + QPoint(m_pBtnRst->width(), 0));
    connect(m_pBtnUndo, SIGNAL(clicked(bool)), static_cast<DefualtOperator*>(m_pGraphicsView->operatorObj().get()), SLOT(onUndo()));

    m_pBtnRedo = new QPushButton("恢复", m_pGraphicsView);
    m_pBtnRedo->setFixedSize(100, 30);
    m_pBtnRedo->move(m_pBtnUndo->pos() + QPoint(m_pBtnUndo->width(), 0));
    connect(m_pBtnRedo, SIGNAL(clicked(bool)), static_cast<DefualtOperator*>(m_pGraphicsView->operatorObj().get()), SLOT(onRedo()));

    rst();
}

bool MainWin::event(QEvent *event)
{
    //根据窗口大小变化调整缩放, 使图元始终保持在窗口中
    static float scale = 1;
    if(event->type() == QEvent::Resize)
    {
        m_pGraphicsView->scale(1/scale, 1/scale);
        if(m_pGraphicsView->width()/m_pGraphicsView->height() >
                m_pCushion->boundingRect().width()/m_pCushion->boundingRect().height())
        {
            scale = m_pGraphicsView->height()/m_pCushion->boundingRect().height()/1.0;
        }
        else
        {
            scale = m_pGraphicsView->width()/m_pCushion->boundingRect().width()/1.0;
        }
        scale *= 0.8f;
        m_pGraphicsView->scale(scale, scale);
        m_pGraphicsView->scene()->setSceneRect(m_pCushion->boundingRect());
    }
    return QWidget::event(event);
}

void MainWin::rst()
{
    //规则:初始状态没有白球, 需要自己放置在半圆形区域
    if(m_pGraphicsView->scene()->items().contains(m_pWhite))
    {
        m_pGraphicsView->scene()->removeItem(m_pWhite);
    }
    //根据规则设置彩球摆放位置
    m_pBlack->setPos(-m_pCushion->m_inRect.x()/2.6/2 + 324/2.6, 0);
    m_pBlack->setSpeed(0);
    m_pPink->setPos(-m_pCushion->m_inRect.x()/2.6/2/2, 0);
    m_pPink->setSpeed(0);
    m_pBlue->setPos(0, 0);
    m_pBlue->setSpeed(0);
    m_pYellow->setPos(m_pCushion->m_inRect.x()/2/2.6 - 737/2.6, 292/2.6);
    m_pYellow->setSpeed(0);
    m_pCoffee->setPos(m_pCushion->m_inRect.x()/2/2.6 - 737/2.6, 0);
    m_pCoffee->setSpeed(0);
    m_pGreen->setPos(m_pCushion->m_inRect.x()/2/2.6 - 737/2.6, -292/2.6);
    m_pGreen->setSpeed(0);
    m_pWhite->setSpeed(0);

    //15个红球可以先确定第一个球(粉球旁边)的位置, 然后通过计算得出其余球位置
    float baseX = -m_pCushion->m_inRect.x()/2.6/2/2 - m_pBlack->r()*2 - 20/2.6;
    float d = m_pBlack->r()*2;
    for(int i = 0; i < 15; ++i)
    {
        float x = 0;
        float y = 0;
        if(i < 1)
        {
            x = baseX;
            y = 0;
        }
        else if(i < 3)
        {
            x = baseX - m_pBlack->r()*qPow(3, 0.5);
            y = 0 + (i - 1.5)*d;
        }
        else if(i < 6)
        {
            x = baseX - m_pBlack->r()*qPow(3, 0.5)*2;
            y = 0 + (i - 4)*d;
        }
        else if(i < 10)
        {
            x = baseX - m_pBlack->r()*qPow(3, 0.5)*3;
            y = 0 + (i - 7.5)*d;
        }
        else
        {
            x = baseX - m_pBlack->r()*qPow(3, 0.5)*4;
            y = 0 + (i - 12)*d;
        }
        m_redBalls[i]->setPos(x, y);
        m_redBalls[i]->setSpeed(0);
        m_redBalls[i]->setZValue(QVector2D(m_redBalls[i]->pos()).distanceToLine(QVector2D(-10000, -10000), QVector2D(1, 1)));
        ADDITEM(m_redBalls[i]);
    }
    //ZValue值大的, 绘制在上层
    m_pClub->setZValue(20000);
    m_pClub->setDir(QVector2D(-1, 0));
    m_pClub->setPos(-m_pClub->length()/2.6/2, m_pCushion->m_outRect.y()/2.6/2 + 100/2.6);
    ADDITEM(m_pBlack);
    ADDITEM(m_pPink);
    ADDITEM(m_pBlue);
    ADDITEM(m_pYellow);
    ADDITEM(m_pCoffee);
    ADDITEM(m_pGreen);
    ADDITEM(m_pClub);
    static_cast<DefualtOperator*>(m_pGraphicsView->operatorObj().get())->start();
}

结语

        代码中已经敲了大致注释, 欢迎提问。

        国际站点:https://github.com/yibobunengyuntian

;