Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draw isometric maps #1529

Closed
wants to merge 3 commits into from

Conversation

@mitchcurtis
Copy link
Contributor

commented Apr 10, 2017

No description provided.

@bjorn

This comment has been minimized.

Copy link
Owner

commented Apr 10, 2017

This is nice! But I think there are some problems:

  • To match the rendering order in Tiled, an isometric map needs to be iterated horizontally (so y+1 and x-1 for each step). You can see this in the isometricrenderer.cpp. While rare, iterating differently will affect some maps.

  • The biggest problem: the logic for "visible tiles" doesn't work for isometric layers. mVisibleTiles is calculated in a way that only makes sense for tile layers that are somewhat orthogonal (you probably didn't test with a large enough map to notice this?). I think the general solution will be to set up batches and determine which ones are visible, but for now if the map is isometric you should probably set mVisibleTiles to the entire layer.

  • It would be nice to share the common functionality for building up the TileData vector between the renderers, but that can be left as a cleanup for later.

Draw the entire layer for isometric maps
This is a temporary solution until a proper fix is made.
@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 10, 2017

That last commit addresses your second comment, still gotta address the first one.

@@ -265,7 +265,7 @@ QSGNode *TileLayerItem::updatePaintNode(QSGNode *node,
if (mLayer->map()->orientation() == Map::Orthogonal)
drawOrthogonalTileLayer(node, mapItem, mLayer, mVisibleTiles);
else
drawIsometricTileLayer(node, mapItem, mLayer, mVisibleTiles);
drawIsometricTileLayer(node, mapItem, mLayer, mLayer->bounds());

This comment has been minimized.

Copy link
@bjorn

bjorn Apr 10, 2017

Owner

You'll want to set mVisibleTiles depending on the orientation in updateVisibleTiles instead, otherwise scrolling through the map will cause it to do needless updates all the time.

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 10, 2017

To match the rendering order in Tiled, an isometric map needs to be iterated horizontally (so y+1 and x-1 for each step). You can see this in the isometricrenderer.cpp. While rare, iterating differently will affect some maps.

I don't really get the connection between "(so y+1 and x-1 for each step)" and the code in IsometricRenderer::drawTileLayer(). :p Are you saying I should start from the right edge of the rect and go to the left?

@bjorn

This comment has been minimized.

Copy link
Owner

commented Apr 10, 2017

I don't really get the connection between "(so y+1 and x-1 for each step)" and the code in IsometricRenderer::drawTileLayer(). :p Are you saying I should start from the right edge of the rect and go to the left?

The connection is here:

            // Advance to the next column
            ++columnItr.rx();
            --columnItr.ry();

The function is actually iterating over the screen positions and keeping track of this tile location separately. You don't need to do that, but the important bit is the order in which the tiles are rendered, which should be such that lower tiles are rendered later, which isn't the case when iterating an isometric map the same way as an orthogonal map.

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 11, 2017

I've been trying to prototype a solution to this in QML, but have been failing. I give up! :D

Is there any reason not to merge this as-is, though, considering that the branch is WIP? Perhaps my understanding is incorrect, but I thought that the current patch will work for most maps/tilesets, just not all of them? Visually it seems to match what I see in Tiled if I open the same map.

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 11, 2017

I found http://clintbellanger.net/articles/isometric_math/, so maybe I can figure this out... also saw clintbellanger/flare#461 by coincidence. :p

@bjorn

This comment has been minimized.

Copy link
Owner

commented Apr 11, 2017

I found http://clintbellanger.net/articles/isometric_math/, so maybe I can figure this out... also saw clintbellanger/flare#461 by coincidence. :p

That explains the issue really well, nice find!

Is there any reason not to merge this as-is, though, considering that the branch is WIP?

Not really, but I'm not in a rush. Are you? I think we have time to do this correctly. Unless you really decide to spend your time elsewhere (though hopefully still on Tiled Quick :-D), at which point I'd agree to merge this for now.

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 12, 2017

A little backstory: my goal is to render an isometric .tmx map in Qt Quick (for a game idea I'm messing around with), something I couldn't find code for, so I thought I could contribute it here and then use it in my project.

I'm not sure a Qt Quick tiled editor would have much advantage over the existing Qt Widgets one (especially since Qt Quick lacks stuff like dock windows). I do think it would be a testament to the desktop support that Qt Quick Controls 1 does have, though, and how easy it is to use that to create a desktop application. It would also be cool to see it ported to Qt Quick Controls 2 (using Qt Labs Platform, when it gets full support for menu bars, etc.) to see the difference in performance. :D

So, I'm more interested in how we could allow developers to get Tiled maps in their Qt Quick games. I think that if there was a library that offered a simple enough API around the tile rendering stuff, more people might use Qt Quick for games... and then maybe some smart person would eventually contribute a decent pathfinding library that integrates nicely with Qt Quick. :D Ok, getting a bit off topic..

What is the plan for the Qt Quick version of Tiled, by the way? Is it just an experiment, or do you think there is some other potential there?

Back to the problem at hand... I've finally come up with a solution in QML, by looking at the patterns in the tile indices (that's how someone who sucks at maths gets stuff done, haha):

import QtQuick 2.9
import QtQuick.Controls 2.2

import App 1.0

ApplicationWindow {
    id: window
    width: 1400
    height: 400
    visible: true

    property int tilesWide: 15
    property int tilesHigh: 10
    property int tileWidth: 32
    property int tileHeight: 32

    property int startX: (tilesWide * tileWidth) / 2

    function indexToMapPos(index) {
        return Qt.point(
            (index % tilesWide),
            Math.floor(index / tilesWide));
    }

    function mapToScreen(mapPos) {
        return Qt.point(
            (mapPos.x - mapPos.y) * (tileWidth / 2),
            (mapPos.x + mapPos.y) * (tileHeight / 2));
    }

    function screenToMap(screenPos) {
        return Qt.point(
            (screenPos.x / (tileWidth / 2) + screenPos.y / (tileHeight / 2)) / 2,
            (screenPos.y / (tileHeight / 2) -(screenPos.x / (tileWidth / 2))) / 2);
    }

    Repeater {
        id: orthogonalRepeater
        model: tilesWide * tilesHigh

        delegate: Rectangle {
            x: (index % tilesWide) * tileWidth
            y: Math.floor(index / tilesWide) * tileHeight
            border.width: 1
            width: tileWidth
            height: tileHeight

            property int idx: index

            Text {
                text: idx
                color: Qt.rgba(idx / orthogonalRepeater.count, 0, 0, 1)
                anchors.centerIn: parent
                font.pixelSize: 14
            }
        }
    }

    Repeater {
        id: wrongRepeater
        model: tilesWide * tilesHigh
        delegate: Rectangle {
            x: 650 + screenPos.x
            y: screenPos.y
            border.width: 1
            width: tileWidth
            height: tileHeight

            readonly property int idx: index
            readonly property point mapPos: Qt.point(index % tilesWide, Math.floor(index / tilesWide))
            readonly property point screenPos: mapToScreen(mapPos)

            Text {
                text: idx
                color: Qt.rgba(idx / wrongRepeater.count, 0, 0, 1)
                anchors.centerIn: parent
                font.pixelSize: 14
            }
        }
    }

    // https://github.com/clintbellanger/flare/issues/461
    // http://clintbellanger.net/articles/isometric_math/
    Repeater {
        id: attemptRepeater

        model: ListModel {
            //
        }

        function addTiles() {
            var tileCount = tilesWide * tilesHigh;
            var i = 0;
            var mapPos = indexToMapPos(i);
            var screenPos = mapToScreen(mapPos);
            model.append({ actualIndex: 0, tileX: screenPos.x, tileY: screenPos.y });
            var tilesCreated = 1;

            var z = 1;
            // This loop takes us down to the widest point of the "diamond".
            // For example, with a 15 x 10 map, this loop will create items
            // up to and including the row starting with index 135 (marked (a)):
            //
            //                  0
            //               15    1
            //            30   16     2
            //         ..    ..   ..    ..
            //      135        ...         9                   (a)
            //         136 122       ...  24 10
            //            137 123    ...     25 11
            //              ..       ...           ..
            //                140 126  ...       28  14        (b)
            //                   ..     ...       ..
            //                      ..   ...   ..
            //                         148  134
            //                            149
            while (z < tilesHigh) {
                i = z * tilesWide;

                do {
                    mapPos = indexToMapPos(i);
                    screenPos = mapToScreen(mapPos);
                    model.append({ actualIndex: i, tileX: screenPos.x, tileY: screenPos.y });
                    ++tilesCreated;
                    i -= tilesWide - 1;
                } while (i > 0)

                ++z;
            }

            while (z < tilesWide) {
                // We've reached the "widest" point of the "diamond"
                // ( (a) on the diagram above).
                // From now on we'll be completing the next part.
                // If the map is square, this loop will be skipped.
                // However, for the 15 x 10 example we've been using,
                // The loop will continue until we're at the last row
                // of the "widest" part ( (b) on the diagram above).
                //
                // z will be 10 the first time this code is hit, so:
                //         z - (tilesHigh - 1)
                //        10 - (    10    - 1)
                //        10 - (          9  )
                //           1
                // The first part of the calculation gets us to the first index
                // of the last "row" we were on:
                //     (tilesHigh - 1) * tilesWide
                //     (10        - 1) *     15
                //                9    *     15
                //                    135
                // 135 + 1 = 136. The index then increases by 1 from then on.
                i = ((tilesHigh - 1) * tilesWide) + (z - (tilesHigh - 1));

                do {
                    mapPos = indexToMapPos(i);
                    screenPos = mapToScreen(mapPos);
                    model.append({ actualIndex: i, tileX: screenPos.x, tileY: screenPos.y });
                    ++tilesCreated;
                    i -= tilesWide - 1;
                } while (i > 0)

                ++z;
            }

            // * 2 because we're already on the last widest row,
            // so we are starting on the one after it.
            var rowEndIndex = (tilesWide * 2) - 1;
            while (tilesCreated < tileCount) {
                i = ((tilesHigh - 1) * tilesWide) + (z - (tilesHigh - 1));

                do {
                    mapPos = indexToMapPos(i);
                    screenPos = mapToScreen(mapPos);
                    model.append({ actualIndex: i, tileX: screenPos.x, tileY: screenPos.y });
                    ++tilesCreated;
                    i -= tilesWide - 1;
                } while (i >= rowEndIndex)

                ++z;
                rowEndIndex += tilesWide;
            }
        }

        Component.onCompleted: addTiles()

        delegate: Rectangle {
            x: 1100 + tileX
            y: tileY
            border.width: 1
            width: tileWidth
            height: tileHeight

            readonly property int idx: index

            Text {
                text: actualIndex
                color: Qt.rgba(index / attemptRepeater.count, 0, 0, 1)
                anchors.centerIn: parent
                font.pixelSize: 14
            }
        }
    }
}

tile-rendering

The map on the left is orthogonal, the one in the middle is my first (incorrect) attempt at isometric rendering, and the one on the right is what I understand to be the "correct" way. Does it look right? If so, I can port it to C++.

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 12, 2017

(Note that the code needs to be fixed for the case where the map is taller than it is wide)

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 12, 2017

Now works with maps of any size, and you can interact with it:

https://gist.github.com/mitchcurtis/11bf1a079b7ad84cd55801e11b3bb359

The code is probably shit, but it works. I'm not sure how much work it would be to adapt it to work with a specific visible region of the map... but this is a start.

@mitchcurtis mitchcurtis referenced this pull request Apr 14, 2017
@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 14, 2017

Continued in #1535. I think it created a new pull request because I wiped my local tiled repo in between pushes...

@bjorn

This comment has been minimized.

Copy link
Owner

commented Apr 14, 2017

I'm not sure a Qt Quick tiled editor would have much advantage over the existing Qt Widgets one (especially since Qt Quick lacks stuff like dock windows). I do think it would be a testament to the desktop support that Qt Quick Controls 1 does have, though, and how easy it is to use that to create a desktop application. It would also be cool to see it ported to Qt Quick Controls 2 (using Qt Labs Platform, when it gets full support for menu bars, etc.) to see the difference in performance. :D

I realized when I tried to start this project years ago that Qt Quick Controls 1 were lacking a lot in terms of features and performance, and that's something which never really changed and now they're pretty much deprecated in favor of Qt Quick Controls 2, which are in turn not quite ready for use in a desktop application. Regarding the dock windows, that's something I would either try building on top of Qt Quick and/or I would try to avoid using them. I don't like the way in which they put the responsibility of creating an efficient UI layout on the user.

Actually my main problem with Qt Quick based applications is that they flicker about while resizing the window. I can't release an application that when resized will see its menu bar flickering up and down... Maybe I'm sensitive, but I think it gives a very bad impression. I'm not actually sure whether the Qt developers are aware of this. I had expected it to be high on their priority list somewhere since it is so noticeable, but they have so far not addressed this problem.

I've tried Slate now and it suffers from this and other problems regarding desktop behavior (like when you open a menu, and hover another menu, it doesn't actually open the other menu unless you click). But when such basic things were fixed, it would be quite a solid and good looking application.

