Cripes it's been so long since my last long-form post, but the project is still alive and well! I've been showing new stuff every week on Twitter but I haven't made time for much blogging lately, sorry.
NOTE: If you want to follow my progress, please go to my Twitter @lunoland, as it's the easiest place for me to consistently post updates. I am quite literally begging you to follow me on Twitter because this blog took me 12 actual hours to write and nobody's got time for that. If you just want the big updates like the release date or when public beta begins, sign up for the email list.---
This devlog entry is all about my character animation system, replete with helpful tips and code snippets!
In the last 2 months I've created a whopping 9 new player actions (fun stuff like shield blocking, dodge rolling, and weapons), 17 new held items, 3 armor sets (plate, silk, and leather), and 6 hair styles. I also finished all the requisite automation and tools and everything is in the game. Here's how I did it!
I hope this information proves useful, and demonstrates that it doesn't take a genius to build some of these tools/automation yourself. The PitchThe original idea was to see if it was at all feasible to have stacked sprites with synchronized animators to create a sort of modular character with interchangeable hair, equipment, and held items. Snappy, hand-draw pixel animation with a truly customizable character.
Of course these features are commonly found in 3D, 2D games with pre-rendered sprites, or 2D games with paper-doll animation, but as far as I know there aren't many games with hand-drawn animation and modular characters (usually because it's too tedious).
I unearthed this ancient GIF from my first month of Unity. This modular sprite thing really was one of my very first experiments in game dev! I built the prototype using Unity's animation system and then added a single shirt, a single pair of pants, one hair style, and 3 items to prove the concept. This required 26 individual animations.
At the time I was doing all my animation in Photoshop and I hadn't bothered to automate anything so it was very tedious. Afterwards I thought, "Ok the basic idea works, I'll do more animations and equipment later." Years later, apparently.
Back in March I designed a lot of the armor (per my previous post) and I could see ways in which this process could be made more manageable. I kept holding off on implementing anything because even with automation I was nervous it wouldn't work out.
I was kind of expecting to have to chuck the customization and create a single protagonist that you play as like most other hand-animated games. But I had my roadmap, and it came time to see if I could tackle this beast!
Spoiler: Everything worked out famously. Read on to discover my ***secrets*** Modular Sprite SystemI. Know Your LimitsI did a lot of art testing and time tracking ahead of time to figure out what was required for this thing to look good and if that level of quality would even be feasible for me.
I wrote down all my animation ideas and organized them into a spreadsheet where I ranked them across different dimensions like utility, cool factor, and re-usability. Somewhat surprisingly, the animation for throwing an item ranked highest on this list (potions, bombs, knives, axes, a ball...checks out).
I came up with a score for each animation and tossed anything that ranked poorly. I had originally planned to do 6 armor sets, but quickly realized that would be too much and cut 3 of them.
The time tracking piece of this was huge and I really recommend it to help you set bounds on questions like "How many enemies can I have in the game?". I was able to extrapolate a good estimate from just a few tests. I continued to track my time and re-calibrate my expectations as I worked on the animations.
To put my money where my mouth is, I'm sharing a copy of my work log for the last 2 months. Note that this time is in addition to my regular job where I work 30hrs a week:https://docs.google.com/spreadsheets/d/1Nbr7lujZTB4pWMsuedVcgBYS6n5V-rHrk1PxeGxr6Ck/edit?usp=sharingII. Palette Swapping For A Better TomorrowWith clever use of color in your sprite design, you can draw a single sprite and get many different variations via palette swapping. Different colors sure, but you can also create various elements that can be turned on and off (e.g. by swapping colors with transparency).
Each armor set has 3 variations, and by mixing and matching the tops and bottoms many looks are possible. I plan to implement a system where you can collect one set of armor for your character's appearance, and another set of armor for stats (a la Terraria).
As I worked, I was pleasantly surprised to discover all kinds of cool combinations. Equipping a plate top with a silk bottom results in a battle mage vibe. The best way to do palette swapping is to use the colors in your sprite to encode a value that you will later use to lookup the actual color from a palette. I'm glossing over the details here, but this video is an OK place to get started:
So I won't bother with all the ins and outs, but what I
will discuss instead are the ways in which you can apply this technique in Unity, and the trade offs for each.
1. Lookup texture for each paletteThis is the best strategy for variant enemies, backgrounds, and anything where many sprites will share the same palette/material. Different materials can't be batched even if they use the same sprite/atlas. Dealing with textures is kind of a pain, but you can change palettes at run time by swapping materials, using SpriteRenderer.sharedMaterial.SetTexture, or MaterialPropertyBlock if you need different palettes for each instance of a material. Sample shader fragment function here:
sampler2D _MainTex;
sampler2D _PaletteTex;
float4 _PaletteTex_TexelSize;
half4 frag(v2f input) : SV_TARGET {
half4 lookup = tex2D(_MainTex, input.uv);
half4 color = tex2D(_PaletteTex, half2(lookup.r * (_PaletteTex_TexelSize.x / 0.00390625f), 0.5));
color.a *= lookup.a;
return color * input.color;
}
2. Color arrayThis was the solution I settled on since I need to swap the palettes every time the player's appearance changes (e.g. equipping items), and build some palettes dynamically (to reflect the player's choice of hair and skin color). For these purposes, I found arrays to be much easier to work with at runtime and in the editor.
sampler2D _MainTex;
half4 _Colors[32];
half4 frag(v2f input) : SV_TARGET {
half4 lookup = tex2D(_MainTex, input.uv);
half4 color = _Colors[round(lookup.r * 255)];
color.a *= lookup.a;
return color * input.color;
}
I represent my palettes with a ScriptableObject type and use a MonoBehaviour tool to edit them. After editing palettes a lot in Aesprite as I worked on the animations I got a feel for what tools I needed and designed these scripts accordingly. If you want to make your own palette editing tool, these are the must-have features I recommend implementing:
- Updating the palettes on various materials as you edit the colors, showing the changes in real time.
- Name and reorder the colors in the palette (use a field to store the color's index, not its order in an array).
- Select and edit multiple colors at once. (PROTIP: You can copy/paste Color fields in Unity: just click one color, copy, click another color, paste, now they're the same!)
- Apply an overlay color to the entire palette
- Write the palettes to texture.
3. Single lookup texture for all palettesIf you want to switch palettes on the fly but also need batching to reduce draw calls, you can use this technique. It might be helpful on mobile, but it's also kind of a pain in the ass to use.
First, you need to pack all of your palettes into a single big texture. Then, you use the color that you set on Unity's SpriteRenderer component (AKA the vertex color) to determine the row that you'll read from the palette texture in your shader. Thus, the palette for that sprite is controlled by SpriteRenderer.color.Vertex color is the only property on a SpriteRenderer you can change without breaking batching (provided the materials are all the same).
In most cases it's best to use the alpha channel to control the index since you probably won't need to have a ton of sprites with different transparency.
sampler2D _MainTex;
sampler2D _PaletteTex;
float4 _PaletteTex_TexelSize;
half4 frag(v2f input) : SV_TARGET {
half4 lookup = tex2D(_MainTex, input.uv);
half2 paletteUV = half2(
lookup.r * _(PaletteTex_TexelSize.x / 0.00390625f),
input.color.a * _(PaletteTex_TexelSize.y / 0.00390625f)
)
half4 color = tex2D(_PaletteTex, paletteUV);
color.a *= lookup.a;
color.rgb *= input.color.rgb;
return color;
}
Ah, the wonders of palette swapping and layered sprites. So many Looks. III. Automate everything and use the right toolsAutomation was absolutely essential for this feature because I ended up with some 300 animations, and thousands of sprites.
The first step was to build an exporter for Aesprite to handle my crazy scheme of layered sprites using the handy
command line interface. It's just a perl script that loops through the layers and tags in my an Aesprite file and exports the images with a certain directory structure and naming convention that I can read in later.
Next I wrote an importer for Unity. Aesprite outputs a helpful JSON file with the frame data, so you can build animation assets programmatically. It was a bit annoying to go through the Aesprite JSON and write out this data type, so I'm including it here. You can easily load it in Unity using JsonUtility.FromJson<AespriteData> just be sure to run Aesprite with the --format 'json-array' option.
[System.Serializable]
public struct AespriteData {
[System.Serializable]
public struct Size {
public int w;
public int h;
}
[System.Serializable]
public struct Position {
public int x;
public int y;
public int w;
public int h;
}
[System.Serializable]
public struct Frame {
public string filename;
public Position frame;
public bool rotated;
public bool trimmed;
public Position spriteSourceSize;
public Size sourceSize;
public int duration;
}
[System.Serializable]
public struct Metadata {
public string app;
public string version;
public string format;
public Size size;
public string scale;
}
public Frame[] frames;
public Metadata meta;
}
From the Unity side, there were 2 places where I really got stuck: Loading/slicing a spritesheet, and building an animation clip. A clear example would have helped me immensely, so here's a snippet from my importer to spare you that pain:
TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter;
textureImporter.spriteImportMode = SpriteImportMode.Multiple;
SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length];
// Slice the spritesheet according to the aesprite data.
for (int i = 0; i < aespriteData.frames.Length; i++) {
AespriteData.Position spritePosition = aespriteData.frames[i].frame;
spriteMetaData[i].name = aespriteData.frames[i].filename;
spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h);
spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor.
spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom".
}
textureImporter.spritesheet = spriteMetaData;
AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate);
Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (i.e. the sheet itself).
for (int i = 1; i < assets.Length; i++) {
sprites[i - 1] = assets[i] as Sprite;
}
// Create the animation.
AnimationClip clip = new AnimationClip();
clip.frameRate = 40f;
float frameLength = 1f / clip.frameRate;
ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration.
float time = 0f;
for (int i = 0; i < keyframes.Length; i++) {
bool lastFrame = i == keyframes.Length - 1;
ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe();
keyframe.value = sprites[lastFrame ? i - 1 : i];
keyframe.time = time - (lastFrame ? frameLength : 0f);
keyframes[i] = keyframe;
time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f;
}
EditorCurveBinding binding = new EditorCurveBinding();
binding.type = typeof(SpriteRenderer);
binding.path = "";
binding.propertyName = "m_Sprite";
AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes);
AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim");
AssetDatabase.SaveAssets();
If you haven't yet, it's easy to get started creating your own tools. The basic trick is to chuck a GameObject into your scene with a MonoBehaviour on it that has the [ExecuteInEditMode] attribute. Add a quick button and you're off to the races! Remember that your own personal tools don't need to look good, they can be purely utilitarian:
[ExecuteInEditMode]
public class MyCoolTool : MonoBehaviour {
public bool button;
void Update() {
if (button) { button = false; DoThing(); }
}
}
For sprite stuff, it's not that hard to automate common tasks (for example, creating palette textures or batch replacing colors across many sprite files). Here's an example to get you started modifying your sprites:
string path = "Assets/Whatever/Sprite.png";
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter;
if (!textureImporter.isReadable) {
textureImporter.isReadable = true;
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
}
Color[] pixels = texture.GetPixels(0, 0, texture.width, texture.height);
for (int i = 0; i < pixels.Length; i++) {
// Do something with the pixels, e.g. replace one color with another.
}
texture.SetPixels(pixels);
texture.Apply();
textureImporter.isReadable = false; // Make sure textures are marked as un-readable when you're done. There's a performance cost to using readable textures in your project that you should avoid unless you plan to change a sprite at runtime.
byte[] bytes = ImageConversion.EncodeToPNG(texture);
File.WriteAllBytes(Application.dataPath + path.Substring(6), bytes);
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
Outgrowing Mecanim: A RantOver time, the prototype version of this modular sprite thing that I built using Mecanim had become the single biggest pain point when upgrading Unity because the API kept changing wildly and it was poorly documented. For a simple state machine you might think it would be reasonable to be able to inquire about which clip is in each state, or to change clips at runtime. Nope! For performance reasons, Unity bakes the clips into their states and makes you use this wonky override system to change them.
Mecanim isn't a bad tool per se, but I think it fails to deliver on its core promise of simplicity. The idea was to replace something that was perceived to be complicated and painful (scripting) with a simple thing (a visual state machine). However:
- Any non-trivial state machine quickly becomes an unwieldy spiderweb of nodes and connections with logic scattered across different layers.
- Simple cases get bogged down by the generalized requirements of the system. You have to create a new controller and define states/transitions just to play one or two animations. There is some performance overhead too of course.
- Comically, you end up writing code anyways because in order for your state machine to do anything interesting you need a script that calls Animator.SetBool et al.
- Re-using a state machine with different clips means duplicating it and swapping the clips manually. Moving forward, you have to make the same change in multiple places.
- If instead you'd rather modify what's in a state at runtime, you hit the wall. The solution is either a bad API or an insane graph with one node for every possible animation.
Rare footage of the Firewatch devs arriving in visual scripting hell. The funniest thing is when he shows the cleaner examples and they STILL look nuts. The audience is literally groaning at 12:41. Factor in the colossal maintenance effort described in the talk and, well, that's a big yikes from me. Many of these problems aren't even the fault of the Mecanim devs, they're just the natural result of incompatible ideas: you can't have a generalized system that is also simple, and representing logic with images is more complicated than using words/symbols (UML flow charts anybody?). I'm reminded of this bit from
Zak McClendon's 2018 Practice NYC talk, and if you have time to watch the whole thing I highly recommend it!
I get it though. Visual scripting is always being slammed by the write-your-own-engine aggro nerds who like, don't understand an artist's needs man. Also it's pretty undeniable that most code looks like inscrutable techno-jargon.
If you're kind of a programmer already and you're making a game with sprites, maybe think twice though. When I first started, I was intimidated into thinking that I could never write any engine-related thing better than the Unity devs.
Guess what buddy, turns out a sprite animator is just a script that changes a sprite every however many seconds.
Anyways, I did eventually write my own. I've since added animation events and some other project-specific features, but the basic version that I wrote in an afternoon covers 90% of what I need, is only 120 lines, and is available here for free:
https://pastebin.com/m9Lfmd94Thank you for coming to my TIG talk. Until next time!