The next project I’m working on involves a lot of work with Cocos2D and tile maps. And with tile maps, you’ll often have some tiles that are exactly the same, except they need to be rotated or combined with another tile in some way.
Manually rotating and laying out these tiles by hand would be very tedious and error prone. So I started looking for a way to write a script to generate images from a source set, and came across the Python Imaging Library!
In this tutorial, we’ll cover how to use the Python Imaging Library to write a script to read a source image, pull out each tile, and generate rotated copies of each tile into a final sprite sheet image.
It is helpful, but not 100% necessary, to have some Python knowledge for this tutorial. Also, you may wish to check out my tutorial series on How To Make A Tile Based Game with Cocos2D if you’re interested in learning how to make a tile-based game with tiles like these on the iPhone.
Ok, so let’s get started!
Hello, Python Imaging Library!
The first thing you’ll need to do is install the Python Imaging Library (PIL for short) on your machine.
Sadly, I installed PIL on my machine some tile ago and didn’t keep good notes on the steps I took to install it, so you’re on your own!
A good place to get started is the PIL home page, where you can download the source for PIL, or instructions from Google on installing PIL on Mac OS X.
Once you have PIL installed, download a a sample image I made by pulling out a few tiles from the desert map tile set that comes with the Tiled map editor.
Then create a file named “generateTiles.py” in the same directory as the image:
from PIL import Image image = Image.open("DesertWall.jpg") image.show() |
Here we import the Image class from PIL, and use the “open” method to return an Image object. We then use the show method on the image to display a preview to our screen!
Run the script (by running “python generateTiles.py” from a Terminal) and if all works well, you should see the image pop up to the screen!
Reading Tiles
Ok, so let’s modify our script to use a class to store the image, and have a helper function to read out a particular tile.
PIL makes this easy with a function named “crop” that you can call on an image. You simply pass in the coordinates for the corner of the box, and it returns another image object to you.
Replace the contents of generateTiles.py with the following:
from PIL import Image class SpriteSheetReader: def __init__(self, imageName, tileSize): self.spritesheet = Image.open(imageName) self.tileSize = tileSize self.margin = 1 def getTile(self, tileX, tileY): posX = (self.tileSize * tileX) + (self.margin * (tileX + 1)) posY = (self.tileSize * tileY) + (self.margin * (tileY + 1)) box = (posX, posY, posX + self.tileSize, posY + self.tileSize) return self.spritesheet.crop(box) reader = SpriteSheetReader("DesertWall.jpg", 32) tile1 = reader.getTile(0, 0) tile1.show() |
Here we just refactor our code into a class, and we store a reference to the sprite sheet, tile size, and margin when we start up.
In the getTile method we use some math to figure out the x, y coordinates for a tile coordinate. We then figure out the coordinates for all four edges, and call crop to return a result image, which we then show.
Give it a try, and if all works well you should see the first tile show up:
Writing Tiles
Now let’s extend our code so that we can write tiles out to an output sprite sheet.
To do this in PIL, first we need to create an empty image with the Image.new() method. This lets us specify the size of the sprite sheet, and a default color (we’ll use transparent).
Once we have the empty image, we can paste an image into it by using the “paste” method.
Let’s see this in action! Add the following class right above the SpriteSheetReader class:
class SpriteSheetWriter: def __init__(self, tileSize, spriteSheetSize): self.tileSize = tileSize self.spriteSheetSize = spriteSheetSize self.spritesheet = Image.new("RGBA", (self.spriteSheetSize, self.spriteSheetSize), (0,0,0,0)) self.tileX = 0 self.tileY = 0 self.margin = 1 def getCurPos(self): self.posX = (self.tileSize * self.tileX) + (self.margin * (self.tileX + 1)) self.posY = (self.tileSize * self.tileY) + (self.margin * (self.tileY + 1)) if (self.posX + self.tileSize > self.spriteSheetSize): self.tileX = 0 self.tileY = self.tileY + 1 self.getCurPos() if (self.posY + self.tileSize > self.spriteSheetSize): raise Exception('Image does not fit within spritesheet!') def addImage(self, image): self.getCurPos() destBox = (self.posX, self.posY, self.posX + image.size[0], self.posY + image.size[1]) self.spritesheet.paste(image, destBox) self.tileX = self.tileX + 1 def show(self): self.spritesheet.show() |
The init method here creates an empty image of the specified sprite sheet dimensions. It initializes the current tile position to 0,0.
In getCurPos, it calculates the x,y coordinates for the current tile. It has the smarts to check to see if the tile would overlap the edge of the sprite sheet and advance the y coordinate if so.
In addImage, we simply calculate the coordinates for the four corners where we want to paste the image, then call the paste method. Finally we advance the tile x coordinate to the next tile.
You can test this out by modifying the code at the bottom of the file to the following:
reader = SpriteSheetReader("DesertWall.jpg", 32) writer = SpriteSheetWriter(32, 256) tile1 = reader.getTile(0, 0) writer.addImage(tile1) writer.addImage(tile1) writer.show() |
Run the script and you should see the following!
Rotating Images
We’re making good progress so far, but what we really wanted to do was automatically rotate our images into the sprite sheet!
Luckily this is insanely easy with PIL – we can just use the rotate method! Add the following two functions to SpriteSheetWriter:
def addImageAndRotations(self, image): self.addImage(image) image = image.rotate(90) self.addImage(image) image = image.rotate(90) self.addImage(image) image = image.rotate(90) self.addImage(image) def save(self, imageName): self.spritesheet.save(imageName) |
Here we save four copies of the passed in image: the original image, and then we rotate/save 3 more times.
We also add in a method to save this off to disk so we can examine our beautiful result.
Test it out by modifying the code at the bottom to the following:
reader = SpriteSheetReader("DesertWall.jpg", 32) writer = SpriteSheetWriter(32, 256) tile1 = reader.getTile(0, 0) writer.addImage(tile1) tile2 = reader.getTile(1, 0) writer.addImageAndRotations(tile2) tile3 = reader.getTile(2, 0) writer.addImageAndRotations(tile3) tile4 = reader.getTile(3, 0) writer.addImageAndRotations(tile4) writer.show() writer.save("DesertWallOut.jpg") |
Run the script and open up the resulting PNG, and you should see the following:
Note that when you call show() it will often show transparent regions as black, but they are really transparent when you save the image don’t worry :]
Where To Go From Here?
Here’s a copy of the Python script using PIL we’ve developed in the above tutorial.
At this point you should know the basics of using PIL for saving yourself time for monotonous image manipulation like this.
It’s definitely come in handy for me for my latest Cocos2D tile-based project! Speaking of which, if you’re interested in following the development of the project, you can follow @razeware on Twitter!).
If any of you have used PIL for your projects, please comment below! :]
Category: General









