Privacy policyContactsAbout siteOpinionsGitHubDonate
© EVILEG 2015-2018
Recommend hosting
TIMEWEB

GameDev on Qt - Tutorial 1. Track mouse movement in QGraphicsScene

Qt, mouse, cursor, отслеживание, QGraphicsScene, GameDev

Everyone remembers the game Crimsonland ? There had to cut stacks of monsters. To move on the playing field, we used the keys W, A, S, D, and shooting direction the mouse cursor, which was produced track. So, in itself mechanism of this very track the mouse is very simple. The implementation can be many, depending on the instruments used in programming, but if we talk about a common QGraphicsScene , then let me show you my version of the implementation of such a mechanism.

Project structure

In this project, in addition to the main file used two additional classes. First - this is a QGraphicsScene customized, which will make tracking the cursor position and transmit information about its position, and the second - is the protagonist of our favorite red triangle, which we will manage with the keys W, A, S, D.

Project structure:

  • TargetMotion.pro - Project Profile;
  • widget.h - Header file the main application window;
  • widget.cpp - source files of the main application window;
  • triangle.h - The header of the protagonist of the Red Triangle;
  • triangle.cpp - source files of the protagonist of the Red Triangle;
  • customscene.h - Header file customized graphic scene;
  • customscene.cpp - File source customized graphic scene;
  • cursor.qrc - resource file, which contains a customized mouse pointer.

Writing mouse cursor tracking

widget.ui

File form of the main application window. It just put an object of class QGraphicsView and stretch it around the window.

customscene.h

All that needs to be done in this header file, it is only declare a method for tracking the movement of the mouse mouseMoveEvent() , and the signal, which will be transferred to the coordinates of the mouse, do not forget to extend class from QGraphicsScene .

#ifndef CUSTOMSCENE_H
#define CUSTOMSCENE_H

#include <QObject>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>

class CustomScene : public QGraphicsScene
{
    Q_OBJECT
public:
    explicit CustomScene(QObject *parent = 0);
    ~CustomScene();

signals:
    // The signal of transmit the coordinates of the mouse position
    void signalTargetCoordinate(QPointF point);

public slots:

private:
    // The function, which made tracking the mouse
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
};

#endif // CUSTOMSCENE_H

customscene.cpp

You will not believe, but in the source code file we just call signal with coordinates in mouseMoveEvent method.

#include "customscene.h"

CustomScene::CustomScene(QObject *parent) :
    QGraphicsScene()
{
    Q_UNUSED(parent);
}

CustomScene::~CustomScene()
{

}

void CustomScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    emit signalTargetCoordinate(event->scenePos());
}

triangle.h

And now declare all objects and functions necessary for the correct rendering of our hero and the implementation of tracking the cursor in the game. For a more complete understanding of the operation of this class and its origins are encouraged to review a series of articles "How to make a game using Qt", which is presented in the section Qt Lessons .

Note. To handle keys used WinAPI library.

#ifndef TRIANGLE_H
#define TRIANGLE_H

#include <QObject>
#include <QGraphicsItem>
#include <QPainter>
#include <QPolygon>
#include <QTimer>

#include <windows.h>

class Triangle : public QObject, public QGraphicsItem
{
    Q_OBJECT
public:
    explicit Triangle(QObject *parent = 0);
    ~Triangle();

signals:

public slots:
    // The slot for the cursor position data
    void slotTarget(QPointF point);

private:
    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);

private slots:
    void slotGameTimer();   // Game slot

private:
    QTimer *gameTimer;      // Game timer
    QPointF target;         // Cursor position
};

#endif // TRIANGLE_H

triangle.cpp

Drawing triangle occurs once in this case, but the reversal of the object and its movement in the graphic scene occurs periodically by a signal from the timer and gaming signals from the graphics cursor is moved on the stage. If the cursor does not move, but only moves the triangle, the triangle still follows the mouse cursor as a triangle stores information about the last cursor position in the graphic scene.

#include "triangle.h"
#include <math.h>