So, I'm more interested in how we could allow developers to get Tiled maps in their Qt Quick games. I think that if there was a library that offered a simple enough API around the tile rendering stuff, more people might use Qt Quick for games... and then maybe some smart person would eventually contribute a decent pathfinding library that integrates nicely with Qt Quick. :D Ok, getting a bit off topic..

It would definitely be nice to provide a good Qt Quick module for working with Tiled maps. I hoped the module written for Tiled Quick would be able to suit that purpose as well. A version of this code is already used by Source of Tales, a mobile RPG I've been working on with some other people.

What is the plan for the Qt Quick version of Tiled, by the way? Is it just an experiment, or do you think there is some other potential there?

I started Tiled Quick because I hoped to be able to do a mobile / tablet version of Tiled. I actually have Tiled Quick installed on my Galaxy S3 and it can view orthogonal maps with smooth scrolling performance. Eventually I'd still like to find some time to continue with it.

Now works with maps of any size, and you can interact with it:

Yes, that is the right drawing order. I'm sure we can simplify the code. :-)

Continued in #1535. I think it created a new pull request because I wiped my local tiled repo in between pushes...

Right, don't delete your fork!

@mitchcurtis

This comment has been minimized.

Copy link
Contributor Author

commented Apr 14, 2017

I realized when I tried to start this project years ago that Qt Quick Controls 1 were lacking a lot in terms of features and performance, and that's something which never really changed and now they're pretty much deprecated in favor of Qt Quick Controls 2, which are in turn not quite ready for use in a desktop application. Regarding the dock windows, that's something I would either try building on top of Qt Quick and/or I would try to avoid using them. I don't like the way in which they put the responsibility of creating an efficient UI layout on the user.

