Unity has been used to develop numerous high quality 2D games over the years. This article, which is based on a talk I gave at three of our regional Unite developer conferences in Korea, Japan and China, gives in-depth instruction for a solid, real-life 2D production workflow. I hope this post can be helpful for any of our readers that are creating 2D games and interactive content with Unity. Due to the length of the tutorial, I’ve split it into two blog posts. Today you can read about authoring and exporting and tomorrow I’ll post the section on importing. You can find a link to download all the files at the end of the post.

The benefits of a good 2D content workflow

This tutorial takes you through the implementation of a real production workflow. When we talk about workflow it means the steps and processes used all the way from the initial authoring until the content is actually running in the game. Regardless of the number of applications needed in the chain from beginning to end, at a high-level a workflow consists of 3 major steps: authoring, exporting and importing.

For some it may seem odd that we need both an exporter and an importer since tools like Unity are able to import data directly. The reason we need both is because we’re essentially creating our own intermediate file format that acts as the glue between any two applications in your workflow, in this case between Photoshop and Unity. This eliminates the need for an external application to be able to read and parse an application’s native file format, and allows chaining of applications to process the content as it moves from one application to the next.

Authoring

It all starts with great content, and the first workflow we’ll look at is using the industry standard workhorse Photoshop. One of the great things about Photoshop is it’s simplicity to get started. Creating and editing imagery and graphics is painless and easy, and as your needs grow there’s an arsenal of tools and techniques at your disposal. The problem is that image files by themselves don’t have much ability to express useful meta-data for games in the same way that 3D models are able to do. What’s more, 2D is inherently in need of more meta-data given it’s limited nature of just being pixels.

So what we want to do is imbue the image with useful information so that ultimately we know how to use the image in a game. To do that, we can use groups and layers to organize and tag them with what we need.

We also need an example of 2D content that is useful and represents a realistic use. One of the last games I worked on before joining Unity was a simple 2D hidden-object game called Goddess Chronicles, and so we’ll use that as an example. In a hidden-object game the general idea is to find certain items that are hidden in plain sight in a scene such as the example shown below.

Depending upon the game you’re making, the meta-data you need to capture will be different. For this game the design called for 2 basic kinds of imagery: “scenery” and “items”.

The importance of layers

Scenery is the non-interactive content that’s there to provide the bulk of what’s seen, communicate a theme and support the gameplay by providing an environment where items can be hidden. So scenery will be placed into a group named “scenery” and we don’t need to care about the art layers in that group because they’re non-interactive. The items are the things you’ll actually be searching for within the scene and comprise the core gameplay. These are placed in groups that are named “item” and, unlike scenery, the art layers are important and each item can have up to 4 unique layers associated with them.

“Whole” layers are required for all items. Usually once an item is found there will be some effect such as zooming the item up, or placing it into the player’s inventory, and we need the whole image so that it looks right.

“Obscured” layers are used when you want to create the illusion that an item is behind something when in fact it’s floating in front of it. By erasing the pixels that should be hidden it tricks the eye into thinking that it’s actually behind something. In theory we could use whole images for everything, but there are many situations when it’s tedious for the artist to draw everything as separate pieces in order to hide an item, plus using the minimal amount of images in the game will increase the runtime performance.

“Shadow” layers are used to help visually place the items into the scene and look like they belong there. Shadows are kept separately from the whole or obscured image so that if the whole image is zoomed up or moved it doesn’t have an odd looking shadow following it, and instead we can just hide the shadow in the scene once the item is found.

“Hotspot” layers are used to increase or decrease the area which the item can be interacted with. For example if you hide a golf club in a scene it could be very difficult to click or tap on. By using a hotspot you could make the interaction area bigger and easier to use.

