Michael Christensen-Calvin /
March 2023
(6573 Words,
37 Minutes)
unity3dgamedevemacs
Sprite Library Assets
When I started on my Bartender game project, I was working with sprite sheets for the first time in 5+ years, and so was exploring some of the (relatively) newer options for integrating sprite sheets and sprite animations into Unity. After making a sprite animation, and then realizing that I would have to make totally new custom animations for each different character sprite sheet I wanted to use, I went searching for a less tedious solution.
After some searching I found out about the SpriteLibrary and SpriteResolver classes in the Unity 2D Animation package, and was excited to find that I could make one animation and and have that be used for all my sprites that shared the same sprite sheet layout.
Then I started working with ManaSeed sprites, and while the naked one worked just fine, I realized as I was adding in all the different layers that this was going to be almost as tedious as making new animations for everything because in order to make a new Sprite Library Asset with override textures, I would need to drag and drop each and every sprite into the library for each override. After doing three of them I decided I would find a way to automate this.
After lots of forum searching, I found that the develoeprs of the package had not provided an API into the SpriteLibrary asset class that I could use to edit sprite overrides with a script. I was bummed, but determined to find a solution. I solved this in a couple different ways, implementing the same tasks in different ways as my needs evolved and I wanted to make a more portable solution.
First Solution - Elisp
I took a look at the .spriteLib file and saw that it was fairly readable, so I could solve this with manual editing. As an avid emacs user, modifying text was my jam, and I am always looking for ways to use regex, but they are so far and few between that I need to relearn how to do it each time. So looking at the Libary Asset file:
if we look at the m_SpriteOverride line:
we can see that we explicity pass in :
- the guid: which is the guid of the source atlas, a Unity sprite in the Multiple mode.
- the fileID: attribute looks to be the ID of the sprite
- the type, which seems to always be 3, for all my files so I am going to ignore this for now.
Since I created my sprite sheets after the first by duplicating and then changing the target texture file oustide unity, the fileIds of all my sprites are the same (Don’t worry, the final solution doesn’t depend on this), so we just need to replace all the guids in the file in those sprite override lines.
I like regex, so using emacs’ handy RE-Builder tool, I can build my regex expression out and make sure it is targeting what I want it to:
Once I have everything captured how I want it to, I can hit C-c TAB and swap from the human-friendly string to the read mode, which adds all the insane escape sequences needed for lisp to read a regex string:
I can then put this into a function so that I can call it on a whole file:
It was at this point that while I had fun, I realized how inflexible this solution was, and that I probaly should make a tool in unity. However the regex stuff is still useful, as we will need that when modifying the file from the Unity Tool.
Writing a Unity Tool
For those that want to skip ahead, the full source code is available here.
While my regex-replace function inside of emacs got the job done for one or a couple, it was very inflexible with some serious limitations:
All the fileIDs had to be identical between the source atlas and the target atlas.
It can only swap to a single file. If the SpriteLibrary has sprites from a number of different files, you could need to run it multiple times.
We could solve both of these with elisp functions and searching through files, but it’s a lot more work with little gain. Since we are doing this to work in Unity, we might as well make a Unity tool to solve this for us.
Problem 1 is an issue because this criteria only occurs is if you duplicate the sprite within unity, and then go outside of unity and change the texture at that path, leaving the meta file intact. While this is how I do lots of these changes, its not the normal work flow, and is also tedious. Unity does provide an API that lets you copy over the sprite meta data from one to another, its just a little wierd how to do it. I found this forum thread giving a solution that you can modify to your needs if you are so inclined.
The advantage of a Unity Tool is that we can take advantage of the fact that Unity has an AssetDatabase that already stores connections between objects. We have the guid, and instead of manually searching through meta data looking for this field from emacs, we can just ask Unity to give us the asset with that guid. So what we can do is lookup the original texture referenced by the library, and categorize its sprites by name. If we do this for the target texture as well, then we can just lookup the new fileID that we would need for the new file, even if they do not match up.
Lets write a tool in Unity to do this for us:
First we create the EditorWindow we will run the tool from:
Define some storage data classes:
First we want to have a class to store the fileID, name and file guid.
Then we want to store a collection of these SpriteInfo classes on a per texture basis:
Looking up the source texture.
Great, we have our structures and are ready to fill them with data. Unfortunately, The SpriteLibrary class doesn’t like to give us information, and since we are already going to have to mess with the files themeselves, lets just say fuck it and read the file directly looking for a guid.
First we need use the AssetDatabase to lookup the filename for the SpriteAssetLibrary.
But because we are going to be reading the text of the file data, we are going to be using System.IO to read this rather than Unity, and so the relative “Assets/….” path doesn’t give us all the information that we need. We need to combine the Application.dataPath with this:
But when we test this we get: “/home/username/unityrepo/Assets/Assets/library.spriteLib”, which isnt a real path. I’m sure there is a better way to fix this, but I am tired so I just want to get it working, so lets hack it together to make:
Aaaand BAM, we have our full file path and can read it.
Next up we are just want to look for our first mSprite definition, and extract its guid. Then we can bail on reading the file as we aren’t ready to mess with the rest of the file yet. For this we will use a FileStream and StreamReader, and will just search each line for match to our regex pattern and if we find it, extract the group that contains our guid:
Lastly we just want to ask Unity to load this file and return the Texture2D from it:
And we have the Texture2D that our master SpriteLibrary is currently reading sprites from!
This still only allows for a single texture to be read, but we can extend this solution to do multiple lookups after we have our base case working.
So our final function is here:
Populating these structures.
Okay now we have our Texture2D source file from the SpriteLibrary, and are ready to look through it for out sprite data. Thankfully we dont need to mess with direct file reads for this, the AssetDatabase lets us load the sprites from a Multiple mode Sprite texture with:
Sweet, now we have a list of our sprites, now lets cast them as Sprite rather than UnityEngine.Object, and we can extract the guid and FileID from each one, and save it into our textureAtlasInfo
We are mostly done, we have the guid of our library’s current texture because we forcibly read it from the file, but we won’t always have the guid of the texture ready right away when we want to ge this information, so lets just generalize this function and grab our Guid and Path from the Texture2D we are passing in:
Awesome, we now have a function to extract out the data from a texture and return it, our final function looks like this:
Overriding these sprites in the SpriteLibrary
Okay now that we have the information we need, it’s time to go back to messing with file writing directly. Our method will take in the library we want to change, and the TextureAtlasInfo from our sourceTexture, and from the one we want to swap the library to reference:
Unlike in our elisp replacement, we aren’t jsut doing to replace the guid values and be done with it. We have the information available to us to look it up and be more precise with our searches.
First let’s do our shenanigans with the path name, and then read the file in:
Next up we are going to define out regex expressions:
Note that everything is in a capture group, this will make it much easier to do a replace search when we are actually modifying the files.
Also note that we are using the string pattern = @"" syntax, this let’s us not have to do a whole bunch of escaped characters. You can also see that in the m_SpriteOverride: pattern search we don’t reference the fileID: or guid: I orignally had these in there, but when you are looking at a spriteLibrary that doesnt have overrides set, this line can just be:
which means that being too specific won’t match it. Since we don’t need to read this line, just write out a new version of it after looking information up, we can just be less specific and capture the area that we want to write into, as we know what this is supposed to look like when it is filed.
Then we interate over and look for our matches, this is big chunk so we will go through it in parts, but seeing the whole thing for context first is helpful:
We are going through this file alternating what we are looking for.
First we look for an m_Sprite defintion. This will tell us what the original sprite is, and we can use that fileID to lookup its name.
We can also avoid updated unwanted items but checking if the guid matches the guid that we texture that we sourced ealier. This means that you could reference multiple textures, we just don’t support editing it all at once yet.
We then store this SpriteInfo object to be used later, and tell our loop that we want to look for start looking for an override line now.
Once in our override search mode, we will look for the line we want to replace:
When we have found it, we will lookup the new information we want to use, and replace the line with that information inserted correctly.
Finally, we just re-write the file with our updated text, and tell Unity to import it:
And now we are done! We have a function that will manually change the SpriteLibrary file to point to the texture we want it to.
Our final function looks like this:
Setting up the tool for use
Now that we have our functions that edit the files in the way we want, we can make our editor tool itself:
Operating on a single file
First let’s make the basic tool that takes a sprite libary, and a texture, and updates the library to use the new texture:
We just define our OnGUI() method, and grab the objects we need:
Targeting a directory
Suppose we want to do this in a bigger batch though. Rather than creating a libary asset clone and targeting a specific file, let’s target a directory of textures, and take in a SpriteLibrary file we want to copy. Then for each texture in that directory, we can create a copy fo the SpriteLibrary, modify it with our method above, and reimport it into unity:
Swapping between the two.
Rather than making two EditorWindow tools, lets just make some buttons and change their color based on if we are in that mode or not to make a pretty mode swap:
the CommonEditor namespace above is a static class I include with a number of other Common... classes in all of my projects as a submodule. It lets me move commonly used code out of project specific repositories and more easily share it. The ColoredButton() Method above is just a small static method:
I only addressed the first problem I talked about above, the second problem about having multiple sprite atlases being referenced in a SpriteLibraryAsset file didn’t really get addressed. I will get to that in a future post, extending this tool to keep track of multiple source atlases and multiple target atlases. This isn’t hard if we are trying to target a single folder, but when we get to doing it in bulk I need to think about a way to handle which target atlases to uses with which source sprite library, etc.. This will probably require some specific naming conventions or just putting everything into their own directories and selecting the parent…
Anyways, thank you for reading this long (for me!) post. I hope you got something out of it, I certainly had fun writing it, and it has helped me work a lot faster with these SpriteLibrary files.