Proper data model for a 2D Tilemap (C++, Qt) - c++

I made a small 2D level editor where you can create 2D tile based maps..however, the performance inside my application is really really bad. I am currently thinking to start all over again.
The Problem is, I currently use QGraphicsItem's to represent a single tile inside a QGraphicsScene. A tile has some properties..including an image. When a map is created, I create an item for each tile which draws an image for each tile..which basically is a lot of graphicitems and it slows down the whole application. This is the function that populates a map once it is created :
for(int i=0;i<map->m_rows;i++)
{
for(int j=0;j<map->m_cols;j++)
{
Tile* thetile=map->getAt(i,j);
if(thetile)
{
if(map->getType()==twoditor::RECTANGLETILE)
{
QGraphicsItem* item= new TileGraphicsItem(thetile);
m_scene->addItem(item);
}
else if(map->getType()==twoditor::HEXAGONTILE)
{
QGraphicsItem* item= new HexagonGraphicsItem(thetile);
m_scene->addItem(item);
}
}
}
}
This works for a map with 100x100 Tiles. But if i want to create even larger maps..the loading time is really unbearable..
Can someone give me advice for a better representation of a tile map? Are there other convenient ways to show a map and edit cells(tiles) inside it?
EDIT: TileGraphicItem paint function:
void TileGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget){
setZValue(0);
if(!m_thetile->getImage().isNull())
{
painter->drawImage(0,0,m_thetile->getImage());
}
QPainterPath circle_path;
QRect duwagrect(boundingRect().x(),boundingRect().y(),boundingRect().width(),boundingRect().height());
circle_path.addRect(duwagrect);
m_pen.setStyle(Qt::SolidLine);
m_pen.setColor(Qt::black);
m_pen.setWidth(1);
painter->setPen(m_pen);
painter->drawPath(circle_path);
if(m_thetile->getProperty()->getBlocks())
{
QPainterPath circle_path;
QRect duwagrect(boundingRect().x()+2,boundingRect().y()+2,boundingRect().width()-3,boundingRect().height()-3);
circle_path.addRect(duwagrect);
m_pen.setStyle(Qt::DotLine);
m_pen.setColor(Qt::red);
m_pen.setWidth(2);
painter->setPen(m_pen);
painter->drawPath(circle_path);
}
if(this->isSelected())
{
QPainterPath circle_path;
QRect duwagrect(boundingRect().x()+2,boundingRect().y()+2,boundingRect().width()-3,boundingRect().height()-3);
circle_path.addRect(duwagrect);
m_pen.setStyle(Qt::SolidLine);
m_pen.setColor(Qt::green);
m_pen.setWidth(3);
painter->setPen(m_pen);
painter->drawPath(circle_path);
}
if(option->state & QStyle::State_MouseOver)
{
QPainterPath circle_path;
QRect duwagrect(boundingRect().x()+2,boundingRect().y()+2,boundingRect().width()-3,boundingRect().height()-3);
circle_path.addRect(duwagrect);
m_pen.setStyle(Qt::SolidLine);
m_pen.setColor(Qt::cyan);
m_pen.setWidth(2);
painter->setPen(m_pen);
painter->drawPath(circle_path);
}
}

Problem is that you are showing everything even things not needed.
You should create only visible items (items in some visible region).
Another faster approach is to create custom QGraphicsItem which paints hole map, and paint only visible tiles (no tiles as sub items).

Related

Qt Scaling Custom QGraphicsItem with unscaled text

