diff --git a/examples/desert.tmx b/examples/desert.tmx index 9f311c5b1e..a20dcee80b 100644 --- a/examples/desert.tmx +++ b/examples/desert.tmx @@ -1,8 +1,6 @@ - - - + eJztmNkKwjAQRaN9cAPrAq5Yq3Xf6v9/nSM2VIbQJjEZR+nDwQZScrwztoORECLySBcIgZ7nc2y4KfyWDLx+Jb9nViNgDEwY+KioAXUgQN4+zpoCMwPmQAtoAx2CLFbA2oDEo9+hwG8DnIDtF/2K8ks086Tw2zH0uyMv7HcRr/6/EvvhnsPrsrxwX7rwU/0ODig/eV3mh3N1ld8eraWPaX6+64s9McesfrqcHfg1MpoifxcVEWjukyw+9AtFPl/I71pER3Of6j4bv7HI54s+MChhqLlPdZ/P3qMmFuo5h5NnTOhjM5tReN2yT51n5/v7J3F0vi46fk+ne7aX0i9l6If7mpufTX3f5wsqv9TAD2fJLT9VrTn7UeZnM5tR+v0LMQOHXwFnxe2/warGFRWf8QDjOLfP diff --git a/examples/desert.tsx b/examples/desert.tsx new file mode 100644 index 0000000000..e8db8e6f50 --- /dev/null +++ b/examples/desert.tsx @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libtiled/mapreader.cpp b/src/libtiled/mapreader.cpp index 712795811d..27c27b84e0 100644 --- a/src/libtiled/mapreader.cpp +++ b/src/libtiled/mapreader.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include using namespace Tiled; @@ -308,6 +309,9 @@ Tileset *MapReaderPrivate::readTileset() if (tileset && !mReadingExternalTileset) mGidMapper.insert(firstGid, tileset); + if (tileset) + tileset->calculateTerrainDistances(); + return tileset; } @@ -384,8 +388,10 @@ void MapReaderPrivate::readTilesetTerrainTypes(Tileset *tileset) const QXmlStreamAttributes atts = xml.attributes(); QString name = atts.value(QLatin1String("name")).toString(); int tile = atts.value(QLatin1String("tile")).toString().toInt(); +// int tile = atts.value(QLatin1String("color")).toString().toInt(); + QString distances = atts.value(QLatin1String("distances")).toString(); - tileset->addTerrainType(name, tile); + tileset->addTerrainType(name, tile, distances); xml.skipCurrentElement(); } diff --git a/src/libtiled/mapwriter.cpp b/src/libtiled/mapwriter.cpp index de057ffa76..f0e8f588dd 100644 --- a/src/libtiled/mapwriter.cpp +++ b/src/libtiled/mapwriter.cpp @@ -206,6 +206,19 @@ static QString makeTerrainAttribute(const Tile *tile) return terrain; } +static QString makeTransitionDistanceAttribute(const TerrainType *t, int numTerains) +{ + QString distance; + for (int i = -1; i < numTerains; ++i ) { + if (i > -1) + distance += QLatin1String(","); + int d = t->transitionDistance(i); + if (d > -1) + distance += QString::number(d); + } + return distance; +} + void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset *tileset, uint firstGid) { @@ -279,8 +292,10 @@ void MapWriterPrivate::writeTileset(QXmlStreamWriter &w, const Tileset *tileset, TerrainType* tt = tileset->terrainType(i); w.writeStartElement(QLatin1String("terrain")); w.writeAttribute(QLatin1String("name"), tt->name()); -// w.writeAttribute(QLatin1String("color"), tt->color()); +// w.writeAttribute(QLatin1String("color"), tt->color()); w.writeAttribute(QLatin1String("tile"), QString::number(tt->paletteImageTile())); + if (tt->hasTransitionDistances()) + w.writeAttribute(QLatin1String("distances"), makeTransitionDistanceAttribute(tt, tileset->terrainTypeCount())); w.writeEndElement(); } w.writeEndElement(); diff --git a/src/libtiled/tileset.cpp b/src/libtiled/tileset.cpp index 54e71e9236..ccd4a8c739 100644 --- a/src/libtiled/tileset.cpp +++ b/src/libtiled/tileset.cpp @@ -112,3 +112,121 @@ int Tileset::columnCountForWidth(int width) const Q_ASSERT(mTileWidth > 0); return (width - mMargin + mTileSpacing) / (mTileWidth + mTileSpacing); } + +void Tileset::addTerrainType(QString name, int imageTile, QString distances) +{ + TerrainType *tt = new TerrainType(mTerrainTypes.size(), this, name, imageTile); + + if (!distances.isEmpty()) { + QStringList distStrings = distances.split(QLatin1Char(',')); + QVector dist(distStrings.size(), -1); + for (int i = 0; i < distStrings.size(); ++i) { + if (!distStrings[i].isEmpty()) + dist[i] = distStrings[i].toInt(); + } + tt->setTransitionDistances(dist); + } + + mTerrainTypes.push_back(tt); +} + +int Tileset::terrainTransitionPenalty(int terrainType0, int terrainType1) +{ + terrainType0 = terrainType0 == 255 ? -1 : terrainType0; + terrainType1 = terrainType1 == 255 ? -1 : terrainType1; + + // Do some magic, since we don't have a transition array for no-terrain + if (terrainType0 == -1 && terrainType1 == -1) + return 0; + if (terrainType0 == -1) + return mTerrainTypes[terrainType1]->transitionDistance(terrainType0); + return mTerrainTypes[terrainType0]->transitionDistance(terrainType1); +} + +void Tileset::calculateTerrainDistances() +{ + // some fancy macros which can search for a value in each byte of a word simultaneously + #define hasZeroByte(dword) (((dword) - 0x01010101UL) & ~(dword) & 0x80808080UL) + #define hasByteEqualTo(dword, value) (hasZeroByte((dword) ^ (~0UL/255 * (value)))) + + // Calculate terrain distances if they are not already present... + // Terrain distances are the number of transitions required before one terrain may meet another + // Terrains that have no transition path have a distance of -1 + + for (int i = 0; i < terrainTypeCount(); ++i) { + TerrainType *type = terrainType(i); + if (type->hasTransitionDistances()) + continue; + + QVector distance(terrainTypeCount() + 1, -1); + + // Check all tiles for transitions to other terrain types + for (int j = 0; j < tileCount(); ++j) { + Tile *t = tileAt(j); + + if (!hasByteEqualTo(t->terrain(), i)) + continue; + + // This tile has transitions, add the transitions as neightbours (distance 1) + int tl = t->cornerTerrainType(0); + int tr = t->cornerTerrainType(1); + int bl = t->cornerTerrainType(2); + int br = t->cornerTerrainType(3); + + // Terrain on diagonally opposite corners are not actually a neighbour + if (tl == i || br == i) { + distance[tr + 1] = 1; + distance[bl + 1] = 1; + } + if (tr == i || bl == i) { + distance[tl + 1] = 1; + distance[br + 1] = 1; + } + + // terrain has at least one tile of its own type + distance[i + 1] = 0; + } + + type->setTransitionDistances(distance); + } + + // Calculate indirect transition distances + bool bNewConnections; + do { + bNewConnections = false; + + // For each combination of terrain types + for (int i = 0; i < terrainTypeCount(); ++i) { + TerrainType *t0 = terrainType(i); + for (int j = 0; j < terrainTypeCount(); ++j) { + if (i == j) + continue; + TerrainType *t1 = terrainType(j); + + // Scan through each terrain type, and see if we have any in common + for (int t = -1; t < terrainTypeCount(); ++t) { + int d0 = t0->transitionDistance(t); + int d1 = t1->transitionDistance(t); + if (d0 == -1 || d1 == -1) + continue; + + // We have cound a common connection + int d = t0->transitionDistance(j); + Q_ASSERT(t1->transitionDistance(i) == d); + + // If the new path is shorter, record the new distance + if (d == -1 || d0 + d1 < d) { + d = d0 + d1; + t0->setTransitionDistance(j, d); + t1->setTransitionDistance(i, d); + + // We're making progress, flag for another iteration... + bNewConnections = true; + } + } + } + } + + // Repeat while we are still making new connections (could take a number of iterations for distant terrains to connect) + } while (bNewConnections); +} diff --git a/src/libtiled/tileset.h b/src/libtiled/tileset.h index 1ead0b5ca1..d29de5c19c 100644 --- a/src/libtiled/tileset.h +++ b/src/libtiled/tileset.h @@ -34,6 +34,7 @@ #include #include +#include #include #include @@ -42,25 +43,70 @@ class QImage; namespace Tiled { class Tile; +class Tileset; class TerrainType { public: - TerrainType(int id, QString name, int imageTile): + TerrainType(int id, Tileset *tileset, QString name, int imageTile): mId(id), + mTileset(tileset), mName(name), mImageTile(imageTile) { } + /** + * Returns ID of this tile terrain type. + */ int id() const { return mId; } + + /** + * Returns the tileset this terrain type belongs to. + */ + Tileset *tileset() const { return mTileset; } + + /** + * Returns the name of this terrain type. + */ QString name() const { return mName; } + + /** + * Returns a tile index that represents this terrain type in the terrain palette. + */ int paletteImageTile() const { return mImageTile; } + /** + * Returns a Tile that represents this terrain type in the terrain palette. + */ +// Tile *paletteImage() const { return mTileset->tileAt(mImageTile); } + + /** + * Returns true if this terrain type already has transition distances calculated. + */ + bool hasTransitionDistances() const { return !mTransitionDistance.isEmpty(); } + + /** + * Returns the transition penalty(/distance) from this terrain type to another terrain type. + */ + int transitionDistance(int targetTerrainType) const { return mTransitionDistance[targetTerrainType + 1]; } + + /** + * Sets the transition penalty(/distance) from this terrain type to another terrain type. + */ + void setTransitionDistance(int targetTerrainType, int distance) { mTransitionDistance[targetTerrainType + 1] = distance; } + + /** + * Returns the array of terrain penalties(/distances). + */ + void setTransitionDistances(QVector &transitionDistances) { mTransitionDistance = transitionDistances; } + private: int mId; + Tileset *mTileset; QString mName; int mImageTile; + QVector mTransitionDistance; }; /** @@ -246,7 +292,17 @@ class TILEDSHARED_EXPORT Tileset : public Object /** * Add a new terrain type. */ - void addTerrainType(QString name, int imageTile); + void addTerrainType(QString name, int imageTile, QString distances); + + /** + * Calculates the transition distance matrix for all terrain types. + */ + void calculateTerrainDistances(); + + /** + * Returns the transition penalty(/distance) between 2 terrains. -1 if no transition is possible. + */ + int terrainTransitionPenalty(int terrainType0, int terrainType1); private: QString mName; diff --git a/src/tiled/terrainbrush.cpp b/src/tiled/terrainbrush.cpp index 8743eb989e..6f7c34b353 100644 --- a/src/tiled/terrainbrush.cpp +++ b/src/tiled/terrainbrush.cpp @@ -200,10 +200,6 @@ void TerrainBrush::mapDocumentChanged(MapDocument *oldDocument, // Reset the brush, since it probably became invalid brushItem()->setTileRegion(QRegion()); setTerrain(NULL); - - // hack - Tileset *t = newDocument->map()->tilesets().at(0); - setTerrain(t->terrainType(2)); } void TerrainBrush::setTerrain(TerrainType *terrain) @@ -271,17 +267,14 @@ static inline unsigned int makeTerrain(int tl, int tr, int bl, int br) return (tl & 0xFF) << 24 | (tr & 0xFF) << 16 | (bl & 0xFF) << 8 | (br & 0xFF); } -Tile *TerrainBrush::findBestTile(unsigned int terrain, unsigned int considerationMask) +Tile *TerrainBrush::findBestTile(Tileset *tileset, unsigned int terrain, unsigned int considerationMask) { // if all quadrants are set to 'no terrain', then the 'empty' tile is the only choice we can deduce if (terrain == 0xFFFFFFFF) return NULL; QList matches; - int confidence = 0; - - // TODO: should we scan each tileset? we really need to use gId's for terrains aswell as tiles i guess... - Tileset *tileset = mapDocument()->map()->tilesets().at(0); + int penalty = INT_MAX; int tileCount = tileset->tileCount(); for (int i = 0; i < tileCount; ++i) { @@ -289,21 +282,16 @@ Tile *TerrainBrush::findBestTile(unsigned int terrain, unsigned int consideratio if ((t->terrain() & considerationMask) != (terrain & considerationMask)) continue; - // prefer tiles with the most possible matches to the requested terrain - int matchingQuadrants = 0; - if ((t->terrain() & 0xFF000000) == (terrain & 0xFF000000)) - ++matchingQuadrants; - if ((t->terrain() & 0xFF0000) == (terrain & 0xFF0000)) - ++matchingQuadrants; - if ((t->terrain() & 0xFF00) == (terrain & 0xFF00)) - ++matchingQuadrants; - if ((t->terrain() & 0xFF) == (terrain & 0xFF)) - ++matchingQuadrants; - - if (matchingQuadrants >= confidence) { - if (matchingQuadrants > confidence) + // calculate the tile transition penalty + int transitionPenalty = tileset->terrainTransitionPenalty(t->terrain() >> 24, terrain >> 24); + transitionPenalty += tileset->terrainTransitionPenalty((t->terrain() >> 16) & 0xFF, (terrain >> 16) & 0xFF); + transitionPenalty += tileset->terrainTransitionPenalty((t->terrain() >> 8) & 0xFF, (terrain >> 8) & 0xFF); + transitionPenalty += tileset->terrainTransitionPenalty(t->terrain() & 0xFF, terrain & 0xFF); + + if (transitionPenalty <= penalty) { + if (transitionPenalty < penalty) matches.clear(); - confidence = matchingQuadrants; + penalty = transitionPenalty; matches.push_back(t); } @@ -318,7 +306,7 @@ Tile *TerrainBrush::findBestTile(unsigned int terrain, unsigned int consideratio void TerrainBrush::updateBrush(const QPoint &cursorPos, const QVector *list) { - // get the current tile layer (TODO: what if the current layer isn't a tile layer?) + // get the current tile layer TileLayer *currentLayer = currentTileLayer(); Q_ASSERT(currentLayer); @@ -329,6 +317,9 @@ void TerrainBrush::updateBrush(const QPoint &cursorPos, const QVector *l if (!currentLayer->bounds().contains(cursorPos)) return; + // TODO: this seems like a problem... there's nothing to say that 2 adjacent tiles are from the same tileset, or have any relation to eachother... + Tileset *tileset = mTerrain ? mTerrain->tileset() : NULL; + // allocate a buffer to build the terrain tilemap (TODO: this could be retained per layer to save regular allocation) Tile **newTerrain = new Tile*[numTiles]; @@ -367,6 +358,9 @@ void TerrainBrush::updateBrush(const QPoint &cursorPos, const QVector *l // get the relevant tiles const Tile *tile = currentLayer->cellAt(p).tile; + if (!tileset && tile) + tileset = tile->tileset(); + Tile *paste = NULL; // find a tile that best suits this position @@ -374,7 +368,7 @@ void TerrainBrush::updateBrush(const QPoint &cursorPos, const QVector *l // the first tiles are special, we will just paste the selected terrain and add the surroundings for consideration // TODO: if we're painting quadrants rather than full tiles, we need to set the appropriate mask - paste = mTerrain ? findBestTile(makeTerrain(mTerrain->id()), 0xFFFFFFFF) : NULL; + paste = mTerrain ? findBestTile(tileset, makeTerrain(mTerrain->id()), 0xFFFFFFFF) : NULL; --initialTiles; } else { // following tiles each need consideration against their surroundings @@ -399,7 +393,9 @@ void TerrainBrush::updateBrush(const QPoint &cursorPos, const QVector *l mask |= 0x00FF00FF; } - paste = findBestTile(preferredTerrain, mask); + paste = findBestTile(tileset, preferredTerrain, mask); + if (!paste) + continue; } // add tile to the brush diff --git a/src/tiled/terrainbrush.h b/src/tiled/terrainbrush.h index c443d61d48..b7d5089623 100644 --- a/src/tiled/terrainbrush.h +++ b/src/tiled/terrainbrush.h @@ -92,7 +92,7 @@ class TerrainBrush : public AbstractTileTool void capture(); - Tile *findBestTile(unsigned int terrain, unsigned int considerationMask); + Tile *findBestTile(Tileset *tileset, unsigned int terrain, unsigned int considerationMask); /** * updates the brush given new coordinates.