bitcraft / pytmx

Python library to read Tiled Map Editor's TMX maps.
GNU Lesser General Public License v3.0
391 stars 83 forks source link

Kivy support #59

Open evbo opened 9 years ago

evbo commented 9 years ago

When displaying tiles in OpenGL formatted coordinate systems (y=0 occurs at bottom of page instead of top of page), there is a simple transformation needed to ensure data appearing at the top of the page has coordinates starting from max(y) rather than 0. Would it make sense to incorporate an image loader that handles this simple, yet common transformation?

See below where I had to subclass TiledMap and override the reload_images method just to set the image_loader to one that sets the y ordinate based on the current tileset's tileheight and height. Is there a cleaner, more future-version-compatible-robust way for loading images with correct coordinates for OpenGL displays?

class OpenGLTiledMap(TiledMap):

    def reload_images(self):

        def GL_image_loader(filename, flags, **kwargs):
            """ convert the y coordinate to opengl format where
                the bottom-most coordinate in the image is 0
            """
            ts = self.fname2ts[filename]
            def load((x,y,w,h)=None, flags=None):
                GL_y = ts.height - ts.tileheight - y
                return filename, (x,GL_y,w,h), flags

            return load

        self.fname2ts = dict((ts.source, ts) for ts in self.tilesets)
        self.image_loader = GL_image_loader
        super(OpenGLTiledMap, self).reload_images()

Also, this issue comes up again after you've loaded the images from the tileset and now need to play them as widgets in your OpenGL display window. Again, the conversion is needing to know tileset specific dimensions. Is there a convenient way to look this up?

For instance, I do something kinda ugly like this:

layer = self.map.get_layer_by_name('ground')
max_y = len(layer.data)
for (x, y, ts_props) in layer.tiles():
    # correct the y value for openGL displays
    y = max_y - y
bitcraft commented 9 years ago

Thanks for your interest with pytmx. As far as inverting the y-coordinate, there is a way to do that already (has partial support). Pass 'invert_y=True' to whatever loader or to the TiledMap constructor. That value will be saved in the TiledMap instance. I think the proper solution would be to modify TiledTileLayer.parse_xml to be inverted_axis aware. That would be somewhere around here: https://github.com/bitcraft/PyTMX/blob/master/pytmx/pytmx.py#L916 It would remove the need for a special loader and all data access would be flipped. Of course, then the data in memory will be flipped as well. Thoughts?

    tmx_data = pytmx.TiledMap(..., invert_y=True)

As far as image loading goes, there is another approach to image loading that you may be interested in. You can define a function to load images, without resorting to subclassing...pass an argument to the TiledMap constructor. Please see the link below for more information: https://github.com/bitcraft/pytmx#custom-image-loading

What graphics library are you using now? ...just curious.

evbo commented 9 years ago

thanks, the built-in invert_y argument looks promising and it appears to only need a simple fix:

line 316 - 317 of pytmx.py:

if self.invert_y:
    o.y -= tileset.tileheight

should be:

if self.invert_y:
    o.y = tileset.height - tileset.tileheight - o.y

However, this code never actually runs for me because self.objects is empty since my Tiled tmx file does not contain objectgroups.

In otherwords, for me self.objects is empty since none of my layers are the expected instance on line 629:

if isinstance(layer, TiledObjectGroup)) 

Also, I am already using the image_loader approach you recommend. See my original comment, with the only difference being that I subclass in order to pass the current tileset. It is necessary to know the current tileset in order to get height and tileheight information. I need to subclass so that reload_images passes the current tileset to the image_loader function.

bitcraft commented 9 years ago

I guess I'm confused here. Does your graphics lib require a flipped tileset image? Could you tell me what graphics lib you are using? That would help me out a lot here. Any code that you've already created would be nice, too. I'd like to see what you are doing, and how I can make pytmx better with your contributions. Thanks.

evbo commented 9 years ago

Sorry, forgot to mention I am using the Python framework Kivy. It displays content assuming y=0 is at the bottom of the page (hence why I must change content at the top of the page to have y>0)

bitcraft commented 9 years ago

Awesome! I'm a big fan of kivy. I've gotten a few requests for ivy support now, I suppose I should do something about it. It would be a great help if you could send your changes in a PR (or diff/paste), so I have somewhere to start.