static const double Pi = 3.14159265358979323846264338327950288419717;
static double TwoPi = 2.0 * Pi;

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

Triangle::Triangle(QObject *parent) :
    QObject(parent), QGraphicsItem()
{
    setRotation(0);      // Set the starting triangle rotation
    gameTimer = new QTimer();   // Init game timer
    // Connect the signal from the timer and the slot game processing timer
    connect(gameTimer, &QTimer::timeout, this, &Triangle::slotGameTimer);
    gameTimer->start(10);   // Start timer
    target = QPointF(0,0);  // Set the initial position of the cursor
}

Triangle::~Triangle()
{

}

QRectF Triangle::boundingRect() const
{
    return QRectF(-20,-30,40,60);
}

void Triangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    /* drawing triangle
     * */
    QPolygon polygon;
    polygon << QPoint(0,-30) << QPoint(20,30) << QPoint(-20,30);
    painter->setBrush(Qt::red);
    painter->drawPolygon(polygon);

    Q_UNUSED(option);
    Q_UNUSED(widget);
}

void Triangle::slotTarget(QPointF point)
{
    // Calculate the distance to the target
    target = point;
    QLineF lineToTarget(QPointF(0, 0), mapFromScene(target));
    // The angle of rotation in the direction to the target
    qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length());
    if (lineToTarget.dy() < 0)
        angleToTarget = TwoPi - angleToTarget;
    angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2);

    /* Depending on whether the left or the right is the purpose of the hero, 
     * set the direction of rotation of the Hero of the tick timer
     * */
    if (angleToTarget >= 0 && angleToTarget < Pi) {
        // Rotate left
        setRotation(rotation() - angleToTarget * 180 /Pi);
    } else if (angleToTarget <= TwoPi && angleToTarget > Pi) {
        // Rotate right
        setRotation(rotation() + (angleToTarget - TwoPi )* (-180) /Pi);
    }
}

void Triangle::slotGameTimer()
{
    /* Move a triangle as a function of the buttons pressed
     * */
    if(GetAsyncKeyState('A')){
        this->setX(this->x() - 2);
    }
    if(GetAsyncKeyState('D')){
        this->setX(this->x() + 2);
    }
    if(GetAsyncKeyState('W')){
        this->setY(this->y() - 2);
    }
    if(GetAsyncKeyState('S')){
        this->setY(this->y() + 2);
    }

    /* Check output of bounds. 
     * If the subject is beyond the set boundaries, then return it back
     * */
    if(this->x() - 30 < 0){
        this->setX(30);         // left
    }
    if(this->x() + 30 > 500){
        this->setX(500 - 30);   // right
    }

    if(this->y() - 30 < 0){
        this->setY(30);         // top
    }
    if(this->y() + 30 > 500){
        this->setY(500 - 30);   // bottom
    }

    QLineF lineToTarget(QPointF(0, 0), mapFromScene(target));
    // The angle of rotation in the direction to the target
    qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length());
    if (lineToTarget.dy() < 0)
        angleToTarget = TwoPi - angleToTarget;
    angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2);

    /* Depending on whether the left or the right is the purpose of the hero, 
     * set the direction of rotation of the Hero of the tick timer
     * */
    if (angleToTarget >= 0 && angleToTarget < Pi) {
        // Rotate left
        setRotation(rotation() - angleToTarget * 180 /Pi);
    } else if (angleToTarget <= TwoPi && angleToTarget > Pi) {
        // Rotate right
        setRotation(rotation() + (angleToTarget - TwoPi )* (-180) /Pi);
    }
}

widget.h

Now all you need to keep the nucleus in the game, which will be located in the class of the main application window.

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QGraphicsScene>
#include <QGraphicsItem>

#include <triangle.h>
#include <customscene.h>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private:
    Ui::Widget *ui;
    CustomScene  *scene;    // We declare a graphic scene
    Triangle *triangle;     // We declare triangle
};

#endif // WIDGET_H

widget.cpp

