We recently released Donut Get! on iPhone and Android. It was originally developed in Flash and we made the mobile ports in Unity. One of the challenges of porting was figuring out how to bring the game’s Flash animation into Unity. Here’s the “quick ‘n dirty” solution I came up with.
Exporting Animation Spritesheet from Flash
The first step was figuring out a good way to export the character animation from Flash. I needed a program that could export a sheet at a uniform size that would be in power of 2’s so it’ll work as a texture (256×256, 512×512, 1024×1024, etc).
I ended up using Keith Peters’ SWFSheet, it loads a swf and allows you to an export a spritesheet from it. I created an FLA for each animation with the dimensions of 256×256. I placed the cop Movieclip in the center, extended the timeline to the appropriate amount of frame and let ‘er rip!
The result is a sprite sheet image that looks like this. I exported it at 2048×2048 but dropped it down to 1024×1024 within Unity for its release.
The Fuzzy Textures Edges Removal
You want to import your textures into Unity as PSDs, especially if there are alpha channels. Once imported, Unity can convert it to whatever type of texture you need with the texture properties panel. One problem I encountered early one was fuzzy white edges around the black outlines of the characters and backgrounds. I found a good trick to get around the problem on Unity Answers.
- Download and install the Flaming Pear “Free Plugins” pack (near the bottom of that list)
- Open your PNG in photoshop.
- Go to Select -> Load Selection and click OK.
- Go to Select -> Save Selection and click OK. This will create a new alpha channel.
- Now Deselect all (Ctrl-D or Cmd-D)
- Select Filter -> Flaming Pear -> Solidify B
I jacked this directly from the thread here. Also in my PSD I made a copy of the original, hid it, and applied the filter to the copy just in case I needed to revert back to the original. I did encounter issues with the alpha channel not working correctly sometimes, you may also have to delete the “Alpha” channel in the Photoshop Channels window to try again.
The Plane
Unity is a 3d game engine. As such, I needed to create a 3d world for my 2d artwork. I used the textures on planes, like billboards. The problem with Unity’s built in plane is that it has too many subdivisions so it uses a lot of unnecessary polys. Fortunately on the Unity forums, I found a CreatePlane.cs script that created a plane models for me. I tried to match up the proportions (width vs height) to a similar ratio as the images, so I didn’t have to scale the models for them to look correct in game. The result was something like this:
Yeah, it looks 2D but it’s all 3D mang!
The 2D Shader
For DONUT GET! I needed a simple shader that didn’t have any lighting and also had transparency. I found some Unlit/Transparent shaders that worked well but I needed something that allowed double-sided polygons. I also needed a Color channel to use to apply tints and alpha fades. Here’ what I came up with:
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 36 37 38 39 40 41 42 43 44 45 46 | Shader "Sokay/Simple Shader" { Properties { _Color ("Tint (A = Opacity)", Color) = (1,1,1,1) _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {} } SubShader { Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"} LOD 100 ZWrite Off Blend SrcAlpha OneMinusSrcAlpha // BACKFACING PASS Pass { Lighting Off Cull Front ColorMaterial AmbientAndDiffuse SetTexture [_MainTex] { ConstantColor [_Color] combine texture * constant } SetTexture [_MainTex] { combine previous * primary } } // FRONT PASS Pass { Lighting Off Cull Back ColorMaterial AmbientAndDiffuse SetTexture [_MainTex] { ConstantColor [_Color] combine texture * constant } SetTexture [_MainTex] { combine previous * primary } } } } |
I wasn’t able to find a way to combine the front and back pass or use a single SetTexture command, although I believe there may be a way, possibly without using Shader language.
The 2D Sprite Class
Here is the class I created to run through the animation.
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | using UnityEngine; using System.Collections; public class SpriteAnimation : MonoBehaviour { private bool initialized = true; // don't run anything until initalized private bool active = false; // wait till active to do anything private bool startedRoutine; public string name = "Sprite Animation Name!"; public Material sheetMaterial; public Texture sheetTexture; public bool isPlaying; public bool loop = true; public string completeFunction; private bool animationComplete = false; // am I done playing yet? public int _totalFrames; public int _currentFrame = 1; public float sheetWidth = 8f; public float sheetHeight = 8f; public int tileWidth; // width in pixels of individual tiles public int tileHeight; // height in pixels of individual tiles public float updateRate = 1f; // update rate in seconds or something public delegate void OnComplete(); // need to figure this delegate shiet out! public void Initialize( Material _material , Texture _texture, int _totalAniFrames, int _tileWidth, int _tileHeight, int sheetWidth, int sheetHeight) { initialized = true; // get the party started! sheetMaterial = _material; sheetTexture = _texture; _totalFrames = _totalAniFrames; sheetWidth = sheetWidth; sheetHeight = sheetHeight; tileWidth = _tileWidth; tileHeight = _tileHeight; } // play an animation! public void Play() { //Debug.Log("Play: " + name); if (!active) { active = true; if (!isPlaying) { isPlaying = true; animationComplete = false; sheetMaterial.mainTexture = sheetTexture; StartCoroutine(Draw()); if (!startedRoutine) { startedRoutine = true; } } } } // play an animation! public void Stop() { //Debug.Log("Stop: " + name); isPlaying = false; active = false; _currentFrame = 1; } // draw function public IEnumerator Draw() { while(isPlaying) { //Debug.Log("Draw: " + name); if (initialized) { if (active) { if (_currentFrame > _totalFrames) _currentFrame -= _totalFrames; if (_currentFrame < 1) _currentFrame += _totalFrames; int _offsetX = (_currentFrame - 1) % (int) sheetWidth; int _offsetY = (_currentFrame - 1) / (int) sheetWidth; //Set the texture to the indicated offset sheetMaterial.mainTextureOffset = new Vector2 (_offsetX / sheetWidth, 1f - ((_offsetY + 1) / sheetHeight)); //Change the scale of the texture sheetMaterial.mainTextureScale = new Vector2 ( 1f / sheetWidth, 1f / sheetHeight); } } // next steps! _currentFrame++; if (_currentFrame > _totalFrames) { if (loop) { _currentFrame = 1; } else { _currentFrame = _totalFrames; // stop at the last frame animationComplete = true; if (!string.IsNullOrEmpty(completeFunction)) CallCompleteFunction(); break; } } // run this again! if (isPlaying) { yield return new WaitForSeconds(updateRate); } else { yield return null; break; } } } void Awake() { sheetMaterial = renderer.material; // modify this instance of the material //sheetMaterial = renderer.sharedMaterial; // modify all materials of this type } // Looks within this gameobject for a function to call void CallCompleteFunction () { //Debug.Log("CallCompleteFunction >>> name: " + name + " , fn: " + completeFunction); gameObject.SendMessage(completeFunction, gameObject, SendMessageOptions.DontRequireReceiver); } public bool GetAnimationComplete() { return animationComplete; } } |
And I used code like this within the Player class component to control the different SpriteAnimation components.
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 | public void PlayAnimation( string _name ) { if (_name != currentAnimationName) { // find animation with the matching name! SpriteAnimation[] sprites = gameObject.GetComponents(); // stop currently playing animation! if (currentAnimation) currentAnimation.Stop(); // save a reference foreach (SpriteAnimation sprite in sprites) { if (sprite.name == _name) { currentAnimation = sprite; break; } } // play that shiet homie! currentAnimation.Play(); currentAnimationName = _name; } } |
The basic ideas I used to write this, I learned from this tutorial. I had used that idea of scaling the texture size to work out a system for creating tile-based stages with spritesheets. I adapted that class to make an animation class, and learn a bit about coroutines in Unity.
The Warnings
This may not be the perfect solution but its a start. This was all I needed for Donut Get! but there were some issues by using this exact same code.
This totally wastes texture space. Many animations in the game were only using 50-70% of the texture sheet size. These texture sheets can eat up a lot of mobile memory quickly, and that’ll crash older phones and tablets easily, so you only want to use as much as you need. Ideally, this code would allow multiple animations per sheet, allowing you to select which tile will be the start of the animation.
There may be other bugs or errors I didn’t encounter for this game, if you’ve got suggestions I’m down to listen! 😉
Play DONUT GET!
Now that you’ve seen the shader, play the game!
- For iPhone & iPad at http://www.donutget.com/download/iphone/
- For Android at http://www.donutget.com/download/android/
- For the web at http://www.sokay.net/play/donut-get