A good workflow to smoothly import 2D content into Unity, Part II: Importing

May 23, 2013 in Technology

Recently I wrote about authoring your 2D content in Photoshop and then setting it up for exporting. Today’s post will cover importing the files into Unity.  You can find a link to download all the files at the end of the post.

Picking up where we left off

Unity will happily import our images and XML file, but the images will be imported as normal textures by default, and the XML file will be imported as a text asset. So we need to write an importer within Unity to process the files for our game.
You might think that writing an AssetPostprocessor would be the way to go and you’d be forgiven for thinking that. The problem with using AssetPostprocessor is that we’re limited in what we can do within that context and it won’t allow us to create a new material which we need to do. So the answer is to simply write an editor script to do what we want.

Our importer will need to:

  • Load and deserialize the exported XML meta-data,
  • Change the texture import settings for our images so that we can process them,
  • Create and texture atlas and pack all our textures into it,
  • Create mesh assets for each of our images,
  • Create GameObjects that reference the meshes,
  • And create a scene to hold our GameObjects.

Writing Editor scripts
As with the exporter, you can download the importer script here (). It should be placed in a folder called “Editor” in order for it to work.
If you haven’t written editor scripts before you’re not taking advantage of one of Unity’s most powerful features. Editor scripts are a superset of normal scripting, so if you already know how to write game scripts you already know the basics of editor scripting too.

Editor scripts are distinguished from regular game scripts in essentially three ways.
Firstly, editor scripts must be placed within a folder named “Editor”, or one of it’s sub-folders. Secondly, at the top of the scripting file you must place the following line:

using UnityEditor;

Finally, in order for the editor script to be called, it must derive from one of the editor sub-classes such as Editor, EditorWindow, ScriptableWizard, etc. You can use classes that don’t derive from these, but if you want to receive events and be interactive, these classes do so in a way similar to MonoBehaviour. For our purposes we’ll use the general Editor base class.

public class HogImporter : Editor
{
        ...
}

As with the exporter we won’t go over every line as the file is well commented, but let’s look at some of the more interesting bits.
To start, we need some way to invoke our importer, so we’ll put it on the Assets menu inside Unity like this:

[MenuItem("Assets/Create/Import HOG Scene...")]
static public void ImportHogSceneMenuItem()
{
        ...
}

So now we are sitting in the Assets menu, and when chosen we can execute a script, so now all we have to do is do something useful.

The first thing we’ll do is convert the XML file to something more useable such as a data class. When we wrote the exporter, we organized our data into a logical set of elements so that it will map easily into a data class. Our data class looks like the following and matches our XML file in structure and data types:

public class HogScene
{
        public Layer[] layers;
        public enum LayerStatus { Active, Found };
        public enum LayerType { Custom, Item, Scenery };
        public class Layer
        {
                public LayerType type;
                public string name;
                public LayerStatus layerStatus;
                public Rect bounds;
                public Image[] images;
        }

        public enum ImageType { Hotspot, Obscured, Whole, Shadow, Custom, Count };
        public class Image
        {
                public ImageType type;
                public string name;
                public float x;
                public float y;
                public float z;
        }
}

To actually convert the XML file into the data class, we use the built-in .NET serialization methods.

HogScene hogScene = (HogScene)DeserializeXml(assetPath, typeof(HogScene));
...
static private object DeserializeXml(string filePath, System.Type type)
{
        object instance = null;
        StreamReader xmlFile = File.OpenText(filePath);
        if(xmlFile != null)
        {
                string xml = xmlFile.ReadToEnd();
                if((xml != null) && (xml.ToString() != ""))
                {
                        XmlSerializer xs = new XmlSerializer(type);
                        UTF8Encoding encoding = new UTF8Encoding();
                        byte[] byteArray = encoding.GetBytes(xml);
                        MemoryStream memoryStream = new MemoryStream(byteArray);
                        XmlTextWriter xmlTextWriter =
                                new XmlTextWriter(memoryStream, Encoding.UTF8);
                        if(xmlTextWriter != null)
                        {
                                instance = xs.Deserialize(memoryStream);
                        }
                }
        }
        xmlFile.Close();
        return instance;
}

Since we’ll be creating a scene using code, it’s possible that the user already has that scene open, or another, and we need to be sure they get a chance to save changes. To do this we call:

if(EditorApplication.SaveCurrentSceneIfUserWantsTo() == false)
{
        return;
}

Then we simply create a new scene, deleting the old one first if it already exists:

string scenePath = baseDirectory + baseFilename + " Scene.unity";
if(File.Exists(scenePath) == true)
{
        File.Delete(scenePath);
        AssetDatabase.Refresh();
}
// now create a new scene
EditorApplication.NewScene();