This file declares and initializes all the game objects (graphical scene and triangle), and is also used design pattern such as Bridge, through which the signal is transmitted from the graphic scene to the triangle.

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    // Устанавливаем параметры окна приложения
    this->resize(600,600);
    this->setFixedSize(600,600);

    ui->setupUi(this);
    scene   = new CustomScene();    // Init custom scene

    ui->graphicsView->setScene(scene);  /// set scene into graphicsView
    ui->graphicsView->setRenderHint(QPainter::Antialiasing);    
    ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 

    scene->setSceneRect(0,0,500,500);   

    // Create Custom Cursor
    QCursor cursor = QCursor(QPixmap(":/cursor/cursorTarget.png"));
    ui->graphicsView->setCursor(cursor);    // Set cursor intoe QGraphicsView
    triangle = new Triangle();  // Init triangle
    triangle->setPos(250,250);  // Set start position
    scene->addItem(triangle);   // Add triangle into the scene

    /* Allowed to monitor the position of the mouse 
     * without the need for pressing the mouse button. 
     * We use this property is to QGraphicsView, which set the scene graphics
     * */
    ui->graphicsView->setMouseTracking(true);

    connect(scene, &CustomScene::signalTargetCoordinate, triangle, &Triangle::slotTarget);
}

Widget::~Widget()
{
    delete ui;
}

Result

The result is a triangle on the graphic scene to be moved by it under the influence of the keys W, A, S, D, and always look in the direction of the mouse cursor when it is in the range of graphic scenes.

Video

10% refund of hotel reservation amount on Booking
10% refund of hotel reservation amount on Booking
We offer a link with a 10% return on the amount of the order when booking a hotel through Booking
r

А где можно найти полный код проекта (проектов)? У меня ошибки линковки типа ( LNK2019: ссылка на неразрешенный внешний символ "public: void __cdecl Triangle::slotTarget(class QPointF)" (?slotTarget@Triangle@@QEAAXVQPointF@@@Z) в функции "public: __cdecl Widget::Widget(class QWidget *)" ).  И может кто подскажет как поправить?

Вот в этой статье в конце есть полный проект.

r

спасибо

r

а если например я хочу сделать вместо треугольников - текстурки, я могу это сделать в этом виджете Qt? или надо брать что то другое?

Для отрисовки используется объект класса QPainter  методе paint у треугольника. А этот класс QPainter имеет метод drawPixmap(), который может отрисовывать изображение из png файла например. Поэтому да, можете лишь переписать метод paint, и отрисовывать текстуры.

r

еще такой вопрос. у меня Qt ругается на GetAsyncKeyState (ошибка линковки). Надо либу какую то подключить? 2) а есть такая функция н кросс платформенная (зачем в таком достаточно высокоуровневом фреймворке использовать winApi?)?

Для MSVC пропишите в pro файле проекта следующие строчки

win32-msvc*{
    LIBS += -luser32
}
Видите ли, фреймворк может и достаточно высокоуровневый, но некоторый особенности работы в нём не реализованы в кросплатформенном варианте.
Например, нет тех же самых глобальных хоткеев . Я сам занимался реализацией подобного функционала для одного своего проекта.
 
Поэтому и проверка статуса клавиш тоже из этого разряда. А если говорить конкретнее, то проблема в том, что состояния клавиш проходят через цепочку событий

цепочку событий Qt приложения. Что влёчет к значительному усложнению кода, да ещё и может помешать фокусировка на некоторых виджетах, то есть события фокуса ещё вмешиваются. Здесь фокус должен сохраняться на виджете, в котором происходит действие, чтобы корректно отрабатывать нажатия клавиш. В общем много головной боли сразу появляется. Обработка события клавиш в Qt немного неподходит для реализации игровой логики на клавиатуре, это как раз один из тех моментов, когда действительно проще навелосипедить своё... хотя я противник велосипедов, но Qt не даёт нужного функционала в достаточной мере.

r

я собираю под win64.    LIBS += -luser32 и  LIBS += -luser64 не помогает

