It can be such a pain to get a 2D tile map that an artist has made for a game (such as an RPG) and turn that into an optimized unique-only tile map for a programmer to use in the actual game. But with a bit of Python scripting, anyone can easily write some code to make this entire process much simpler for themselves. Our solution won’t work for everyone, but hopefully it will inspire others to use scripting languages to automate processes of their game development to make their lives easier.
Overview
What are we trying to accomplish?
The problem is that often the developer wants the map optimized for the game, but the artist just wants an image he can easily draw and edit. So the goal is to resolve this issue in a more automatic way using scripting. Sure, there is the initial cost of coding but after that, it’s an automated process.
Our Specific Problem
The main thing I need to mention about our specific situation is that we have gone with a 32×32 tile system. We know its not quite as complex or cool as the free-movement based systems, but the trade off is a much easier and maintainable structure for us currently. It was just so much easier coding bounding boxes, collision detection, rendering, items, etc. So the goal is for our artist Igor to be able to make a map in Photoshop, with 32×32 grid lines showing just as reference, and allow him to simply give me that image and my script to optimize it for me to use in the game.
Visual Representation
So if you’re still a bit confused about what exactly I mean, then let me show you a real example of what we’re trying to do. We’re trying to take a map that Igor has created in Photoshop, such as this:
And then programatically turn it into an image like this:
As you can see, the second image is overall much, much smaller than the original image. But it would be a complete pain for the artist to do that himself! How is it so much smaller you ask? Because the second image is nothing but the unique tiles of the first image. So there are no duplicates of the exact same tile in the second image. To give you an idea of how big of a difference this can be, the original image which is 1280×544 (pixels) contained 680 tiles (32×32) while the second image is 384×384 (pixels) and contains 144 tiles. Now that’s what I’m talking about!
XML File
So I bet you’re already wondering how we then handle that second type of image in our actual game. I’m going to have to be kind of short/brief with this because every game is different. But in ours, we’ve got a grid where each node is a Tile. Each Tile contains the X & Y coordinates of the image it should render for itself. How do we know the X & Y coordinates? Glad you asked. The script we run to generate the second image, also generates an XML mapping of every tile found in the original image but with X & Y coordinates that point to the image it should use from the second image. Below is an example of what was generated for the previous two images.
<xml> <tileImages> <row>0x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|2x0|3x0|4x0|4x0|4x0|4x0|4x0|4x0|5x0|6x0|7x0|8x0|9x0|9x0|9x0|10x0|11x0|0x1|1x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|11x0|0x1|</row> <row>0x0|3x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|4x1|5x1|1x0|1x0|1x0|1x0|1x0|8x0|9x0|9x0|9x0|10x0|6x1|7x1|6x1|8x1|6x1|8x1|6x1|8x1|1x1|2x1|6x1|7x1|</row> <row>0x0|9x1|1x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|10x1|2x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|1x0|1x0|1x0|1x0|8x0|9x0|9x0|9x0|10x0|11x0|0x1|11x0|0x1|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|</row> <row>0x0|9x1|1x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|11x1|1x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|1x0|1x0|1x0|1x0|8x0|9x0|9x0|9x0|10x0|6x1|7x1|6x1|7x1|6x1|7x1|6x1|7x1|1x1|2x1|6x1|7x1|</row> <row>0x0|9x1|1x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|1x0|1x0|1x0|1x0|8x0|9x0|9x0|9x0|10x0|11x0|0x1|11x0|0x1|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|</row> <row>0x0|9x1|1x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|1x0|1x0|1x0|1x0|8x0|9x0|9x0|9x0|10x0|1x0|1x0|6x1|7x1|6x1|7x1|6x1|7x1|1x1|2x1|6x1|7x1|</row> <row>0x0|9x1|1x1|0x2|1x2|2x2|3x2|4x2|5x2|6x2|7x2|1x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|1x0|1x0|8x2|9x2|10x2|9x0|9x0|9x0|11x2|0x3|1x3|11x0|0x1|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|</row> <row>0x0|9x1|1x1|2x3|3x3|4x3|5x3|6x3|7x3|8x3|9x3|1x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|1x0|1x0|10x3|11x3|9x0|9x0|9x0|9x0|9x0|0x4|1x4|1x0|1x0|6x1|7x1|6x1|7x1|1x1|2x1|6x1|7x1|</row> <row>0x0|9x1|1x1|2x4|3x4|4x4|5x4|6x4|7x4|8x4|9x4|2x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|8x2|9x2|10x2|10x4|9x0|9x0|9x0|9x0|9x0|9x0|11x2|11x4|1x3|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|</row> <row>0x0|9x1|1x1|0x5|1x5|2x5|3x5|4x5|3x5|5x5|6x5|1x1|2x1|2x1|2x1|2x1|2x1|9x1|1x0|10x3|7x5|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|8x5|1x4|6x1|7x1|6x1|7x1|1x1|2x1|6x1|7x1|</row> <row>0x0|9x5|4x1|10x5|11x5|0x6|1x6|2x6|3x6|4x6|5x6|4x1|4x1|4x1|6x6|1x0|7x6|8x6|1x0|8x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|10x0|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|</row> <row>0x0|1x0|1x0|9x6|10x6|11x6|0x7|1x7|2x7|3x7|4x7|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|5x7|6x7|7x7|9x0|9x0|9x0|9x0|9x0|9x0|9x0|9x0|8x7|9x7|6x1|7x1|1x1|2x1|2x1|2x1|6x1|7x1|</row> <row>0x0|1x0|1x0|1x0|1x0|1x0|1x0|10x7|11x7|0x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|0x8|1x8|2x8|9x0|9x0|9x0|9x0|9x0|9x0|9x0|3x8|4x8|5x8|11x0|0x1|1x1|2x1|2x1|2x1|11x0|0x1|</row> <row>0x0|1x0|1x0|1x0|1x0|1x0|1x0|10x7|11x7|0x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|5x7|6x8|9x0|9x0|7x8|9x0|9x0|8x7|9x7|6x1|8x1|6x1|7x1|1x1|2x1|6x1|8x1|6x1|7x1|</row> <row>8x8|1x0|1x0|1x0|1x0|1x0|1x0|9x8|11x7|0x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|10x8|11x8|2x8|9x0|9x0|9x0|3x8|4x8|5x8|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|11x0|0x1|</row> <row>0x9|1x9|1x9|1x9|1x9|1x9|1x9|2x9|3x9|0x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|4x9|5x7|5x9|9x0|6x9|9x7|6x1|8x1|6x1|7x1|6x1|7x1|1x1|2x1|6x1|7x1|6x1|7x1|</row> <row>7x9|8x9|8x9|8x9|8x9|8x9|8x9|9x9|10x9|11x9|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|1x0|0x10|0x8|1x10|2x10|4x8|5x8|11x0|0x1|11x0|0x1|11x0|0x1|1x1|2x1|11x0|0x1|11x0|0x1|</row> </tileImages> </xml>
In the game code, I just loop through all the rows and pull the X & Y coordinates and then set them on each Tile that is apart of our grid. So as you can see from the above file, the very first top left rows look like:
<row>0x0|1x0|... <row>0x0|3x1|... <row>0x0|9x1|...
Point to these tiles from the optimized tile map:
And when drawn to screen in game, look like:
Which is the top left corner piece of the original image.
Python Script
Dependencies
How to Use
python the_script.py example_original.png example
Where the_script.py is the code below, example_original.png is the complete image that your artist gives you, and example will result in the script creating a example.png (optimized unique-only tile map) and example.xml (the xml mappings).
The Actual Code
import math, operator import os, sys import ImageChops from PIL import Image # We need the input image and a name for the output if len(sys.argv) < 3: print "You did not supply two arguments." sys.exit(0) inputImage = sys.argv[1] outputName = sys.argv[2] # Set initial variables TILE_SIZE = 32 # Function to check and see if two images are indentical def SameImage(image1, image2): return ImageChops.difference(image1.convert('RGBA'), image2.convert('RGBA')).getbbox() is None # Function to check if a tile is already in our tileset def TileExists( tileSet, image ): rowCount = 0 for rowTile in tileSet: columnCount = 0 for columnTile in rowTile: if SameImage(image, columnTile): return (columnCount, rowCount) columnCount += 1 rowCount += 1 return (-1, -1) # Function to return the total size of the tile set def TileSetSize( tileSet ): tileSetSize = 0 for rowTile in tileSet: for columnTile in rowTile: tileSetSize += 1 return int(math.ceil(math.sqrt(tileSetSize))) # Function to create the tileset image and output it def CreateImage( tileSet, outputImage ): tileSetSize = TileSetSize( tileSet ) tileSetImageSize = tileSetSize*TILE_SIZE print "TileSet Image Size: " + str(tileSetImageSize) + "x" + str(tileSetImageSize) print "Total Tiles in TileSet Image: " + str(tileSetSize * tileSetSize) blankImage = Image.new("RGB", (tileSetImageSize, tileSetImageSize), "white") rowCount = 0 columnCount = 0 for rowTile in tileSet: for columnTile in rowTile: if columnCount == tileSetSize: rowCount += 1 columnCount = 0 blankImage.paste(columnTile, (columnCount*TILE_SIZE, rowCount*TILE_SIZE)) columnCount += 1 blankImage.save(outputImage) # Function to create multi-dimension list of tiles from image def CreateImageMapFromImage( sourceImage ): image = Image.open(sourceImage) width, height = image.size columns = width / TILE_SIZE rows = height / TILE_SIZE print "Image Size: " + str(width) + "x" + str(height) print "Tile Size: " + str(width/TILE_SIZE) + "x" + str(height/TILE_SIZE) print "Total Tiles in Image: " + str(rows * columns) imageMap = [] for row in range(0, rows): for column in range(0, columns): cropRectangle = (column*TILE_SIZE, row*TILE_SIZE, (column*TILE_SIZE)+TILE_SIZE, (row*TILE_SIZE)+TILE_SIZE) croppedImage = image.crop(cropRectangle) if len(imageMap) <= row: imageMap.append([croppedImage]) else: imageMap[row].append(croppedImage) return imageMap # Function to create tile sheet def CreateTileSet( imageMap, outputName ): tileSet = [] for row in imageMap: for image in row: if TileExists(tileSet, image) == (-1, -1): if len(tileSet) <= row: tileSet.append([image]) else: tileSet[row].append(image) CreateImage(tileSet, outputName + ".png") # Function to create XML mapping file def CreateXML( sourceImageMap, tileSetImageMap, outputName ): xmlOutput = [] for row in sourceImageMap: tempRow = "" for column in row: existingTileX, existingTileY = TileExists(tileSetImageMap, column) if existingTileX != -1 and existingTileY != -1: tempRow += str(existingTileX) + "x" + str(existingTileY) + "|" xmlOutput.append(" <row>" + tempRow + "</row>\n") fileHandler = open(outputName + ".xml", 'w') fileHandler.write("<xml>\n <tileImages>\n") for xmlRow in xmlOutput: fileHandler.write(xmlRow) fileHandler.write(" </tileImages>\n</xml>") fileHandler.close() # ** -- Run the Script -- ** # sourceImageMap = CreateImageMapFromImage(inputImage) CreateTileSet(sourceImageMap, outputName) tileSetImageMap = CreateImageMapFromImage(outputName + ".png") CreateXML(sourceImageMap, tileSetImageMap, outputName)
Disclaimer
Optimization
For better optimization, make sure your artist tries to not make tiles non-unique for no real reason (aka doesn’t add any value). We by no means claim this to be the best way or most optimized to handle tile sheets but we find it is overall a much better process for us than the artist trying to break down the images himself into a tileset and/or using Mappy, etc.
Script Issues
I’ve not tested the script on any machine other than mine, which runs Windows 8.1 with Python 2.6 and PIL 1.1.7. It may not work on others by default, but should probably only need minor adjustments. Also, I don’t claim that this script is optimized code either. I wrote something that gets the job done, so I could get back to actually coding the game itself. 😀