Next up is to fix the texture import settings. By default textures aren’t readable and we need to read them in order to put them into an atlas. While we’re at it, we also need to change some of the other settings.

List<Texture2D> textureList = new List<Texture2D>();
for(int layerIndex = 0; layerIndex < hogScene.layers.Length; layerIndex++)
{
        for(int imageIndex = 0;
                imageIndex < hogScene.layers[layerIndex].images.Length;
                imageIndex++)
        {
                // we need to fixup all images that were exported from PS
                string texturePathName = baseDirectory +
                        hogScene.layers[layerIndex].images[imageIndex].name;
                Texture2D inputTexture =
                        (Texture2D)AssetDatabase.LoadAssetAtPath(
                        texturePathName, typeof(Texture2D));
                // modify the importer settings
                TextureImporter textureImporter =
                        AssetImporter.GetAtPath(texturePathName) as TextureImporter;
                textureImporter.mipmapEnabled = false;
                textureImporter.isReadable = true;
                textureImporter.npotScale = TextureImporterNPOTScale.None;
                textureImporter.wrapMode = TextureWrapMode.Clamp;
                textureImporter.filterMode = FilterMode.Point;
                ...
        }
}

Now we’re ready to create the new atlas material, texture and pack everything into it.

// create material
string materialPath = baseDirectory + baseFilename + " Material.mat";
// remove previous one if it exists
if(File.Exists(materialPath) == true)
{
        File.Delete(materialPath);
        AssetDatabase.Refresh();
}
// make a material and link it to atlas, save that too
Material material = new Material(Shader.Find("Transparent/Diffuse"));
AssetDatabase.CreateAsset(material, materialPath);
AssetDatabase.Refresh();
// load it back
material = (Material)AssetDatabase.LoadAssetAtPath(materialPath, typeof(Material));
// make a new atlas texture
Texture2D atlas = new Texture2D(2048, 2048);
// to make an atlas we need an array instead of a list
Texture2D[] textureArray = textureList.ToArray();
// pack it with all the textures we have
Rect[] atlasRects = atlas.PackTextures(textureArray, 0, 2048);
// save it to disk
byte[] atlasPng = atlas.EncodeToPNG();
string atlasPath = baseDirectory + baseFilename + " Atlas.png";
if(File.Exists(atlasPath) == true)
{
        File.Delete(atlasPath);
        AssetDatabase.Refresh();
}
File.WriteAllBytes(atlasPath, atlasPng);

Since we have created all the basic parts we need, we can now loop over the images, create meshes, create the GameObjects and put them into the scene. It’s a fair amount of code so it isn’t repeated in it’s entirety, but certainly the most interesting part has to be creating the 2D sprites in Unity. You might be surprised to find out how simple and easy it is. I use a simple function to do it for all my 2D stuff within Unity and it looks like the following.

static private void ConfigureGo(GameObject go, Texture2D texture, Material material,
Rect uvRect, string meshPath)
{
        // create meshFilter if new
        MeshFilter meshFilter = (MeshFilter)go.GetComponent(typeof(MeshFilter));
        if(meshFilter == null)
        {
                meshFilter = (MeshFilter)go.AddComponent(typeof(MeshFilter));
        }
        // create mesh if new
        Mesh mesh = meshFilter.sharedMesh;
        if(mesh == null)
        {
                mesh = new Mesh();
        }
        mesh.Clear();

        // setup rendering
        MeshRenderer meshRenderer =
                (MeshRenderer)go.GetComponent(typeof(MeshRenderer));
        if(meshRenderer == null)
        {
                meshRenderer =
                        (MeshRenderer)go.AddComponent(typeof(MeshRenderer));
        }
        meshRenderer.renderer.material = material;
        // create the mesh geometry
        // Unity winding order is counter-clockwise when viewed
        // from behind and facing forward (away)
        // Unity winding order is clockwise when viewed
        // from behind and facing behind
        // 1---2
        // | / |
        // | / |
        // 0---3
        Vector3[] newVertices;
        int[] newTriangles;
        Vector2[] uvs;

        float hExtent = texture.width * 0.5f;
        float vExtent = texture.height * 0.5f;

        newVertices = new Vector3[4];
        newVertices[0] = new Vector3(-hExtent, -vExtent, 0);
        newVertices[1] = new Vector3(-hExtent, vExtent, 0);
        newVertices[2] = new Vector3(hExtent, vExtent, 0);
        newVertices[3] = new Vector3(hExtent, -vExtent, 0);

        newTriangles = new int[] { 0, 1, 2, 0, 2, 3 };
        uvs = new Vector2[4];
        uvs[0] = new Vector2(uvRect.x, uvRect.y);
        uvs[1] = new Vector2(uvRect.x, uvRect.y + uvRect.height);
        uvs[2] = new Vector2(uvRect.x + uvRect.width, uvRect.y + uvRect.height);
        uvs[3] = new Vector2(uvRect.x + uvRect.width, uvRect.y);
        Color[] vertColors = new Color[4];
        vertColors[0] = Color.white;
        vertColors[1] = Color.white;
        vertColors[2] = Color.white;
        vertColors[3] = Color.white;

        // update the mesh
        mesh.vertices = newVertices;
        mesh.colors = vertColors;
        mesh.uv = uvs;
        mesh.triangles = newTriangles;
        // generate some some normals for the mesh
        mesh.normals = new Vector3[4];
        mesh.RecalculateNormals();

        if(File.Exists(meshPath) == true)
        {
                File.Delete(meshPath);
                AssetDatabase.Refresh();
        }
        AssetDatabase.CreateAsset(mesh, meshPath);
        AssetDatabase.Refresh();
        meshFilter.sharedMesh = mesh;
        // add collider
        go.AddComponent(typeof(MeshCollider));
}