Вы вообще что-то своё уже пишите, или как? Я скачал проект, запустил его на работе на Win64 и он работает.

А так по факту заголовочник нужен
#include <windows.h>
r

Да нет. Чет у меня с qt только проблемы. Ваш проект так в qt не смог собрать (из за GetAsyncKeyState). Собрал его в visual studio, но там почему то пришлось в ручную кидать .dll в каталог (хотя путь добавлял) (может тут что изменить см скрин https://yadi.sk/i/-D9CsBhh3SL2eQ ).

r

Еще пару вопросов по code-style

1)override в qt не используется?
2)почему 0 а не nullptr ?
3)макрос Q_UNUSED. Почему нельзя просто не давать переменным имя и тд?
4)Зачем объявлять что то если там пустое тело?  (2)
5)Зачем прописывать вызов конструктора по умолчанию? (2)
6)Почему иногда не чистится динамическая память? это ошибка? не лучше ли использовать std::unique_ptr указатели вместо обычных?
7)почему нужно объявлять именно указатели на объекты и вызывать new в конструкторе, а не просто делать объекты членами класса (а не их указатели)?
8) такие штуки как public slots: , где  slots макрос который раскрывается в ничего. Для чего нужны? (просто для оформления)?
9) такие штуки как emit. Это пустые макросы. Они для оформления, или код сначала прогоняется в компиляторе qt который понимает эти "ключевые слова" ?

Честно говоря... не знаю... Я не работаю с Visual Studio вообще. В основном Qt Creator, да иногда с CLion. Больше походит на то, что у вас некорректно установлены пути к библиотекам Qt в операционной системе. Либо сама Visual Studio криво встала.

По code-style

