An improved tiled scene implementation
-
Qt3 had some convenience functions to implement scenes with tiles, like the JavaME 'Tiled Layer' functionality. In "porting guide(porting guide)":http://doc.qt.nokia.com/4.6/graphicsview-porting.html#porting-scenes-with-tiles to Qt4 there is a simple example showing how to achieve the same with QGraphicsView framework. The example can be enhanced and optimized in many ways though and one way is to utilize bit shifting operations and minimal repaint. Assume you have an image including a number of 64x64 pixel tiles to build the scene with. Arrange the tiles in the image to 8 tiles per column (or any other ^2 value, but then the bitvalue in the example must be changed) and define the bit shifting values. Also I prefer a QList instead of QVector. I wonder if an integer array would be faster if the scene size doesn't change.
@class TileScene : public QGraphicsScene
{
public:
enum { PIXMAP_TILECOLUMNS_BITS = 3, PIXMAP_TILECOLUMNS_MASK = 7, PIXMAP_TILECOLUMNS = 8 };
enum { TILESIZE_BITS = 6, TILESIZE = 64 };// ...
private:
QPixmap tilesPixmap;
int mapColumns;
int mapRows;
QList<int> tilesMap;@Then populate that tilesMap in a suitable way (remember that it is now 1-dimensional, so count row*mapColumn + column to find the right spot) and finally paint it with drawBackground as normal
@void TileScene::drawBackground(QPainter *painter, const QRectF ▭)
{
int startColumn = (rect.x() > 0) ? int(rect.x()) >> TILESIZE_BITS : 0;
int startRow = (rect.y() > 0) ? int(rect.y()) >> TILESIZE_BITS : 0;
int endColumn = qMin(int(rect.right()) >> TILESIZE_BITS, mapColumns-1);
int endRow = qMin(int(rect.bottom()) >> TILESIZE_BITS, mapRows-1);
int rowOffset = startRow * mapRows;
int tileNumber;for (int row = startRow; row <= endRow; ++row) { for (int column = startColumn; column <= endColumn; ++column) { tileNumber = tilesMap.at(rowOffset + column); painter->drawPixmap(QRect(column << TILESIZE_BITS, row << TILESIZE_BITS, TILESIZE, TILESIZE), tilesPixmap, QRect((tileNumber & PIXMAP_TILECOLUMNS_MASK) << TILESIZE_BITS, (tileNumber >> PIXMAP_TILECOLUMNS_BITS) << TILESIZE_BITS, TILESIZE, TILESIZE)); } rowOffset += mapRows; }
}@
You may also want to turn on setCacheMode(QGraphicsView::CacheBackground); in the actual view class. Notice that this implements static backgrounds only. If you want animation, you need to change the content of cells regularly and either update the whole screen or a bigger part of it. But the bit shifting tricks remain the same.
If you have comments or improvements please share!
-
This is a grate section thank You for making it.
I am trying to develop a mobile game using Qt and I have implemented the same thing in a slightly different way.
@class GameScene : public QGraphicsScene
{
//...
quint8 mySize;
Env::Type myEnv;
QPixmap* scene;
quint8** obsticleMap;
quint8** florMap;
};@@void GameScene::drawBackground(QPainter* painter, const QRectF&)
{
if(scene == NULL)
renderBackGround();
painter->setPen(Qt::transparent);
painter->drawPixmap(0,0,scene);
}
void GameScene::renderBackGround()
{
delete scene;
scene = new QPixmap(30 + (mySize * 50),30 + (mySize * 50));
QPainter painter(scene);
painter.setPen(Qt::transparent);
painter.setBrush(QBrush(QPixmap(LookFactory::getBackground(myEnv))));
painter.drawRect(QRect(0,0,scene->width(),15));
painter.drawRect(QRect(0,scene->height() - 15 ,scene->width(),15));
painter.drawRect(QRect(0,0,15,scene->height()));
painter.drawRect(QRect(scene->width() - 15,0,15,scene->height()));
painter.fillRect(QRect(15,15,scene->width() - 30, scene->height() - 30),
QBrush(QPixmap(LookFactory::getGround(myEnv))));
for(quint8 i = 0; i < mySize; ++i)
{
for(quint8 j = 0; j < mySize; ++j)
{
if(florMap[i][j] > 0)
painter.drawPixmap(15 + (i * 50),15 + (j50),
LookFactory::getFlor(florMap[i][j],myEnv));
}
}
for(quint8 i = 0; i < mySize; ++i)
{
for(quint8 j = 0; j < mySize; ++j)
{
if(obsticleMap[i][j] > 18 && obsticleMap[i][j] < 36)
painter.drawPixmap(15 + (i * 50),15 + (j*50),
LookFactory::getObsticle(obsticleMap[i][j],myEnv));
}
}
painter.setPen(Qt::black);
painter.setBrush(QBrush(Qt::transparent));
painter.drawRect(15,15,scene->width() - 30, scene->height() - 30);
}
@ -
Interesting, I wasn't aware Qt 3 had this feature and the code in the porting guide.
As a hobby I've done some extensive work on a free generic tile-based map editor called Tiled. The latest version is Qt based, and as such also contains fast (as far as possible with QPainter::drawPixmap) tile map rendering code. The trick is indeed to only draw the tiles within the exposed rect.
My code looks like this:
"http://gitorious.org/tiled/tiled-qt/blobs/4498bb0/src/orthogonalrenderer.cpp#line109(orthogonalrenderer.cpp)":http://gitorious.org/tiled/tiled-qt/blobs/4498bb0/src/orthogonalrenderer.cpp#line109
I didn't resort to bit shifting there, which isn't really possible if you support arbitrary tile sizes. But I don't believe an integer division would be significantly slower. In the end, this is done once every repaint, and the actual drawing of the pixmaps will always take most of the time.
The code is somewhat abstracted in OrthogonalRenderer, which implements a MapRenderer interface. This allows the editor to support both orthogonal and isometric projections, with potentially more coming in the future.
Eventually I hope to expose the core of the editor, including the map loader and renderer, as a separate library, so that it can be easily plugged into another project.
Tiled website: "http://www.mapeditor.org/(Tiled Map Editor)":http://www.mapeditor.org/
@kkrzewniak: Your method should be the fastest, but is only suitable for drawing small maps (otherwise memory usage will become a problem) and most effective when the scene is often repainted entirely.
-
Actually the code in my example has elements learned from your Qt Tiled version 0.3, especially regarding the exposed tile calculations. I just combined that with compact tile mapping and bitshifting and other micro optimizations (e.g. x % 8 -> x & 7, preferring addition & substraction calculations and less function calls in general). Many will consider those unnecessary, but since I came from JavaME world, I learned there to try to squeeze everything out when possible with mobiles in mind. But enabling the optimization flags and disabling item indexing (in suitable setups) is far more important. A nice way to see how well a tiled background renders is to enable ScrollHandDrag and make a map larger than the screen. Then compile it to a mobile and try again.
@kkrzewniak: Your method actually resembles the way it is done in JavaME (MIDP2). There everything is painted in an offscreen buffer first and then just flushed on the screen. The offscreen buffer doesn't need to be wiped away if not scrolling around. It would be nice to measure how that technique actually compares to the exposed rect example in performance. However, a full screen pixmap may become quite large. Are you repainting the whole background pixmap if you scroll in some direction? Regarding the exposed rect method above, Qt caches that part of the background that remains visible in a scroll and therefore the exposed rectangle that needs to be repainted can be small.
An excellent article of painting speed comparisons written by gunnar can be found "here(here)":http://labs.trolltech.com/blogs/2010/01/11/qt-graphics-and-performance-the-cost-of-convenience/
-
gunnar has written several excellent articles about QGraphicsView and performance. Related to the topic e.g. "Qt Graphics and Performance - An Overview(Qt Graphics and Performance - An Overview)":http://labs.trolltech.com/blogs/2009/12/16/qt-graphics-and-performance-an-overview/ and "Qt Graphics and Performance - The Raster Engine(Qt Graphics and Performance - The Raster Engine)":http://labs.trolltech.com/blogs/2009/12/18/qt-graphics-and-performance-the-raster-engine/.