Actually my main problem with Qt Quick based applications is that they flicker about while resizing the window. I can't release an application that when resized will see its menu bar flickering up and down... Maybe I'm sensitive, but I think it gives a very bad impression. I'm not actually sure whether the Qt developers are aware of this. I had expected it to be high on their priority list somewhere since it is so noticeable, but they have so far not addressed this problem.

Yep, I know what you're talking about, and I agree. This problem has been around for a while, and I'm not sure what's being done about it, honestly. It's a problem that's a bit outside my area of competence, to put it lightly. :D

https://bugreports.qt.io/issues/?jql=text%20~%20%22flickering%20when%20resizing%22%20ORDER%20BY%20watchers%20DESC%2C%20votes%20DESC

I've tried Slate now and it suffers from this and other problems regarding desktop behavior (like when you open a menu, and hover another menu, it doesn't actually open the other menu unless you click). But when such basic things were fixed, it would be quite a solid and good looking application.

Yeah, my usage of menus in Slate is really hacky. I'm using the non-native, Qt Quick-based Menu control, because Qt Labs Platform doesn't support menus on Windows (yet). Menu is designed for touch interfaces, so a lot of the stuff you'd expect from a Desktop menu won't work. I could have used Qt Labs Platform for the menus and menu bars, but my primary target for Slate is Windows.