Ray, great tutorial! However, I think I’m missing something. Wouldn’t you save texture space by rotating the tiles as sprites in cocos2d?
@Adam: Ah yes, I should have mentioned something about this.
If you have your tiles as CCSprites, you can automatically flip them within Cocos2D by using the flipX or flipY property.
However, by default CCTMXTiledMap has an optimization where as long as you don’t try to get a tile with tileAt, it stores the tiles as simply a quad in the texture atlas (see CCTMXLayer::addQuadFromSprite).
This is a lot more efficient, but as far as I know there’s no way to use flipX/flipY if you go this route. I could be wrong though, let me know if I am!
But either way – the main point of this tutorial was to show the cool stuff you could do with PIL, there are many times where it could save you time! :]
Thanks “ray” that was great tutorial. i ve been waiting another tutorial ^.^
im going to use this on my game
since i followed your tutorials i understand cocos2d way better
and now with this tutorial, i understand making tiles to
thanks for all of your work, and lets be honest, geek and dad have helped me to with the tutorial nr 3
so thanks all
@kristof: Awesome, yeah this technique has come in very handy for me too with my latest project…
can you also make the spritesheed bigger ?
how big can you make a sprite to make it work in cocos2d ?
i tried to use a sprite of 960 X 960
and i got a crash with a SIGABRT error message
@kristof: If I recall 2048×2048 is the maximum sprite sheet size.
You should be able to use a 960×960 sprite (if you really need to, not recommended though as that’s pretty damn big and memory intensive).
As for the crash, here’s my general advice for how to diagnose a crash issue on iOS:
1) Set a breakpoint in your code and step through until you narrow down where it’s crashing
2) Set the NSZombieEnabled argument in your executable options, which sometimes helps narrow down the cause
3) Run with Apple Instruments such as leaks to look for memory issues
4) Tried and true “comment out code till it works” then backtrack from there :]