Pytmx rendering animated tiles from Tiled map

977 views Asked by At

So I have been working on a project for some time now and I realy wanted to get animated tiles in to the game. Im creating a 2d pixel art styled game with pygame and Im using the an editor called Tiled to create the map. Tiled generates a .tmx file as well as a .tsx file to be used to render the map. I have gotten the map to render without any problems. The problems comes with rendering animated tiles. They just dont get animated. I understand the basics of how the animation works. I just need to get the first image of the animation, wait the duration between frames and then render the next frame. But I just cant figure out how to get it working. There is minimal documentation of pytmx and how it reads animations from Tiled files.

This is the .tmx file:

<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.3.4" orientation="orthogonal" renderorder="right-down" width="32" height="32" tilewidth="96" tileheight="96" infinite="0" nextlayerid="5" nextobjectid="5">
 <tileset firstgid="1" source="Bigger-Textures(96x96).tsx"/>
 <layer id="1" name="Tile Layer 1" width="32" height="32">
  <data encoding="csv">
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,4,6,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,13,15,1,1,1,1,1,1,12,12,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,13,15,1,1,1,1,1,1,11,12,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,13,25,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,22,23,23,23,23,23,23,23,9,7,23,23,23,23,23,23,24,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,13,15,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,13,15,1,1,1,1,12,3,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,12,12,1,4,5,5,27,25,5,5,6,1,1,12,12,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,11,12,1,13,7,8,9,7,8,9,15,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,12,1,1,13,16,17,18,16,11,18,15,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,13,25,26,27,25,26,27,15,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,13,7,23,23,23,23,23,24,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,13,15,1,1,1,1,1,10,12,12,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,13,15,1,1,1,1,1,12,1,12,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,13,25,5,5,5,5,5,5,5,5,5,6,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,22,23,23,23,23,23,23,23,23,23,23,24,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
</data>
 </layer>
 <objectgroup id="4" name="Obstacles">
  <object id="1" name="wall" x="0" y="-96" width="3168" height="96"/>
  <object id="2" name="wall" x="3072" y="0" width="96" height="3168"/>
  <object id="3" name="wall" x="-96" y="3072" width="3168" height="96"/>
  <object id="4" name="wall" x="-96" y="-96" width="96" height="3168"/>
 </objectgroup>
</map>

And this is the .tsx file:

<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.2" tiledversion="1.3.4" name="Bigger-Textures (96x96)" tilewidth="96" tileheight="96" tilecount="81" columns="9">
 <image source="../gfx/tiles/tilesheets/Textures-sprite-sheet-4X.png" width="864" height="864"/>
 <tile id="1">
  <animation>
   <frame tileid="1" duration="500"/>
   <frame tileid="2" duration="500"/>
  </animation>
 </tile>
 <tile id="2">
  <animation>
   <frame tileid="2" duration="500"/>
   <frame tileid="1" duration="500"/>
  </animation>
 </tile>
 <tile id="9">
  <animation>
   <frame tileid="9" duration="500"/>
   <frame tileid="10" duration="500"/>
  </animation>
 </tile>
 <tile id="10">
  <animation>
   <frame tileid="10" duration="500"/>
   <frame tileid="9" duration="500"/>
  </animation>
 </tile>
</tileset>

And this is how I currently render the tiles:

    def render(self):
        self.ti = self.handler.currentMap.get_tile_image_by_gid
        xStart = max(0, self.handler.camera.xOffset / self.handler.currentMap.tilewidth)
        xEnd = min(self.handler.currentMap.width, (self.handler.camera.xOffset + self.handler.displayWidth) / self.handler.currentMap.tilewidth + 1)
        yStart = max(0, self.handler.camera.yOffset / self.handler.currentMap.tileheight)
        yEnd = min(self.handler.currentMap.height, (self.handler.camera.yOffset + self.handler.displayHeight) / self.handler.currentMap.tileheight + 1)

        for i in range(len(self.handler.currentMap.layers) - 1):
            for x in range(int(xStart), int(xEnd)):
                for y in range(int(yStart), int(yEnd)):
                    tile = self.handler.currentMap.get_tile_image(x, y, i)
                
                    if (tile):
                        self.display.blit(tile, (x * self.handler.currentMap.tilewidth - self.handler.camera.xOffset,
                                                 y * self.handler.currentMap.tileheight - self.handler.camera.yOffset))

This is what it says on the Pytmx github :

    # just iterate over animated tiles and demo them

    # tmx_map is a TiledMap object
    # tile_properties is a dictionary of all tile properties

    # iterate over the tile properties
        for gid, props in tmx_map.tile_properties.items():

            # iterate over the frames of the animation
            # if there is no animation, this list will be empty
            for animation_frame in props['frames']:
   
                # do something with the gid and duration of the frame
                # this may change in the future, as it is a little awkward now
                image = tmx_map.get_tile_image_by_gid(gid)
                duration = animation_frame.duration
                ...

Any help is greatly appreciated! Here is the project on GitHub if it is to any use :D

1

There are 1 answers

0
Dowdheur On

I had fun looking for this today. And it's not that complicated after all.

I made something like this in Tiled

Note the animated water tiles.

Now look at the code below:

 def update(self, frame):
    if frame in getFrequencyList(6):
        self.current_anim_index += 1

    if self.current_anim_index == 4:
        self.current_anim_index = 0

 def getSurface(self):
    for layer in self.tmx_data.visible_layers:
        for x, y, image in layer.tiles():
            for gid, props in self.tmx_data.tile_properties.items():
                if image == self.tmx_data.get_tile_image_by_gid(props['frames'][0].gid):
                    image = self.tmx_data.get_tile_image_by_gid(props['frames'][self.current_anim_index].gid)
                    self.surface.blit(image, (x * 16, y * 16))
                else:
                    self.surface.blit(image, ((x * 16) + layer.offsetx, (y * 16) + layer.offsety))

    return super().getSurface()

when you browse the list of tiles by layers, you recover the image by position (x and y). The principle is that, before displaying the current image (the tile), we check that it is not in fact an animation. For that you have to get all the animated tiles.

So we do

for gid, props in self.tmx_data.tile_properties.items():

All animated tiles are in props['frames'].

If you look at your tsx file, you could see something like this :

<?xml version="1.0" encoding="UTF-8"?>
    <tileset version="1.5" tiledversion="1.6.0" name="Overworld (Light)" 
tilewidth="16" tileheight="16" tilecount="1664" columns="52">
        <image source="Overworld (Light).png" trans="ff00ff" width="832" height="512"/>
        <tile id="30">
            <animation>
                <frame tileid="30" duration="250"/>
                <frame tileid="82" duration="250"/>
                <frame tileid="134" duration="250"/>
                <frame tileid="186" duration="250"/>
            </animation>
        </tile>
        ...

So each part of props['frame'] are tables represent each animation nodes. So

if image == self.tmx_data.get_tile_image_by_gid(props['frames'][0].gid):

means that current image is an animated tile. In that case all you need to de is to blit one of the tile in your props['frame'] table. As you can see I created a current_anim_index attribute. I vary it in the update method. I make sure that this is called in my game loop. The frame argument varies from 0 to 60 (Yes, 60 FPS). And getFrequencyList(6) return a table like [0, 10, 20, 30, 40, 50].