So putting all this together we can use groups to designate whether something in our scene is scenery or an item, and we can use the art layers for items to hold the whole image for the item and optionally include obscured, shadow and hotspot images. The image below shows a set of layers for some scenery and an item. The item group is labelled “item:Beads” and contains 2 art layers called “whole” and “hotspot”. The scenery group is labelled “scenery:Column” and it contains any number of art layers that can be named anything since scenery layers aren’t special and we don’t need to keep track of them.

The end result is that we have a well composed scene where the group and layer names are encoded with what we need to give them meaning in our game. You can download the package at the end of the post and have a look for yourself. The next step is to now get all this exported.

Exporting

Once we have some content, we need some way to get it all out for the next application in our workflow, which in this case is Unity. What we want to export are the images used to construct the scene along with the meta-data describing the position, order and other information. In order to do that we need a way to interact with the application at a low level with something that understands how to do that. Fortunately for us, Photoshop is scriptable using javascript and this will do exactly what we need.

In fact quite a few Adobe apps are scriptable including Fireworks, Illustrator, Flash and others. We can use this capability to write our own exporter that can prepare the images for use in Unity and also capture the meta-data we need to make them meaningful. Adobe provides useful documentation and a script editor and debugger called ExtendScript for free.

If you have the Creative Suite, it’s probably already installed on your system. On Mac it can be found in the Utilities/Adobe Utilities – CS6/ExtendScript Toolkit CS6 folder for the latest Creative Suite. On Windows, you should be able to find it at C:\Documents and Settings\\Application Data\ Adobe\ExtendScript Toolkit\3.8.

Pretty much anything you can do in Photoshop can be scripted, and if there isn’t an API for a particular thing you want to do, you can record actions and convert them to command codes that you can paste into your scripts (alpha channel operations, I’m looking at you!).

For our purposes, we need a simple script that basically:

Checks to see if we have an open document,

Makes sure the document has layers to export,

Prompts the user as to where the exported files should be placed,

Loops through the layers from back to front, trimming away the empty space, saving the image to disk and capturing the position and filename in an XML formatted string,

And finally saving out the XML data to a file.

The final exporter script is in the tutorial package. To use the script you place it in the Presets/Scripts sub-directory inside your Photoshop application directory. The script contains comments that explain what it does so I won’t go through all of it, but I will cover some of the more important parts.

Firstly, the scripts are written in javascript which makes it easy to learn and use. The javascript engine used by Adobe isn’t very fast, but it works and is debuggable with the ExtendScript editor which makes it very useful.

Since the meta-data is actually the part that gives meaning to the data, we need to spend the most time figuring out exactly what meta-data we need and how it should be formatted. I use XML as the way to specify the meta-data since that makes it really easy to parse later on in Unity. Based on the game design we know we have certain data that will be needed in the game. An extract of that data looks like this:

<?xml version="1.0" encoding="utf-8"?> <HogScene xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <layers> <Layer> <type>Scenery</type> <name>Scenery 01-4 background</name> <images> <Image> <type>Whole</type> <name>Scenery Tree.png</name> <x>25</x> <y>63</y> </Image> </images> </Layer> ... </layers> </HogScene> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 & lt ; ? xml version = "1.0" encoding = "utf-8" ? & gt ; & lt ; HogScene xmlns : xsi = "http://www.w3.org/2001/XMLSchema-instance" xmlns : xsd = "http://www.w3.org/2001/XMLSchema" & gt ; & lt ; layers & gt ; & lt ; Layer & gt ; & lt ; type & gt ; Scenery & lt ; / type & gt ; & lt ; name & gt ; Scenery 01 - 4 background & lt ; / name & gt ; & lt ; images & gt ; & lt ; Image & gt ; & lt ; type & gt ; Whole & lt ; / type & gt ; & lt ; name & gt ; Scenery Tree . png & lt ; / name & gt ; & lt ; x & gt ; 25 & lt ; / x & gt ; & lt ; y & gt ; 63 & lt ; / y & gt ; & lt ; / Image & gt ; & lt ; / images & gt ; & lt ; / Layer & gt ; . . . & lt ; / layers & gt ; & lt ; / HogScene & gt ;

