whydoitweet

FF0

In the previous edition of GameWorld we added support for tile images, but make those images by hand is rather tedious. Today, we will begin with making your life easier, by developing a tile image generator. These images are useful for 2D games, but could possibly serve a lot of other purposes as well :).

This part will be the first of a sub-series as one can create a huge number of types of tiles. We will start with plane tiles, as well as basic grid and brick structures. However, I fully intend to move on to things like houses, trees and even sprites in due time!

Mini-sidebar: Pixel art on Coil

Now I cannot and should not continue to descend into the world of Pixel-Art related topics without paying a brief homage to AussieNinja. He provides a range of very nice pixel art tutorials, e.g. on walking animations, dungeon sceneries, your own avatar or Santa Claus. Have a look if you value the manual craft, it'll be worth it! As for me, I suppose I'm especially interested in generating stuff automatically...

Because we are just generating images, there is no need for Brython or other sophisticated game engines. Instead, we'll simply resort to using Pillow, which we used a while ago, and which is working well on repl.it again :).

Groundwork for the Tile Generator

In this tutorial we'll make two files:

  • tile_patterns.py – which contains the tile objects and a range of pattern functions we can apply.
  • main.py – which we use to generate the tileset and a range of different tiles.

I will start of with building the library in tile_patterns, and to do so we will first need to import PIL:

from PIL import Image, ImageDraw

And define an object to store the tiles in, and manipulate them.

class Tiles:

In this class, we make an init function, which creates an empty tileset with num_tiles tiles. We do this as follows:

def __init__(self, num_tiles):

self.tw = 47 # tile width (adjusted for weird pillow drawing)

self.th = 47 # tile height

self.tws = 49 # tile width with whitespace

self.ths = 49 # tile height with whitespace

self.img = Image.new('RGBA', (4*self.tws, int(int(num_tiles+3)/4)*self.ths), (0,0,0,0))

self.d = ImageDraw.Draw(self.img)

All the work is done in the last two lines, where we create a rectangular transparent image, and a drawing canvas. Everything else is just about storing values that we are going to need very often. Because we are going to define patterns, patterns of patterns, patterns of patterns of patterns etc., we are going to need very easy access to many low-level variables.

These member variables help with that, but we'll also define a few so-called getters. Here's one for getting the coordinates in the image file of a tile at place index:

def get_tile_coords(self, index):

return (index%4)*self.tws, int(index/4)*self.ths

Here's to get the x coordinate only:

def tx(self, index):

return (index%4)*self.tws

And the y coordinate:

def ty(self, index):

return int(index/4)*self.ths

Next up is a trivial function to save our tileset object as an image (I love Pillow with this):

def save(self):

self.img.save("tiles.png")

And lastly, a function that returns all the main properties of a particular tile:

def get_properties(self, i):

return self.d, self.tx(i), self.ty(i), self.tw, self.th

This includes respectively the drawing canvas, the x,y location of the corner where we start drawing, and width and height of the tile.

Okay, enough about this groundwork, let's draw ourselves a few patterns!

Drawing patterns

Because we defined such a simplistic class, drawing basic patterns is relatively simple too. Here's how we draw a plain tile of a single color:

def draw_plain(t, i, color):

d,x,y,tw,th = t.get_properties(i)

d.rectangle([x, y, x+tw, y+th], color)

...which just involves drawing a filled rectangle. For color you can put all sorts of things, but I personally prefer the short hex notation #rgb (e.g. “#FF0” for yellow), or the “rgb(255,255,0)” style notation. Remember though, it should always be a string :).

Next up, we make a function for verticle lines, each of them bw pixels apart (and a default light grey color):

def draw_vlines(t, i, color="#aaa", bw=8):

d,x,y,tw,th = t.get_properties(i)

for xi in range(0,t.tw):

if xi%bw == 0:

d.line([x+xi,y,x+xi,y+th], color)

And we do the same for horizontal lines, bh pixels apart:

def draw_hlines(t, i, color="#aaa", bh=12):

d,x,y,tw,th = t.get_properties(i)