That may seem like a lot of code, but it’s actually quite straightforward and simple.
Finally, after we built all our 2D sprite assets, we simply fix-up the camera so that it will render our scene pixel perfect like it was originally in Photoshop.

// setup our game camera
Camera.main.gameObject.AddComponent<HOGController>();
position = Vector3.zero;
position.z = -hogScene.layers.Length;
Camera.main.transform.position = position;
Camera.main.isOrthoGraphic = true;
Camera.main.orthographicSize = (768.0f/2.0f);
Camera.main.nearClipPlane = 1;
Camera.main.farClipPlane = hogScene.layers.Length + 1;
RenderSettings.ambientLight = Color.white;

If you look through the importer you should be able to follow it pretty easily.

If everything went well, after running the importer script you should see the scene fully composed and running in Unity.

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

Comments (20)

Subscribe to comments
  1. Brett Bibby

    July 24, 2013 at 10:23 am / 

    @VICENTE

    Sorry about the delay in replying. If you’re targeting a platform with unsupported texture type, just change the editor script that modifies the atlas format to one that it supports. This solution is a bit similar to 2D toolkit and they both solve a similar problem in different ways. You can use this method along with 2D Toolkit, but they don’t directly work together.

    @SEVOSHA

    Hmm, strange. Is there any errors? Can you email your XML file so I can take a look? It’s brettb (at) unity3d.com.

  2. Sevosha

    July 21, 2013 at 3:40 pm / 

    I tried to import a previously exporting scene and nothing happened. I click import HOG scene, then find my XML, pick it up and … silence.
    what am I doing wrong?

  3. Vicente

    July 14, 2013 at 9:15 pm / 

    Also, I’m having this error:

    Unsupported texture format – needs to be ARGB32, RGBA32, RGB24 or Alpha8

  4. Vicente

    July 14, 2013 at 5:18 pm / 

    How does this process work with another tools like 2D toolkit? This one creates his own atlas

  5. Brett Bibby

    May 29, 2013 at 5:08 pm / 

    @yohan

    Sure. This tutorial is for a static screen. But it would be easy to modify and optimize for an endless runner too. Just put all the blocks into a different layer, tag them with whatever meta-data you need, export. On import, create the scene along with prefabs of the individual pieces, hide them into a pool, then spawn them out of there. I have made such types of things before using the same process outlined in the tutorial. The point is to modify it and use it in ways that works for you. Have fun!

  6. Yohan Hadjedj

    May 27, 2013 at 5:07 am / 

    @Brett
    Yes correct, you can move the camera, but this technique do not work in all kind of games. Take for example a 2D infinite runner where you also need all of the screen (at least horizontally), or games like Candy Crush were you can’t do that.

  7. Brett Bibby

    May 23, 2013 at 9:05 pm / 

    @richard

    There are a lot of things you can’t do in an AssetPostprocessor that I need such as generating a mesh, making a prefab of it and getting the material assigned all within a single postprocessor. It’s something we’re looking at changing in the future, but for now, you have to run the importer at a time when you have available all the editor functions for asset creation and modification.

  8. Richard Harrington

    May 23, 2013 at 9:00 pm / 

    “it won’t allow us to create a new material which we need to do”
    Not sure what you mean by that – I have several AssetPostprocessor setups that create and assign materials. Just use the OnAssignMaterialModel method: http://docs.unity3d.com/Documentation/ScriptReference/AssetPostprocessor.OnAssignMaterialModel.html

  9. Brett Bibby

    May 23, 2013 at 8:48 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

  10. Mark Walters

    May 23, 2013 at 12:53 pm / 

    Thanks for the great tutorial!
    FYI, something I ran into that should be mentioned is that Photoshop’s units need to be changed to pixels. By default the setting is inches, which on export will screw up all the positions in the xml doc.
    Perhaps that could be checked (and possibly changed) in the Photoshop script.
    Thanks again!

  11. Georges Paz

    May 23, 2013 at 12:11 pm / 

    Even if we aren’t working in a 2D project, those tips are really welcome to anyone willing to jump into the 2D side of Unity. The only thing missing is the GUI system! :)
    Good job!

  12. Brett Bibby

    May 23, 2013 at 7:41 am / 

    @yohan

    Right, if you want to support odd sizes, then what I did was add pan/zoom to the controller. I stripped this out of the code. Basically if I pinched zoom/scrollwheel or pan the scene I call an update bounds and position routines. The sizes in the routines are based on the size of the authored art, and this will let it move within the bounds. Of course, if it was a web page, you should change the player size to be the same size as the authored content.

    private void UpdateCameraBounds()
    {
    // given the orthographic size, calc the camera bounds
    cameraBounds.x = ((Camera.main.pixelWidth/Camera.main.pixelHeight) * Camera.main.orthographicSize) – 512;
    cameraBounds.y = Camera.main.orthographicSize – 384;
    cameraBounds.width = -cameraBounds.x * 2;
    cameraBounds.height = -cameraBounds.y * 2;
    }

    private void UpdateCameraPosition()
    {
    // clamp to cameraBounds
    Vector3 newCameraPosition = Camera.main.transform.position;

    // drag the scene within the bounds
    if(newCameraPosition.x < cameraBounds.x)
    {
    newCameraPosition.x = cameraBounds.x;
    }
    if(newCameraPosition.x > cameraBounds.x + cameraBounds.width)
    {
    newCameraPosition.x = cameraBounds.x + cameraBounds.width;
    }
    if(newCameraPosition.y < cameraBounds.y)
    {
    newCameraPosition.y = cameraBounds.y;
    }
    if(newCameraPosition.y > cameraBounds.y + cameraBounds.height)
    {
    newCameraPosition.y = cameraBounds.y + cameraBounds.height;
    }
    Camera.main.transform.position = newCameraPosition;
    }

  13. Yohan Hadjedj

    May 23, 2013 at 7:16 am / 

    @brett

    Yup thanks, but doing that will “crop” your image and scene.

    Check out here : http://imageshack.us/a/img841/7815/pixelperfectissue.png

  14. Brett Bibby

    May 23, 2013 at 6:39 am / 

    @yohan

    It’s a good question, glad you asked. In this case I set the orthographic size to 768/2 as the default for the camera so it looks correct in the editor window. In actuality the camera is set to Screen.height/2 on Start in the HOGController (line 60) like this:

    Camera.main.orthographicSize = (Screen.height/2.0f);

    So the line you’re talking about is simply to set it up in the editor, but the game will adapt to the screen it’s running on.

  15. Yohan Hadjedj

    May 23, 2013 at 5:39 am / 

    Hi,

    Thanks for the articles!

    Looks good, I’ll try to make articles to detail my 2D workflow because it’s different from yours, and I want people to tell me how I can improve it.

    But one question though, I think this is a webplayer game so you don’t have the issue, but you’ve set the camera orthographic size, to half the size of the height (768) :
    Camera.main.orthographicSize = (768.0f/2.0f);

    What if your game is running in a 960×640 window for example ? You will have some borders with nothing on them at the left and right… and if you change the orthographic size, you will lose the pixel perfect.

    Thanks !

  16. Ashkan

    May 23, 2013 at 5:01 am / 

    This workflow is generally really nice even for GUIs. The free range games has some asset on asset store which allows making GUIs from PSD files and these kind of solutions are really nice cause they allow artists to do much without relying on devs and also without doing time consumming tasks twise.

    Microsoft’s blend product does the same thing for code and interface separation in a so much powerful way.

  17. Brett Bibby

    May 23, 2013 at 3:02 am / 

    @David I feel both your pain and joy, been there many times until we switched to Unity. Glad you found it helpful. We saved so much time and money in development, it allowed us a lot of upside opportunity. Good luck!

  18. David

    May 23, 2013 at 2:55 am / 

    Amazing. I’m used creating Casual Adventure Games for BFG using C++ and some f… millions lines of code. I was planning to write another heavy engine to switch our production to Unity. The PSD workflow is amazing, and your post is just amazing. Thanks Brett.

  19. thanks!

    May 23, 2013 at 2:16 am / 

    i had a question about tranparent PNGs. what is the best way to create and import them?

  20. thanks!

    May 23, 2013 at 2:13 am / 

    thanks brett!

Comments are closed.