Признаюсь честно, сам находился в плавающем состоянии, когда писал ранние статьи, как раз чтобы всё уложить в голове по полочкам, поэтому многие ваши замечания весьма к месту. Но сейчас могу более адекватно и развёрнуто Вам ответить на ваши вопросы.
  1. override в Qt используется, но они в своей странной манере в очередной раз всё переопределили своими макросами, хотя наверняка были ещё какие-то причины, которые сейчас можно считать морально устаревшими. По факту у них есть макрос Q_OVERRIDE, но я всегда пишу override (до кучи ещё и virtual добавляю), а в использовании Q_OVERRIDE не вижу смысла.
  2. На момент изучения всей этой кухни Qt не особо обратил внимание на этот момент, да и не работал плотно в рамках стандарта C++11, а так да, лучше использовать nullptr . Сейчас всегда его использую.
  3. Это уже вопрос вкусовщины. Я придерживался рамок code-style Qt, а по факту можно и не писать имя переменной, разницы принципиально никакой не будет. Q_UNUSED всё равно приводит переменную к void и типо она используется.
  4. Относительно конструкторов и деструкторов - это привычка, писать их реализацию.
  5. Там лучше передать parent`а в конструктор базового класса, мой косяк.
  6. Насчёт памяти в Qt такая ситуация, у всех классов, наследованных от Qt желательно передавать parent. Тогда при уничтожении, parent будет прибирать за собой всех потомков, поэтому и не потребуется использовать умные указатели. В этом примере некоторые недочёты имеются, нужно передавать парента при создании объекта, чего я не везде сделал, тогда утечек не будет.
  7. Лучше, как вы и сказали объявлять объекты членами класса, там например QTimer можно объявить  на стеке. Не помню, только с графической сценой как лучше будет, точно помню есть проблемы с объектами QGraphicsItem , их всегда приходится объявлять в куче, на стеке память не выделить. Работать просто не будет. С графической сценой кажется тоже какие-то проблемы в отображении были, не помню уже.
  8. Объявление в качестве slots нужно было для формирования moc файлов, когда использовался старый синтаксис сигналов и слотов на основе макросов. Скомпилироваться-то скомпилируется, если слот не объявлен в секции слотов, но работать не будет... А вот в случае с новым синтаксисом на основе указателей на методы уже да, не обязательно писать slots и т.д. Но по мне здесь лучше оставить их, это уже вопрос код стайла и разделения по секциями методов, которые должны использоваться как слоты, а какие нет.
  9. а вот emit - это действительно пустышки, я пытался найти адекватное объяснение, для чего они, но кроме того, что это синтаксический сахар, который призван указать вызов именно метода сигнала в коде, ничего не нашёл. Обычно всегда пишу их, чтобы глазу было за что зацепиться. Возможно раньше, в более ранних версиях Qt это было обязательным, сейчас точно нет. Но я застал только Qt 4.8, да и то только вскользь.
r

Спасибо за ответы. Еще хотел спросить. Вы занимались программированием сетевых игр? вот например если у нас 2 клиента (у каждого свой треугольник). Как например синхронизируется картинка, сколько раз в секунду передаются действия? просто не оч понимаю как удается избегать задержек и делать плавную картинку

r

А и по библиотеке. Зачем у qt своя библиотека вещей которые есть в в stl, boost и тд. Например зачем использовать всякие вектора Qt и тд когда они есть в stl? Или это зачем то нужно?

К моему великому сожалению я имею к геймдеву не очень большое отношение, особенно не занимался сетевыми играми, но почитываю информацию об этом в свободное время.


По факту синхронизация изображения должна соответствовать как минимум FPS, в идеале не ниже 60 раз в секунду, лучше больше. Но для очень динамичных игр используется не только передача данных о положении объектов, но и методологии предсказания будущего положения игроков и объектов в самой игре без получения данных от сервера или другого игрока. Именно поэтому иногда возникают лаги, когда вашего игрока перекидывает в другое место карты. Просто предсказание сработало неправильно, а при очередном обновлении данных от сервера игра скорректировала положения игрока на карте.  Это очень большая и обширная тема... к сожалению я тут профан.

Моё основное направление сейчас - это программное обеспечение для расчётов нагрузок строительных конструкций и зданий. В основном модели данных и взаимодействие всех компонентов системы. И т.д.

Ммм... здесь за версту отдаёт желанием написать всё своё. Кстати, голос разума у них всё-таки проснулся в Qt 5. Например, для сортировки там используется и рекомендуется уже std::sort , вместо qSort, который они пометили как deprecated. Что касается контейнеров, то у контейнеров Qt есть кое-какие удобные методы. Но последнее время я перехожу на контейнеры stl. Они гораздо функциональнее. Что касается boost, то сигналы и слоты, которые есть в boost, как говорят в интернетах, слизали именно с Qt. Так что все понемножку друг у друга заимствуют.



r

я переопределил paint треугольника. сделал рисование танчика и его пушки.

painter->drawPixmap(-30, -50, *spriteImage, 40, 0, 60, 100);
painter->drawPixmap(-20, -50, *spriteImage, 0, 0, 40, 100);
но текстура пушки белым фоном закрашивает часть танка (см скрины https://yadi.sk/d/QKmAHw8e3SL5A4 ). Как это исправить?
r

разобрался. заменил белый фон на альфаканал.  теперь другой вопрос. вот  Pixmap пушки я хочу поворачивать вокруг своей оси. можно это сделать както через setRotation например? (тоесть нарисовал корпус, повернул обьект, нарисовал пушку)

r

Фух и с этим разобрался. Теперь такой вопрос, а можно ли как-то по лучше обрабатывать столкновения обьектов, и чтоб к стене например можно было подъехать в плотную, и при этом не вьехать в ней повернувшись на месте?

Здесь уже нужно обрабатывать очертания танка и не давать возможность ему разворачиваться, если есть риск въехать в стену.

Но для этого нужно немного изменить логику обработки коллизий. Дело в том, что в данном варианте коллизии обрабатываются по факту, когда коллизии уже произошли.
А вот следующим шагом можете начать обрабатывать коллизии с предсказанием. То есть условно взять будущее положение танка и проверить на коллизии. У графической сцены есть метод items(), в этот метод можно передать объект QPolygonF, который можете описывать очертания танка в его будущем положении на карте. Если будет возвращён хотя бы один объект, то моно считать, что коллизия имеет место быть, а значит танк нельзя подвинуть или повернуть, а тогда просто не выполняете движение танка в заданном положении.
r

А не подскажите как сделать плавность анимации или как то так. например танк (прямоугольник) едет вверх и вертикален. Потом игрок нажимает клавишу влево и он должен уже ехать вертикально. Можно конечно плавно поворачивать текстуру но я думаю можно сделать лучше. Просто играл в танчики где корпус и пушка могут вращатся и ехать мгновенно на любой градус, и при этом там не было видимого резкого скачка (может это какоето сглаживание?)

Если просто сменить угол поворота или направление, то скачок всегда будет резким. Если хотите добавить некоторую плавность в поворот, то нужно делать поворот за несколько отсчётов игрового таймера, чтобы это было достаточно быстро, но при этом добавило некоторую плавность.


Нечто подобное я сделал вот в этой статье . Посмотрите следующий метод void Spider::slotGameTimer().

И ещё, у меня просьба. Создавайте, пожалуйста, новые вопросы на форуме . Там можно при создании вопроса указать статью, к которой имеет отношение данный вопрос. Так будет лучше для развития ресурса. Спасибо.
a

forgetting otnasledovat class from QGraphicsScene :D

Ну да, поленился внимательно посмотреть перевод после Google Translate :D

Спасибо, поправил

Comments

Only authorized users can post comments.
Please, Log in or Sign up
МБ
April 21, 2019, 9:40 a.m.
Моисей Бушуев

Qt - Test 001. Signals and slots

  • Result:0points,
  • Rating points-10
AA
April 17, 2019, 7:40 p.m.
Anton Ablin

Qt - Test 001. Signals and slots

  • Result:73points,
  • Rating points1
E
April 17, 2019, 6:16 p.m.
Evgeny

Qt - Test 001. Signals and slots

  • Result:100points,
  • Rating points10
Last comments
April 21, 2019, 4:22 p.m.
Евгений Легоцкой

Через метод setIcon table.horizontalHeaderItem(0).setIcon("qrc://path/to/icon.png")
April 21, 2019, 3:48 p.m.
Евгений Легоцкой

Добрый день! Спасибо за комментарий. Там действительно лучше будет сделать с инициализацией по умолчанию.
U
April 18, 2019, 3:37 p.m.
Unreal_man

А как иконку в хедер задать?
u
April 18, 2019, 2:15 a.m.
uaa

доброго времени,большое спасибо за пример для начинающего)при адаптации к своему проекту столкнулся с таким ньансом:в vepolyline.h в 47 строке нужна инициализация по умолчанию: int m_pointF...
E
April 11, 2019, 12:49 p.m.
Evgeny

Спасибо за ответ) У меня компоновщик на нее ругался просто. Оказалось, просто забыл Q_OBJECT в начале класса указать.
Now discuss on the forum
April 24, 2019, 11:22 a.m.
Ruslan Polupan

Согласен. но ситуация не поменялась. Такое чуство что данные не записываются в модель.
April 24, 2019, 6:20 a.m.
Ruslan Polupan

я так понимаю надо инфорация об устройствах.Я бы пробовал так rust@suse:~> lsblk -PNAME="sda" MAJ:MIN="8:0" RM="0" SIZE="111,8G" RO="0" TYPE="disk" MOUNTPOINT=""NAME="sda1" MAJ:MIN="8...
April 21, 2019, 4:16 p.m.
Евгений Легоцкой

Приветствую Нужно сохранять где-то выбранное значение, а потом восстанавливать его. Или использовать QSettings или добавить метод open(), в который передавать начальные значения для того...
R
April 19, 2019, 9:55 a.m.
RED_Spider

мені важко це зараз навіть перевірити, тому що знайшов коміт, це ще було в 2016 році, і цей код не буде працювати коректно зараз, єдине скажу що це були QThread
Join us in social networks

For registered users on the site there is a minimum amount of advertising