It has been a little over a month since I last posted a blog about my updates on the work I've been doing. I admit I've been a bit lazy these past few weeks and pushed a few deadlines ahead, but I've caught up with my planned time line now after a few night-outs :D.
Stuff not working and dealing with futile attempts at optimization are to blame for my temporary loss of enthusiasm. I've also been reading A Song of Ice and Fire which is the book series Game of Thrones is based on, and those books are difficult to put down once you've started. I was going to work on a TMX parser for directly loading maps from a TMX file. Implementing a parser was easy because I had planned on using an existing python library. I ended up using python-tmx which is a pretty lightweight and straightforward library with the minimal set of features our module requires unlike the feature rich and heavy PyTMX which I had mentioned last time.
All python-tmx does is parses the tmx file's xml and loads the data into python objects arranged hierarchically according to the xml nesting. So my job just involved reading data from those python objects and initializing all the referenced components. The code is so simple to understand that I was able to submit a patch to this library for animation frames support.
First we need to load the tilesets as textures in KivEnt and register them in
the texture_manager
. Kivy, and even KivEnt uses image atlases for loading
multiple textures from a single source image, which is faster because we load
and process one large image file instead of multiple smaller image files. The
atlas file contains the texture name and its position and size in the source
image, so its pretty obvious how to extract the texture from the image.
In graphics rendering term you only need to modify the mesh matrix without the
overhead of loading an image. More details can be found in this
Gamasutra article
Tiled use the fact that each tile is of the same size to its advantage in storing atlas-like information in a concise manner. Each tile in a tileset has an index assigned to it starting with the top-left tile as 0 and incrementing as we move left. So a 3x3 tileset would have tile indices like so:
0 1 2
3 4 5
6 7 8
Each tileset has a unique id called the firstgid
or first global id.
The intention is that each tile of each tileset should have a unique id or
gid
which is the firstgid + index
for each tile. So if we have 3 tilesets
3x3, 2x2 and 2x3 respectively and the firstgid
of the first is 1. Then the
gids of the tiles in that first tileset is 1,2,..9. Hence to maintain uniqueness
the firstgid
of second tileset is 10, similarly for the third. Now all you
need to store is the firstgid
for each tileset and you know the gid of each
tile.
The next thing we have to do is to arrange these tiles on the grid to make our
desired map. The way to do it is now extremely simple. Each position on the
map (x,y)
will have a gid
corresponding to the tile we place there or 0 to
denote no tile or empty space (notice how gids are greater than 1 because the
firstgid of the first tileset was 1). For multiple layers you just need to have
this data for each layer.
You can play around with the tilesets and tiles to add properties like vertical or horizontal flipping, animation frames, opacity, etc. by adding a tile property element in the tileset with the index of the tile and the property you want to add.
The map grid data for layers is stored as one big sequence of gids. Because we
know the size of the map grid we can easily find the (x,y)
coordinates from
the index in this sequence where
x = index % width
y = index / width
Once you know the (x,y)
coordinate, we also know the actual pixel position
on the canvas of where to draw this tile because we know the tile size. We also
know what texture to draw because of the gid.
I have fixed the textures and models to be loaded with the name 'tile_\<gid>'.
To pass to the tile_manager
we need a three-dimensional array spanning
rows x columns each element being a list of dictionaries with the
model and texture name and layer index for that grid location. It is easy to
form this array of dicts from the layer data when we have fixed the naming
convention for textures and models. This also lets us form a list of all the
gids we will use so that we only optimally load those.
Once we have this list of gids we have to load, we will create an atlas dict
for each tileset which has the texture name in our defined format and the
position of that texture in the image calculated from the index of the tile.
We pass this atlas dict to the texture_manager
which then handles the rest of
the job. Once we have the textures loaded we load the models using the same
naming convention. And then we load the tile_map using that array of dicts we
generated before. This order is necessary because map_manager
loads data for
textures and models using the registered names and for that the names need to
be registered with the texture and model managers first.
Next task is to initialize each tile as an entity so that it gets rendered on
the screen. Each layer requires a different renderer if we need to control the
z-index of the tiles. And for animated tiles each renderer requires its own
animation system. I made a util function for easily initializing multiple
renderers and animation systems at once and adding them to the gameworld. And
I tweaked the init_enitity_from_map
function from before to support layers
and use the correct renderer names.
The result is pretty amazing :D
I tried researching into trying to use a single renderer with z-index support for rendering layers without the need for multiple systems. But there are a lot of problems which would arise when batching. It is better performance-wise to let Kivy handle z-index in terms of order of drawing the canvas.
Currently the util functions are all pure python functions because the api to init entities and register textures and models is python-based. A cython api doesn't exist and hence we have the python overhead of creating dicts to pass as init args. Kovak says there is some more tweaks in the base of KivEnt required to correctly implement a cython api, and that he has been considering it for months now. So for now, I have to stick to using the python api.
Next is implementing hexagonal and isometric tiles support, which is essentially just figuring out the pixel position of the tiles on the canvas differently. I think the toughest part will be to create a hexagonal and isometric map but you never know :P.