So our exporter script will capture these simple XML elements during export and when it’s all completed write that out to a file.

In our exporter script, we start off with creating some file scope variables that we need for convenience and then call the “main” function which is defined right after that. The basic way this works is that when you run your script it’s evaluated and executed from top to bottom. You find examples on the internet where some code is contained in functions and some is not, and I find this a bit confusing. So I put everything into functions and call the entry point explicitly as a matter of convention.

Inside the main function, there’s a couple of interesting parts. The first one is the line:

duppedPsd = app.activeDocument.duplicate(); 1 duppedPsd = app . activeDocument . duplicate ( ) ;

Duplicating the active document is highly recommended for two reasons. The first is that in Photoshop if you touch anything in a document, even expanding or collapsing a group, it causes the document to be marked as dirty. So if we didn’t duplicate the active document, the process of exporting would always cause it to be marked as dirty. When the artist quits Photoshop or closes the document they will be prompted to save, and they may not remember if they made changes to the document or not and will likely just save it. This will cause the timestamp of the file to be newer than the exported files, and once checked into version control will be confusing because a designer, producer or programmer won’t know if they need to re-export the file or not. This creates an endless cycle because the file will always be newer than the export. By making a duplicate it will leave the original document in it’s original state, which still may cause a problem, but that’s then a production process problem and not something we’re causing.

The second reason for duplicating the document is simply that things will go wrong while creating your own exporters, and when things go wrong it will leave your document in an unknown state. So by duplicating it we always have our untouched master copy to work with.

The next thing of interest in the main function is:

removeAllTopLevelArtLayers(duppedPsd); 1 removeAllTopLevelArtLayers ( duppedPsd ) ;

Since we’re using groups as the containers for the things we’re interested in, we can let the artists or designers add art layers that aren’t in any group (“top level”). They can use this for concept or photo references, screen layout guides, or placing placeholder elements like a game HUD. So we clean up the duplicated file by removing these images before processing.

Other than that, the main function calls the export function, then creates and writes the XML file containing our meta-data.

The export function exportLayerSets is a recursive function. A recursive function means it calls itself if needed to “drill down” into our groups to find the lowest level group that is deepest in the scene.

function exportLayerSets(obj) { for(var i = obj.layerSets.length-1; 0 < = i; i--) { exportLayerSets(obj.layerSets[i]); } ... } 1 2 3 4 5 6 7 8 function exportLayerSets ( obj ) { for ( var i = obj . layerSets . length - 1 ; 0 & lt ; = i ; i -- ) { exportLayerSets ( obj . layerSets [ i ] ) ; } . . . }

It does this by simply looping backwards through the list of groups, and if one of the groups has a group inside it, then it recurses and calls itself again with that group and so on. We loop backwards because in Photoshop the bottom-most layer in the list is the first one and groups and layers higher in the list are drawn on top of lower ones. So we process everything back to front.

Once we have an actual art layer, we then look at the group name and see if it starts with “item:”, “custom:” (which we used for HUD elements) or else it’s assumed to be scenery.

if(obj.name.search("item:") >= 0) { ... } else if(obj.name.search("custom:") >= 0) { ... } else // must be a scenery group { ... } 1 2 3 4 5 6 7 8 9 10 11 12 if ( obj . name . search ( "item:" ) & gt ; = 0 ) { . . . } else if ( obj . name . search ( "custom:" ) & gt ; = 0 ) { . . . } else // must be a scenery group { . . . }

For items, we then loop through the art layers, switch on the layer and export whichever of the known types we support.

