Search Unity

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

May 23, 2013 in Games | 8 min. read
Topics covered
Share

Is this article helpful for you?

Thank you for your feedback!

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.

May 23, 2013 in Games | 8 min. read

Is this article helpful for you?

Thank you for your feedback!

Topics covered