I'm trying to create customObject (rectangle and it inherit from QGraphicsItem) that will be painted on scene with ceratin text(stored in attribute), but when I scale it - i wish to keep same size of text. Here is my over. paint function:
void CustomRectangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *options, QWidget *widget)
{
QColor currentColor = get_ColorByCurrentState();
QRectF rect = boundingRect();
QPen pen(currentColor, Own_LineWidith);
painter->setPen(pen);
painter->drawRect(rect);
QRectF rect_text(rect.x(), rect.y(),100,100);
painter->drawText(rect_text,this->getText() );
}
and my two scaling functions:
void CustomObject::scaleUp()
{
scale(ScaleFactor_X,ScaleFactor_Y);
}
void CustomObject::scaleDown()
{
scale(1/ScaleFactor_X,1/ScaleFactor_Y);
}
But text still keep scaling along with rectangle.
EDIT 1
I tried adding it another way, i nfucntion that creates and adds my rectangle to scene (here - named "newObject"), but result is still the same.
QGraphicsTextItem* GTI = new QGraphicsTextItem(newObject->toStringForScene(), newObject);
I'm beginign to think that I shoud create each text object as separeted object and save it different list. Ofcours, i would have to update it then, whenever it's object moved.
Try this:
QGraphicsTextItem* gti = new QgraphicsTextItem("text");
gti->setFont(QFont("Arial", 18));
// this is important
gti->setFlag(QGraphicsTextItem::ItemIgnoresTransformations, true);
scene->addItem(gti);
The QGraphicsItem::ItemIgnoresTransformations flag prevents your graphics item to be scaled when you scale your view (QGraphicsView).
That means that you need a separated item for rendering text. But it can be a child item of your rectangle item.
I resolved this with QGraphicsTextItem's poitner as class's attribute.
QGraphicsTextItem* GTI;
I initialzie it in constructor:
GTI_Description = new QGraphicsTextItem(this->toStringForScene());
and then I call function to updated it's X and Y:
void updateTextPosition()
{
GTI->setX( this->x() );
GTI->setY( this->y() );
}
and to add it to the scene:
addTextToScene(DragScene* _scene)
{
updateDescriptionPosition();
_scene->addItem(GTI_GTI);
_scene->update();
}
Then i just call updateTextPosition() whenerver I change positions (in my mouseRelease event's handler).

Fastest way to draw QGraphicItems on an "equivalent" QPixmap

I want to draw colored tiles as background for a QGraphicsscene and provide pan and zoom functionality for the scene using a QGraphicsView. First I used QGraphicItems to draw each tile. Since I have many tiles this was quite a performance problem when panning or zooming but since I do not need to modify any part of the tiles afterwards I switched to generating a QPixmap using the following code:
void plotGrid(){
Plotable::GraphicItems items;
append(items,mParticleFilter.createGridGraphics());
append(items,mParticleFilter.getRoi().mRectangle.createGraphics(greenPen()));
scaleItems(items,1.0,-1.0);
QGraphicsScene scene;
showItemsOnScene(items,&scene);
QRectF boundingRect = scene.itemsBoundingRect();
double cScale = ceil(1920.0/boundingRect.size().width());
QSize size(boundingRect.size().toSize()*cScale);
QPixmap pixmap(size);
pixmap.fill(Qt::transparent);
QPainter p(&pixmap);
//p.setRenderHint(QPainter::Antialiasing);
scene.render(&p);
p.end();
QGraphicsPixmapItem* item = new QGraphicsPixmapItem(pixmap);
item->setOffset(boundingRect.topLeft()*cScale);
item->scale(1/cScale,1/cScale);
mpView->showOnScene(item);
}
While this solves the zoom and pan problem, the time to generate the pixmap introduces some significant delay, probably because I first create a scene and then render it. Is there a faster way of producing a QPixmap on the fly starting from QGraphicItems ?
Just for completeness an image of the tiles:
So I finally got at least past using an intermediary scene. The following code only relies on a QPainter for rendering the pixmap. My main problem was to get all transformations right. Otherwise it is quite straight forward....
This version halves the processing time to 500ms in my scenario. 450ms are spent on painting the items. If someone has more suggestions for more improvement it would be most welcome (cahnging the resolution does not help much by the way)
void SceneWidget::showAsPixmap(Plotable::GraphicItems const& items){
QRectF boundingRect;
boostForeach(QGraphicsItem* pItem,items) {
boundingRect = boundingRect.united(pItem->boundingRect());
}
QSize size(boundingRect.size().toSize());
double const cMaxRes =1920;
double const scale = cMaxRes/boundingRect.size().width();
QPixmap pixmap(size*scale);
pixmap.fill(Qt::transparent);
QPainter p(&pixmap);
//p.setCompositionMode( QPainter::CompositionMode_Source );
p.translate(-boundingRect.topLeft()*scale);
p.scale(scale,scale);
QStyleOptionGraphicsItem opt;
boostForeach(QGraphicsItem* item,items) {
item->paint(&p, &opt, 0);
}
p.end();
QGraphicsPixmapItem* item = new QGraphicsPixmapItem(pixmap);
item->scale(1.0/scale,-1.0/scale);
item->setOffset(boundingRect.topLeft()*scale);
showOnScene(item);
}

What is the fastest way to get QWidget pixel color under mouse?

I need to get the color of pixel under mouse, inside mouseMoveEvent of a QWidget (Breadboard). Currently I have this code->
void Breadboard::mouseMoveEvent(QMouseEvent *e)
{
QPixmap pixmap = QPixmap::grabWindow(winId());
QRgb color = pixmap.toImage().pixel(e->x(), e->y());
if (QColor(color) == terminalColor)
QMessageBox::information(this, "Ter", "minal");
}
Take a look at (scaled down) screenshot below-
When user moves his mouse on breadboard, the hole should get highlighted with some different color (like in red circle). And when the mouse exits, the previous color (grey) should be restored. So I need to do following steps-
Get color under mouse
According to color, floodfill the hole. (Different holes are distinguished using color)
On mouse out, restore the color. There would be wires going over holes, so I can't update the small rectangle (hole) only.
What is the fastest way of doing this? My attempt to extract color is not working i.e the Message box in my above code never displays. Moreover I doubt if my existing code is fast enough for my purpose. Remember, how fast you will be moving your mouse on breadboard.
Note - I was able to do this using wxWidgets framework. But due to some issues that project got stalled. And I am rewriting it using Qt now.
You are invited to look at code https://github.com/vinayak-garg/dic-sim
The "idiomatic" way of doing this in Qt is completely different from what you're describing. You'd use the Graphics View Framework for this type of thing.
Graphics View provides a surface for managing and interacting with a large number of custom-made 2D graphical items, and a view widget for visualizing the items, with support for zooming and rotation.
You'd define your own QGraphicsItem type for the "cells" in the breadboard that would react to hover enter/leave events by changing their color. The connections between the cells (wires, resistors, whatever) would also have their own graphics item types with the features you need for those.
Here's a quick and dirty example for you. It produces a 50x50 grid of green cells that become red when the mouse is over them.
#include <QtGui>
class MyRect: public QGraphicsRectItem
{
public:
MyRect(qreal x, qreal y, qreal w, qreal h)
: QGraphicsRectItem(x,y,w,h) {
setAcceptHoverEvents(true);
setBrush(Qt::green);
}
protected:
void hoverEnterEvent(QGraphicsSceneHoverEvent *) {
setBrush(Qt::red);
update();
}
void hoverLeaveEvent(QGraphicsSceneHoverEvent *) {
setBrush(Qt::green);
update();
}
};
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QGraphicsScene scene;
for (int i=0; i<50; i++)
for (int j=0; j<50; j++)
scene.addItem(new MyRect(10*i, 10*j, 8, 8));
QGraphicsView view(&scene);
view.show();
return app.exec();
}
You could modify the hover event handlers to talk to your "main window" or "controller" indicating what's currently under the mouse so you can update your caption, legend box or tool palette.
For best speed, render only the portion of the widget you're interested in into a QPaintDevice (like a QPixmap). Try something like this:
void Breadboard::mouseMoveEvent(QMouseEvent *e)
{
// Just 1 pixel.
QPixmap pixmap(1, 1);
// Target coordinates inside the pixmap where drawing should start.
QPoint targetPos(0, 0);
// Source area inside the widget that should be rendered.
QRegion sourceArea( /* use appropriate coordinates from the mouse event */ );
// Render it.
this->render(&pixmap, targetPos, sourceArea, /* look into what flags you need */);
// Do whatever else you need to extract the color from the 1 pixel pixmap.
}
Mat's answer is better if you're willing to refactor your application to use the graphics view API.

