Categories & Tags
Archive

A good workflow to smoothly import 2D content into Unity, Part I: authoring and exporting

May 17, 2013 in Demos, Tutorials and Tips, Technology by

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>

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();

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);

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]);
        }
        ...
}

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
{
        ...
}

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>";
}

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>");
}

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

Share this post

Comments (30)

Comments are closed.

17 May 2013, 4:29 am

Great article, As I’m just about to start work on a 2D game it’s very helpful, however I was disappointed that no mention of ideal size of artwork was mentioned & it’s something I need to understand. Could you provide any info about this please?

Rob T
17 May 2013, 6:05 am

Steve, use what ever you think might fit your project.

Brett Bibby
17 May 2013, 6:16 am

Hi Steve, basically Rob is right. But if you’re not sure, let the audience/platform guide you. In the case of casual games it was common to target 1024 x 768, sometimes a bit smaller to support netbooks. The second part is then how to translate that into Unity. The two fairly common approaches is to use either 1 Unity unit = 1 pixel or 1 Unity unit = 100 pixels. The former can be had by setting orthographicSize = Screen.height/2 and the latter is a scaled version of that such that a 128 pixel sprite would be 1.28 units (meters) in game. But yeah, experiment and think about your audience, the target platform and the scope of your scenes in the game to guide you on the “right” size. Good luck!

17 May 2013, 6:39 am

Rob T, That is not a very good answer. the fact is that this work flow works best if there are no constraints based on platform and 2d resource requirements. I’m assuming that the following parts of this article will address issues like atlases for draw call performance and cover mobile device best practices since most 2D art is used for that area of gaming.

This issues must be thought of when creating 2d art otherwise you will end up with miscommunication and frustration between the art team and those that must implement the art.

17 May 2013, 9:13 am

Thanks for the reply Brett Bibby, makes a bit more sense now.

Lux
17 May 2013, 9:23 am

Very interesting, thanks!

yo
17 May 2013, 11:03 am

Thanks Brett! Can you go over how to get rid of the “white halo” around images with transparencies? and tell us why that’s happening? Thx!

Breakmachine
17 May 2013, 2:05 pm

YO: There’s a great plugin for photoshop from Flaming Pear called Solidify that fixes “halos”.

YO
17 May 2013, 3:08 pm

Thanks breakmachine, but I want to know how to do it myself without the need for plugins. And more detail on whats causing it.

JustinISO
17 May 2013, 3:11 pm

I am a Unity developer with little experience in Photoshop.
When I try to run the given script from Photoshop I get this:

“Error 8800: General Photoshop error occurred. This functionality may not be available in this version of Photoshop.
- The command “Delete” is not currently available.
Line: 298
-> obj.artLayers[i].remove();”

Any ideas?

Brett Bibby
17 May 2013, 5:31 pm

@justiniso

Did you run it from ExtendScript debugger? Or from Photoshop? If ExtendScript, make sure ExtendScript is set to use Photoshop in the pop-up menu in the upper-left corner of the window. You can also restart Photoshop and try running it directly. It should work with any recent version of Photoshop.

18 May 2013, 2:48 am

Idea: Photoshop layers should pop open to reveal separate layers in the project window, just like 3d files pop open to reveal separate objects. Then they could be drag dropped as if they were separate textures.

18 May 2013, 3:03 am

Excellent, had not thought to script Photoshop itself, now to spend the day learning how powerful or not extendscript is :-)

pixnlove
18 May 2013, 8:01 am

I have a very similar workflow, thank you for sharing.
I know that Photoshop is the industry standard, but please note that everything mentioned here can be done as well in Gimp using Python script. It also has as a very powerful XML api too!

JustinISO
20 May 2013, 10:55 am

@BRETT BIBBY

I ran it in photoshop and realized my mistake. I had my background layer outside of a group, I created a group to put it in and that problem was fixed. However, when I run the script to import the HOG scene, all of the “item:” objects are drawn in the middle of the scene and not in their position from the Photoshop file.

nirta
21 May 2013, 5:32 am

Where is part two…?

imaginaryhuman
21 May 2013, 6:25 am

Yeah waiting on part 2

Sraya
22 May 2013, 7:54 am

Hello, Brett, can you add an example of full export script? Thanks

Brett Bibby
22 May 2013, 8:14 am

Hi all,
Part 2 will be published tomorrow. Sorry about the delay, it was meant to be published earlier but it was our mistake.

@sraya If you download the package you will get the entire exporter. There simply wasn’t space to repeat it within the article.

Cheers,
Brett

23 May 2013, 10:52 am

Can you post full js export to XML code? How are you writting a xml on windows?

Mark Walters
23 May 2013, 2:06 pm

@justiniso To avoid the items being centered in Unity, make sure your units in Photoshop are set to pixels and not the default of inches. Re-export and re-import. That should fix it. Worked for me when I ran into that same issue.

Brett Bibby
23 May 2013, 8:52 pm

@mark

Yes, thanks for posting this! You can fix it in script too by doing this at the beginning of the main function (but AFTER any code that can return) so it doesn’t mess with the user settings:

var savedRulerUnits = app.preferences.rulerUnits;
var savedTypeUnits = app.preferences.typeUnits;
app.preferences.rulerUnits = Units.PIXELS;
app.preferences.typeUnits = TypeUnits.PIXELS;

And then restoring it when done at the end of the main function:

app.preferences.rulerUnits = savedRulerUnits;
app.preferences.typeUnits = savedTypeUnits;

I have updated the package with a modified exporter that adds the above code to fix the issue. Thanks!

Cheers,
Brett

2 Jun 2013, 8:55 am

Have you ever thought about adding a little bit more than just your articles? I mean, what you say is valuable and everything. Nevertheless think of if you added some great images or video clips to give your posts more, “pop”! Your content is excellent but with images and video clips, this website could definitely be one of the most beneficial in its field. Great blog!

Ruben
10 Jun 2013, 10:32 am

Do you have any idea how a script can be hidden in Photoshop?

Chowdery
10 Jul 2013, 3:54 pm

I know that you’re calculating the top-left position in the above, however do you know if it’s possible to get the bottom-right position? I’m a bit new to JavaScript and am trying to have each individual images size exported to XML, which I could do by calculating the bottom-right position however I’m a bit confused as to how to do this using code similar to your above.

Brett Bibby
10 Jul 2013, 4:00 pm

@chowdery

In the exporter, just after the trim on line 222 you can simply derive the values as you alluded to:

var right = left + psd.width.value;
var bottom = top + psd.height.value;

14 Jul 2013, 9:46 pm

A person essentially help to make critically articles I’d state. This is the first time I frequented your website page and so far? I amazed with the research you made to create this actual put up amazing. Fantastic task!

22 Jul 2013, 8:24 am

Extraordinary issues listed here. My business is content to see your post. Thank you so much a whole lot and i’m looking forward to hint anyone. Might you generously fall us a mailbox?

26 Jul 2013, 12:47 am

Hey there, You have done an excellent job. I’ll definitely digg it and in my view suggest to my friends. I am confident they will be benefited from this web site.

Shandi Goligoski
26 Jul 2013, 2:53 pm

Keep up the good work i will return often.

Leave a Reply

Comments are closed.