for yi in range(0,t.th):

if yi%bh == 0:

d.line([x,y+yi,x+tw,y+yi], color)

Now, with these three functions, we can already make plain tiles, and gridded ones, by combining horizontal lines and verticle lines. Let's make a short-hand function for grids as well then:

def draw_grid(t, i, color, bw=12, bh=8):

draw_vlines(t, i, color, bw)

draw_hlines(t, i, color, bh)

I really like this one, as it's a schoolbook example of how programming can be rather simple if you break it down into pieces that are small enough :). But this grid doesn't have a background, so let's make a grid with a background too:

def draw_plaingrid(t, i, color, grid_color="#aaa", bw=12, bh=8):

draw_plain(t, i, color)

draw_grid(t, i, grid_color, bw, bh)

Lastly, just to add a little flavour to this tutorial, I'll share a basic function to make brick-like patterns, where the verticle lines vary in location:

def draw_brick(t, i, cement_color, bw, bh):

d,x,y,tw,th = t.get_properties(i)

if bw%2 > 0:

print("Error: brick width must be even.")

Here we draw he horizontal lines as normal...

draw_hlines(t, i, cement_color, bh)

But for the vertical ones, we go a little bit more exotic, with a flip-flop style branching mechanism:

yi=0

while yi < t.th:

if yi % (2*bh) == 0:

for xi in range(x,x+t.tw):

if xi%bw == 0:

d.line([xi,y+yi,xi,y+yi+bh], cement_color)

else:

for xi in range(x,x+t.tw):

if xi%bw == bw/2:

d.line([xi,y+yi,xi,y+yi+bh], cement_color)

yi += bh

Lastly, let's do bricks with a background :)

def draw_plainbrick(t, i, color, brick_color="#aaa", bw=12, bh=8):

draw_plain(t, i, color)

draw_brick(t, i, brick_color, bw, bh)

All this gives us functions to draw with, but we still have to actually draw of course!

Drawing the tiles

Now, using this library is meant to be simple. Let's see if you think that's indeed the case.

We start off with a few necessary imports:

from PIL import Image, ImageDraw

import tile_patterns as tp

And a function for our main code:

if __name__ == "__main__":

We make a tileset with 12 tiles:

t = tp.Tiles(12)

...with a transparent tile on spot 0:

...a dark green tile on spot 1:

tp.draw_plain(t, 1, "#080")

...a fine-gridded dark red tile on spot 2:

tp.draw_plaingrid(t, 2, "#800", "#aaa")

...a plain “water” tile on spot 3:

tp.draw_plain(t, 3, "#33F")

...a plain “sand” tile on spot 4:

tp.draw_plain(t, 4, "#FF4")

...a dark gray grid tile on spot 5:

tp.draw_plaingrid(t, 5, "#444", "#aaa")

...a dark gray fine brick tile on spot 6:

tp.draw_plainbrick(t, 6, "#444", "#aaa")

...a dark red coarses brick tile on spot 7:

tp.draw_plainbrick(t, 7, "#A11", "#aaa", bw=24)

...a plain rock surface tile on spot 8:

tp.draw_plain(t, 8, "#DDD")

...followed by white stone

tp.draw_plainbrick(t, 9, "#DDD", "#555", bw=24, bh=12)

...large yellow tiles

tp.draw_plaingrid(t, 10, "#DD3", "#333", bw=16, bh=16)

...and fine yellow tiles:

tp.draw_plaingrid(t, 11, "#DD3", "#333", bw=6, bh=6)

Finally, we save the whole thing and print “done” just to tell the user that the image has been generated :)

t.save()

print("done.")

Now if you run this program, it will generate a file called tiles.png. And that file will look like this:

Closing thoughts

Now this tile generator is not nearly enough to make a whole game with, but at least you can do a few basic terrain tiles, floors and walls with it.

Lastly, you can find the Repl for the Tile Generator here:

Feel free to fork it and make it suit your purposes :).

For subscribers I'll briefly share what will come up next in this area in future editions:

Read more...