evbo commented 9 years ago

Sorry, I'm not too familiar with git hub but here is a working example with Kivy and Tiled tmx:

from kivy.uix.widget import Widget

from kivy.properties import StringProperty

from kivy.core.image import Image as CoreImage
from kivy.uix.image import Image

from pytmx import TiledMap

class TileGrid(Widget):
    """Creates a Kivy grid and puts the tiles in a KivyTiledMap in it."""
    map_file = StringProperty('path\to\tmx.tmx')

    def __init__(self, **kwargs):

        self.map = OpenGLTiledMap(self.map_file)
        # self.map = TiledMap(self.map_file, invert_y=True)

        super(TileGrid, self).__init__(
            rows=self.map.height, cols=self.map.width,
            row_force_default=True,
            row_default_height=self.map.tileheight,
            col_force_default=True,
            col_default_width=self.map.tilewidth,
            **kwargs
        )

        layer = self.map.get_layer_by_name('ground')
        max_y = len(layer.data)
        for (x, y, ts_props) in layer.tiles():

            tileset_fname = ts_props[0]
            (tx, ty, tw, th) = ts_props[1]

            texture = CoreImage(tileset_fname).texture.get_region(
                tx, ty, tw, th)

            y = max_y - y

            pos = (x*tw, y*th)
            image = Image(  texture=texture, 
                            texture_size=(tw, th),
                            pos=pos)

            self.add_widget(image)

class OpenGLTiledMap(TiledMap):

    def reload_images(self):

        def GL_image_loader(filename, flags, **kwargs):
            """ convert the y coordinate to opengl format where
                the bottom-most coordinate in the image is 0
            """
            ts = self.fname2ts[filename]
            def load((x,y,w,h)=None, flags=None):
                GL_y = ts.height - ts.tileheight - y
                return filename, (x,GL_y,w,h), flags

            return load

        self.fname2ts = dict((ts.source, ts) for ts in self.tilesets)
        self.image_loader = GL_image_loader
        super(OpenGLTiledMap, self).reload_images()

from kivy.app import App

from kivy.base import EventLoop
EventLoop.ensure_window()

class MyApp(App):
    def build(self):
        layout = TileGrid()
        return layout

MyApp().run()
bitcraft commented 9 years ago

No worries! Thanks so much for sharing that. I will take a look at it later today.

labuzm commented 8 years ago

Any news on this?

labuzm commented 8 years ago

How about simply passing tileset object into image_loader? (see #72). This approach still requires manually converting y axis coordinate inside the loader, but it provides all necessary data required to do the math. You can use it this way: https://gist.github.com/labuzm/6ee08adfa64e6dc6e4e0 This also wouldn't break anything.

bitcraft commented 8 years ago

Iabuzm, I've been busy with other projects at the moment, but I will look into your proposal. Thanks for your suggestions, I will check back when I get time.

ghost commented 3 years ago

helo i am on a project and i need to use pytmx i need pytmx support for kivy today itself plz

bitcraft commented 3 years ago

Hi thanks for your interest. I haven’t worked with kivy compatibility lately. Have you tried the code that is in this thread?

ghost commented 3 years ago

yes but i am getting some errors in this part of the code

def load((x,y,w,h)=None, flags=None):

ghost commented 3 years ago

can you give me a clean code from that

ghost commented 3 years ago

when i am using opengltilemap class its working fine

2

but when i am using your code
self.map = TiledMap(self.map_file, invert_y=True) i am getting the image Capture

its displaying another image in my directorythat i used in that tmx file

ghost commented 3 years ago

i am not giving up on pytmx because i love pytmx with pygame

ghost commented 3 years ago

and the first image is correct i made i test tmx it worked with the opengltiled class in the thread

but not working with your way of coding i love that neet

bythaby this is the only image i am using tiles

ghost commented 3 years ago

and i am a student at 13 years sorry to ask this much questions wasting your time i would like to know how to scale our scrolling tmx file accourding to the window

bitcraft commented 3 years ago

looks pretty good so far. one thing i notice is that the images don't seem to align with the tiles. that looks like the margin or spacing of the tileset isn't set or being used properly. if you can share the map and code you are using, i could help you get this fixed. as for scrolling, you would have to rely on Kivy to do that...its been a long time since i've used it so i don't have a good idea for that right now.