QGraphicsItem drawing problem

I am working on a small Tower Defense game in order to learn Qt. I am using a QGraphicsScene to hold all the object of the game. To make them move, I am not using the Animation framework but I'm calling the advance() method along with a QTimer.
I'd like to make my projectiles explode when they hit an enemy. The problem is that when I'm trying to draw an ellipse to simulate the explosion, it doesn't get drawn correctly.
You can see the problem in this video.
I tried to play with the z-indexes but it didn't change anything.
Here's the code I use to draw the projectile :
void Projectile::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
if(!isExploding) {
painter->drawPixmap(boundingRect().toRect(), image);
} else {
if(cnt < 50) {
painter->setBrush(QBrush(explosion));
painter->drawEllipse(-cnt, -cnt, 2.0*cnt, 2.0*cnt);
cnt++;
} else {
this->isFinished = 1;
}
}
}
QRectF Projectile::boundingRect() const
{
// Taille de l'image de l'insecte
return QRectF(0, 0, 6, 6);
}
Do you have any clues on how to solve this problem?
Thank you.
Say cnt is 3.
You are drawing an ellipse with
painter->drawEllipse(-3,-3,6,6)
This would require a boundingRect with at least a width and height of 9.
Also the boundingRect is specified using the internal item coordinate system. You are drawing from (-3,-3) to (6,6) which is outside of the boundingRect.