It would definitely be nice to provide a good Qt Quick module for working with Tiled maps. I hoped the module written for Tiled Quick would be able to suit that purpose as well. A version of this code is already used by Source of Tales, a mobile RPG I've been working on with some other people.

The tiledquickplugin seems like it will do the job. My problem is using it in a game. The current options seem to be: fork tiled and get rid of the unnecessary stuff, but be left with a huge burden should you ever need to "pull" in updates, or have it as a "3rdparty" submodule, with some commits on top to remove the unnecessary stuff (which is what I'd go for).

How do you want to see it done? How do you do it with Source of Tales?

I started Tiled Quick because I hoped to be able to do a mobile / tablet version of Tiled. I actually have Tiled Quick installed on my Galaxy S3 and it can view orthogonal maps with smooth scrolling performance. Eventually I'd still like to find some time to continue with it.

Ah, cool. :)

@bjorn

This comment has been minimized.

Copy link
Owner

commented Apr 18, 2017

How do you want to see it done? How do you do it with Source of Tales?

I copy code over from one repo to another. It makes sense in the current state, since the code size is small and it's still very much work in progress. There are also differences that are not handled in a way that would currently allow the code to be the same, like tiledquickplugin assumes local files whereas in Source of Tales, the similar classes are loading the resources through QNetworkAccessManager.

The goal in the end would probably be to have a separate git repository for the plugin and to use it in the game and in Tiled Quick, but at the moment I feel like that possibility is still quite far away. Although, I've never really felt like it would benefit anyone if I moved for example libtiled into its own repository. Source of Tales again has a copy of it, but it has some modifications related to loading resources from the network as well. And having it inside the Tiled repository means I don't run into problems when changing the library in some way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.