Моя увага звернула на те, що для роботи з сигналами та слотами замість класу успадкованого від QGraphicsItem та від QObject можна використовувати клас успадкований від QGraphicsObject . І справді, якщо трохи покопатися у вихідниках QGraphicsObject , то виявиться, що це клас успадкований від QGraphicsItem та від QObject . Тобто також застосовується множинне успадкування, тільки в цьому випадку всі велосипеди вже написані до нас. Тому спробуємо попрацювати з цим класом з прикладу ігрової механіки.
А саме, пропоную написати програму, в якій Ми переміщатимемо героя кліком миші по графічній сцені, як у будь-якій РПГ на кшталт Diablo.
Структура проекту для роботи з QGraphicsObject
- QGraphicsObjectExample.pro - профайл проекту;
- main.cpp - основний файл вихідних кодів;
- widget.h - заголовний файл вікна програми;
- widget.cpp - файл вихідних кодів вікна програми;
- customscene.h - заголовний файл кастомізованої графічної сцени ;
- customscene.cpp - файл вихідних кодів кастомізованої графічної сцени;
-
triangle.h
- заголовний файл класу героя-трикутника, який пересуватиметься;
triangle.cpp - файл вихідних кодів класу героя-трикутника.
mainwindow.ui - QGraphicsObjectExample.pro - main.cpp
Форма головного вікна нічим не примітна. У ній знаходиться лише об'єкт QGraphicsView. Файли QGraphicsObjectExample та main.cpp створюються за замовчуванням та не модифікуються.
віджет.h
Для реалізації задуманого нам знадобиться лише кастомізована графічна сцена та більше нічого. Хоча її цілком можна було оголосити і локально всередині конструктора класу Widget.
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include "customscene.h" #include "triangle.h" namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); private: Ui::Widget *ui; CustomScene *scene; }; #endif // WIDGET_H
widget.cpp
У конструкторі класу Widget проводимо ініціалізацію графічної сцени та об'єкта трикутника. А також з'єднуємо сигнал signalTargetCoordinate від графічної сцени зі слотом трикутника. signalTargetCoordinate передаватиме координати миші, до яких повинен буде рухатися трикутник.
#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(); ui->graphicsView->setScene(scene); ui->graphicsView->setRenderHint(QPainter::Antialiasing); // Устанавливаем сглаживание ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключаем скроллбар по вертикали ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключаем скроллбар по горизонтали scene->setSceneRect(0,0,520,520); // Устанавливаем размеры графической сцены Triangle *triangle = new Triangle(); triangle->setPos(260,260); scene->addItem(triangle); // Соединяем сигнал о положении курсора со слотом для передвижения героя connect(scene, &CustomScene::signalTargetCoordinate, triangle, &Triangle::slotTarget); } Widget::~Widget() { delete ui; }
customscene.h
Для передачі координат миші на графічній сцені буде використовуватися сигнал, а передаватиметься він з подій mousePressEvent і mouseMoveEvent. При цьому передача сигналу з даними буде проводитися тільки в тому випадку, якщо ліву кнопку миші затиснуто.
#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: // Сигнал для передачи координат положения курсора мыши void signalTargetCoordinate(QPointF point); public slots: private: // Функция, в которой производится отслеживание положения мыши void mouseMoveEvent(QGraphicsSceneMouseEvent *event); void mousePressEvent(QGraphicsSceneMouseEvent *event); }; #endif // CUSTOMSCENE_H
customscene.cpp
#include "customscene.h" #include <QApplication> CustomScene::CustomScene(QObject *parent) : QGraphicsScene(parent) { } CustomScene::~CustomScene() { } void CustomScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if(QApplication::mouseButtons() == Qt::LeftButton){ emit signalTargetCoordinate(event->scenePos()); // Передаём данный о местоположении клика } } void CustomScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if(QApplication::mouseButtons() == Qt::LeftButton){ emit signalTargetCoordinate(event->scenePos()); // Передаём данный о местоположении клика } }
трикутник.h
Даний клас успадковуємо від класу QGraphicsObject і не застосовуємо успадкування від класу QObject, оскільки QGraphicsObject вже успадкований від нього, а також від QGrapihcsItem. Подальша робота з даним об'єктом проводиться також, як би ми працювали з класом, успадкованим від QGrapgicsItem.
Невеликим моментом у порівнянні з іншими статтями про GameDev , наприклад тієї, де ми просто відстежуємо переміщення миші герой, є те, що тут є змінна стани стояти/йти .
Вона необхідна для того, щоб героя зупинити в тому випадку, коли він упирається в стіну або межу території, і точка його прибуття виявиться недосяжною. А так тут також використовується ігровий таймер з ігровим слотом, де обробляється процес руху головного героя.
А ось слот отримання координат цільової точки обробляє поворот героя у бік мети.
#ifndef TRIANGLE_H #define TRIANGLE_H #include <QObject> #include <QTimer> #include <QGraphicsObject> #define GO true #define STOP false class Triangle : public QGraphicsObject { Q_OBJECT public: explicit Triangle(); signals: private: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); public slots: void slotTarget(QPointF point); // Слот для установки координаты, куда нужно идти private: QTimer *gameTimer; // Игровой таймер QPointF target; // Положение курсора bool state; // Статус идти/стоять private slots: void slotGameTimer(); // Игровой слот }; #endif // TRIANGLE_H
triangle.cpp
#include "triangle.h" #include <math.h> #include <QPainter> 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() : QGraphicsObject() { setRotation(0); state = STOP; gameTimer = new QTimer(); // Инициализируем игровой таймер // Подключаем сигнал от таймера и слоту обработки игрового таймера connect(gameTimer, &QTimer::timeout, this, &Triangle::slotGameTimer); gameTimer->start(5); // Стартуем таймер } QRectF Triangle::boundingRect() const { return QRectF(-12,-15,24,30); } void Triangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { // Отрисовка треугольника QPolygon polygon; polygon << QPoint(0,-15) << QPoint(12,15) << QPoint(-12,15); painter->setBrush(Qt::red); painter->drawPolygon(polygon); Q_UNUSED(option); Q_UNUSED(widget); } void Triangle::slotGameTimer() { if(state){ QLineF lineToTarget(QPoint(0,0), mapFromScene(target)); if(lineToTarget.length() > 2){ setPos(mapToParent(0, -2)); } /* Проверка выхода за границы поля * Если объект выходит за заданные границы, то возвращаем его назад * */ if(this->x() - 30 < 0){ this->setX(30); /// слева state = STOP; // Останавливаемся } if(this->x() + 30 > 520){ this->setX(520 - 30); /// справа state = STOP; // Останавливаемся } if(this->y() - 30 < 0){ this->setY(30); /// сверху state = STOP; // Останавливаемся } if(this->y() + 30 > 520){ this->setY(520 - 30); /// снизу state = STOP; // Останавливаемся } } } void Triangle::slotTarget(QPointF point) { // Определяем расстояние до цели target = point; QLineF lineToTarget(QPointF(0, 0), mapFromScene(target)); // Угол поворота в направлении к цели qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length()); if (lineToTarget.dy() < 0) angleToTarget = TwoPi - angleToTarget; angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2); // Поворачиваем героя к цели 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); } state = GO; // Разрешаем идти }
Підсумок
В результаті Ви отримаєте додаток, в якому керуватимете переміщенням головного героя за допомогою однієї лише мишки, ніби це РПГ, на зразок Diablo. Демонстрацію роботи програми Ви можете побачити у відеоуроку.
Посилання на завантаження проекту в zip-архіві: QGraphicsObjectExample
Добрый день! Пожалуйста подскажите, что может означать ошибка - ~\customscene.h:17: ошибка: 'void CustomScene::signalTargetCoordinate(QPointF)' is protected
Добрый день! По какой-то причине у вас данный сигнал оказался в protected секции, а не в секции сигналов, то есть скорее всего он объявлен в заголовочнике так
Большое спасибо. Файл был Ваш, но принудительное добавление public: перед void signal эту ошибку устранило. Но есть еще ошибка -
~\widget.cpp:27: ошибка: no matching function for call to 'Widget::connect(CustomScene*&, void (CustomScene::*)(QPointF), Triangle*&, void (Triangle::*)(QPointF))'
Если можно помогите пожалуйста - я в Qt вторую неделю всего.
Скорее всего в одном из классов не объявлена нужная функция (слот или сигнал). Нужно, чтобы сигнатуры сигнала и слота совпадали в данном конкретном примере. А в одном из классов видимо отсутствует нужный метод ( сигнал или слот - почитайте эту статью, чтобы разобраться с теорией сигналов и слотов).
Большое спасибо, удачи Вам, здоровья, и всего доброго.
Добрый день! Еще раз спасибо за уроки. Все заработало, помог ваш урок 24. Public был ни при чем, нужно было две строки проекта с connect записать в стиле Qt 4.8.5. Правда - стрелками на клавиатуре треугольник двигается с точностью наоборот. Но это мелочи. Всего доброго.
Добрый день!
Что? А почему? Вы используете Qt 4.8?
Добрый день! Это требование руководства, увы!
Вы случаем не в ОНИИПе работаете?
Добрый день! Точно так, в НИИ, только не О...П. Остальное, к сожалению, не моя информация. Всего доброго.