QGraphicsView ensureVisible() and centerOn()

I am going to do pan/scale stuff on QGraphicsView.
So I read the documentation of QGraphicsView and see some utility functions like ensureVisible() and centerOn().
I think I understand what the documentation says but I can' t manage to write a working example.
Could you please write/suggest me an example code to understand the issue.
Ton pan the view by a certain amount (for example in your view's mouseMoveEvent()), assuming MyView is a subclass of QGraphicsView (all the following code was ported from Python, I didn't test it):
void MyView::moveBy(QPoint &delta)
{
QScrollBar *horiz_scroll = horizontalScrollBar();
QScrollBar *vert_scroll = verticalScrollBar();
horiz_scroll->setValue(horiz_scroll.value() - delta.x());
vert_scroll->setValue(vert_scroll.value() - delta.y());
}
To fit a rectangle specified in scene coordinates by zooming and panning:
void MyView::fit(QRectF &rect)
{
setSceneRect(rect);
fitInView(rect, Qt::KeepAspectRatio);
}
Note that if your scene contains non transformable items (with the QGraphicsItem::ItemIgnoresTransformations flag set), you'll have to take extra steps to compute their correct bounding box:
/**
* Compute the bounding box of an item in scene space, handling non
* transformable items.
*/
QRectF sceneBbox(QGraphicsItem *item, QGraphicsItemView *view=NULL)
{
QRectF bbox = item->boundingRect();
QTransform vp_trans, item_to_vp_trans;
if (!(item->flags() & QGraphicsItem::ItemIgnoresTransformations)) {
// Normal item, simply map its bounding box to scene space
bbox = item->mapRectToScene(bbox);
} else {
// Item with the ItemIgnoresTransformations flag, need to compute its
// bounding box with deviceTransform()
if (view) {
vp_trans = view->viewportTransform();
} else {
vp_trans = QTransform();
}
item_to_vp_trans = item->deviceTransform(vp_trans);
// Map bbox to viewport space
bbox = item_to_vp_trans.mapRect(bbox);
// Map bbox back to scene space
bbox = vp_trans.inverted().mapRect(bbox);
}
return bbox;
}
In that case the bounding rect of your objects becomes dependent on the view's zoom level, meaning that sometimes MyView::fit() won't fit exactly your objects (for example when fitting a selection of objects from a largely zoomed out view). A quick and dirty solution is to call MyView::fit() repeatedly until the bounding rect naturally "stabilizes" itself.