// process layers for(var layerIndex = 0; layerIndex < obj.artLayers.length; layerIndex++) { sceneData += "<Image>"; obj.artLayers[layerIndex].visible = true; switch(obj.artLayers[layerIndex].name) { case "hotspot": saveScenePng(...); break; case "obscured": saveScenePng(...); break; case "shadow": saveScenePng(...); break; case "whole": saveScenePng(...); break; } obj.artLayers[layerIndex].visible = false; sceneData += "</Image>"; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // process layers for ( var layerIndex = 0 ; layerIndex & lt ; obj . artLayers . length ; layerIndex ++ ) { sceneData += "<Image>" ; obj . artLayers [ layerIndex ] . visible = true ; switch ( obj . artLayers [ layerIndex ] . name ) { case "hotspot" : saveScenePng ( . . . ) ; break ; case "obscured" : saveScenePng ( . . . ) ; break ; case "shadow" : saveScenePng ( . . . ) ; break ; case "whole" : saveScenePng ( . . . ) ; break ; } obj . artLayers [ layerIndex ] . visible = false ; sceneData += "</Image>" ; }

When we find something we want to save, we call the function to save the image as a PNG file. This function collapses the image and trims the left and top in order to determine the X and Y coordinates of the image. Then we trim off the right and bottom and save it out, and capture the meta-data into the XML string.

function saveScenePng(psd, imageType, fileName) { // we should now have a single art layer if all went well psd.mergeVisibleLayers(); // figure out where the top-left corner is so it can be exported // into the scene file for placement in game // capture current size var height = psd.height.value; var width = psd.width.value; var top = psd.height.value; var left = psd.width.value; // trim off the top and left psd.trim(TrimType.TRANSPARENT, true, true, false, false); // the difference between original and trimmed is the amount of offset top -= psd.height.value; left -= psd.width.value; // trim the right and bottom psd.trim(TrimType.TRANSPARENT); // find center top += (psd.height.value / 2) left += (psd.width.value / 2) // unity needs center of image, not top left top = -(top - (height/2)); left -= (width/2); // save the image var pngFile = new File(destinationFolder + "/" + fileName + ".png"); var pngSaveOptions = new PNGSaveOptions(); psd.saveAs(pngFile, pngSaveOptions, true, Extension.LOWERCASE); psd.close(SaveOptions.DONOTSAVECHANGES); // save the scene data sceneData += ("<type>" + imageType + "</type> <name>" + fileName + ".png</name> <x>" + left + "</x><y>" + top + "</y>"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function saveScenePng ( psd , imageType , fileName ) { // we should now have a single art layer if all went well psd . mergeVisibleLayers ( ) ; // figure out where the top-left corner is so it can be exported // into the scene file for placement in game // capture current size var height = psd . height . value ; var width = psd . width . value ; var top = psd . height . value ; var left = psd . width . value ; // trim off the top and left psd . trim ( TrimType . TRANSPARENT , true , true , false , false ) ; // the difference between original and trimmed is the amount of offset top -= psd . height . value ; left -= psd . width . value ; // trim the right and bottom psd . trim ( TrimType . TRANSPARENT ) ; // find center top += ( psd . height . value / 2 ) left += ( psd . width . value / 2 ) // unity needs center of image, not top left top = - ( top - ( height / 2 ) ) ; left -= ( width / 2 ) ; // save the image var pngFile = new File ( destinationFolder + "/" + fileName + ".png" ) ; var pngSaveOptions = new PNGSaveOptions ( ) ; psd . saveAs ( pngFile , pngSaveOptions , true , Extension . LOWERCASE ) ; psd . close ( SaveOptions . DONOTSAVECHANGES ) ; // save the scene data sceneData += ( "<type>" + imageType + "</type> <name>" + fileName + ".png</name> <x>" + left + "</x><y>" + top + "</y>" ) ; }

When we run the exporter on the file, we should end up with a directly of cropped PNG files plus an XML file with the same base filename as the original document such as that shown below.

Now that we successfully exported all our images and meta-data, we move to the next application in our workflow which is Unity and get the files imported. I’ll cover this in my next blog post. Thanks!

You can download a Unity package with all the files for this tutorial.

Update: Part II