home..

Modifying Unity SpriteLibrary sprites with code.

unity3d gamedev emacs

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:

    ...
    MonoBehaviour:
      m_ObjectHideFlags: 0
      m_CorrespondingSourceObject: {fileID: 0}
      m_PrefabInstance: {fileID: 0}
      m_PrefabAsset: {fileID: 0}
      m_GameObject: {fileID: 0}
      m_Enabled: 1
      m_EditorHideFlags: 0
      m_Script: {fileID: 11500000, guid: a5e6fedc2472449cead18ef23b5cb30d, type: 3}
      m_Name: 
      m_EditorClassIdentifier: 
      m_Library:
      - m_Name: idle_down
        m_Hash: 297738837
        m_CategoryList: []
        m_OverrideEntries:
        - m_Name: idle_down
          m_Hash: 297738837
          m_Sprite: {fileID: 1628358433, guid: 8b41dbacdf42456b3941cf3f90d5db18, type: 3}
          m_FromMain: 0
          m_SpriteOverride: {fileID: 1628358433, guid: 8b41dbacdf42456b3941cf3f90d5db18, type: 3}
        m_FromMain: 0
        m_EntryOverrideCount: 1
    ...

if we look at the m_SpriteOverride line:

    m_SpriteOverride: {fileID: 1628358433, guid: 8b41dbacdf42456b3941cf3f90d5db18, type: 3}

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:

img

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:

"\\(.*m_SpriteOverride: {fileID: \\)\\(.*\\)\\(, guid: \\)\\(.*\\)\\(,.*\\)"

I can then put this into a function so that I can call it on a whole file:

(defun my/sprite-lib-replace-override-file-guid(newguid)
 "Replace the sprite override guid with a new one in a spritelib file."
 (interactive "sNewGuid")
 (goto-char (point-min))
 (while (re-search-forward "\\(.*m_SpriteOverride: {fileID: \\)\\(.*\\)\\(, guid: \\)\\(.*\\)\\(,.*\\)" nil t)
   (replace-match (format "\\1\\2\\3%s\\5" newguid))))
;; This is the re-builder 'string' version of this search
;;"\(.*m_SpriteOverride.*\)\(fileID: \)\(.*\)\(, guid: \)\(.*\)\(,.*\)"

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:

  1. All the fileIDs had to be identical between the source atlas and the target atlas.
  2. 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:

public class SpriteLibaryCopier : EditorWindow
{
  [MenuItem("Toosl/Sprite Library Asset Copier")]
  public static void CreateEditorWindow()
  {
    SpriteLibaryCopier window = (SpriteLibaryCopier)EditorWindow.GetWindow(typeof(SpriteLibaryCopier));
    window.Show();
  }
}

Define some storage data classes:

First we want to have a class to store the fileID, name and file guid.

public class SpriteInfo
{
  public string guid;
  public string name;
  public long fileID;
  public SpriteInfo(string name, string guid, long fileID)
  {
    this.name = name;
    this.guid = guid;
    this.fileID = fileID;
  }
}

Then we want to store a collection of these SpriteInfo classes on a per texture basis:

public class TextureAtlasInfo
{
  public string guid;
  public Dictionary<long, SpriteInfo> fileIdLookup;
  public Dictionary<string, SpriteInfo> nameLookup;

  public TextureAtlasInfo(string guid)
  {
    this.guid = guid;
    fileIdLookup = new Dictionary<long, SpriteInfo>();
    nameLookup = new Dictionary<string, SpriteInfo>();
  }

  public void Add(SpriteInfo info)
  {
    if (!fileIdLookup.ContainsKey(info.fileID))
    {
      fileIdLookup[info.fileID] = info;
    }
    if (!nameLookup.ContainsKey(info.name))
    {
      nameLookup[info.name] = info;
    }
  }
}

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.

string libraryPath = AssetDatabase.GetAssetPath(library);

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:

string filePath = Path.Combine(Application.dataPath, libraryPath)

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:

string filePath = Path.Combine(Application.dataPath, libraryPath).Replace("Assets/Assets", "Assets");

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:

string textureGuid;
using (FileStream stream = File.OpenRead(filePath))
{
  using (StreamReader reader = new StreamReader(stream))
  {
    string line;
    while ((line = reader.ReadLine()) != null)
    {
      Regex regex = new Regex(@"m_Sprite: {fileID: (.*), guid: (.*), type: 3}");
      Match m = regex.Match(line);
      if (m.Success)
      {
        textureGuid = m.Groups[2].ToString();
        break;
      }
    }
  }
}

Lastly we just want to ask Unity to load this file and return the Texture2D from it:

string path = AssetDatabase.GUIDToAssetPath(textureGuid);
return AssetDatabase.LoadAssetAtPath<Texture2D>(path);

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:

public static Texture2D GetLibraryMasterSourceTexture(SpriteLibraryAsset library)
{
  string libraryPath = AssetDatabase.GetAssetPath(library); 
  // Because we are using a file stream to read the data, we need the actual path, not just the relative one.
  string filePath = Path.Combine(Application.dataPath, libraryPath).Replace("Assets/Assets", "Assets");
  string textureGuid = "";
  using (FileStream stream = File.OpenRead(filePath))
  {
    using (StreamReader reader = new StreamReader(stream))
    {
      string line;
      while ((line = reader.ReadLine()) != null)
      {
        Regex regex = new Regex(@"m_Sprite: {fileID: (.*), guid: (.*), type: 3}");
        Match m = regex.Match(line);
        if (m.Success)
        {
          textureGuid = m.Groups[2].ToString();
          break;
        }
      }
    }
  }
  string path = AssetDatabase.GUIDToAssetPath(textureGuid);
  return AssetDatabase.LoadAssetAtPath<Texture2D>(path);
}

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:

UnityEngine.Object[] _objects = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);

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

TextureAtlasInfo textureAtlasInfo = new TextureAtlasInfo(texGuid);
for (int i = 0; i < _objects.Length; i++)
{
  Sprite sprite = _objects[i] as Sprite;
  if (sprite == null)
  {
    Logger.LogError(1, $"Failed to cast object as Sprite, skipping {_objects[i].name}");
    continue;
  }
  string guid;
  long fileId;
  if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sprite, out guid, out fileId))
  {
    SpriteInfo info = new SpriteInfo(sprite.name, guid, fileId);
    textureAtlasInfo.Add(info);
  }
}

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:

string texGuid;
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(tex, out texGuid, out long _);
TextureAtlasInfo textureAtlasInfo = new TextureAtlasInfo(texGuid);

Awesome, we now have a function to extract out the data from a texture and return it, our final function looks like this:

public static TextureAtlasInfo LoadAtlasInfo(Texture2D tex)
{
  string texGuid;
  AssetDatabase.TryGetGUIDAndLocalFileIdentifier(tex, out texGuid, out long _);
  TextureAtlasInfo textureAtlasInfo = new TextureAtlasInfo(texGuid);
  string path = AssetDatabase.GetAssetPath(tex);
  UnityEngine.Object[] _objects = AssetDatabase.LoadAllAssetRepresentationsAtPath(path);
  for (int i = 0; i < _objects.Length; i++)
  {
    Sprite sprite = _objects[i] as Sprite;
    if (sprite == null)
    {
      Logger.LogError(1, $"Failed to cast object as Sprite, skipping {_objects[i].name}");
      continue;
    }
    string guid;
    long fileId;
    if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sprite, out guid, out fileId))
    {
      SpriteInfo info = new SpriteInfo(sprite.name, guid, fileId);
      textureAtlasInfo.Add(info);
    }
    else
    {
      Logger.LogError(0, $"Failed to load guid/fileID from object {sprite.name}");
    }
  }
  return textureAtlasInfo;
}

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:

public static void SetOverrideSprites(SpriteLibraryAsset library, TextureAtlasInfo sourceInfo, TextureAtlasInfo targetInfo) {}

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:

string libraryPath = AssetDatabase.GetAssetPath(library);
string filePath = Path.Combine(Application.dataPath, libraryPath).Replace("Assets/Assets", "Assets");
// Read the file in.
string[] lines = File.ReadAllLines(filePath);

Next up we are going to define out regex expressions:

string spritePattern = @"(.*m_Sprite: {fileID: )(.*)(, guid: )(.*)(, type: 3}.*)";
string overridePattern = @"(.*m_SpriteOverride: )({.*})(.*)";

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:

m_SpriteOverride: { guid: 0 }

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:

bool isLookingForOverride = false;
SpriteInfo sourceSprite = null;
// Read through them, replacing the sprite overide lines with their respective values from the new sprite target.
for (int i = 0; i < lines.Length; i++)
{
  string line = lines[i];
  if (!isLookingForOverride)
  {
    // Look for the a sprite match.
      Match match = Regex.Match(line, spritePattern);
    if (match.Success)
    {
      string textureGuid = match.Groups[4].ToString();
      string fileIdString = match.Groups[2].ToString();
      long fileId = System.Convert.ToInt64(fileIdString);
      if (sourceInfo.guid == textureGuid) // If the source guid is our source atlas.
      {
        if (sourceInfo.fileIdLookup.ContainsKey(fileId)) // Check if we found the sprite.
        {
          sourceSprite = sourceInfo.fileIdLookup[fileId];
          isLookingForOverride = true;
        }
        else
        {
          Logger.LogError(0, $"Atlas guid matched, but we couldn't find the sprite with id: {fileId}");
        }
      }
      else
      {
        Logger.LogError(0, $"Texture guid {textureGuid} did not match, skipping this one.");
      }
    }
  }
  else
  {
    // Otherwise look for the overide sprite match.
    Match match = Regex.Match(line, overridePattern);
    if (match.Success)
    {
      if (sourceSprite != null)
      {
        if (targetInfo.nameLookup.ContainsKey(sourceSprite.name)) // Try and find the sprite name in the other atlas
        {
          SpriteInfo info = targetInfo.nameLookup[sourceSprite.name];
          string replacementString = \\$"$1\\{\\{fileID: {info.fileID}, guid: {targetInfo.guid}, type: 3}}$3";
          string replaced = Regex.Replace(line, overridePattern, replacementString);
          line = replaced;
        }
      }
      else
      {
        Logger.LogError(1, $"Found an override match, but we don't have a corresponding SpriteInfo, something went wrong here.");
      }
      isLookingForOverride = false;
    }
  }
  output.Add(line);
}

We are going through this file alternating what we are looking for.

  1. 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.

if (!isLookingForOverride)
{
  // Look for the a sprite match.
  Match match = Regex.Match(line, spritePattern);
  if (match.Success)
  {
    string textureGuid = match.Groups[4].ToString();
    string fileIdString = match.Groups[2].ToString();
    long fileId = System.Convert.ToInt64(fileIdString);
    if (sourceInfo.guid == textureGuid) // If the source guid is our source atlas.
    {
      if (sourceInfo.fileIdLookup.ContainsKey(fileId)) // Check if we found the sprite.
      {
        sourceSprite = sourceInfo.fileIdLookup[fileId];
        isLookingForOverride = true;
      }
      else
      {
        Logger.LogError(0, $"Atlas guid matched, but we couldn't find the sprite with id: {fileId}");
      }
    }
    else
    {
      Logger.LogError(0, $"Texture guid {textureGuid} did not match, skipping this one.");
    }
  }
}

Once in our override search mode, we will look for the line we want to replace:

else
{
  // Otherwise look for the a sprite match.
  Match match = Regex.Match(line, overridePattern);
  if (match.Success)
  {
    if (sourceSprite != null)
    {
      if (targetInfo.nameLookup.ContainsKey(sourceSprite.name)) // Try and find the sprite name in the other atlas
      {
        SpriteInfo info = targetInfo.nameLookup[sourceSprite.name];

        string replacementString = @"$1/\{\{/fileID: {info.fileID}, guid: {targetInfo.guid}, type: 3/\}\}/ $3";
        string replaced = Regex.Replace(line, overridePattern, replacementString);
        line = replaced;
      }
    }
    else
    {
      Logger.LogError(1, $"Found an override match, but we don't have a corresponding SpriteInfo, something went wrong here.");
    }
    isLookingForOverride = false;
  }
}

Finally, we just re-write the file with our updated text, and tell Unity to import it:

File.WriteAllLines(filePath, output);
AssetDatabase.ImportAsset(libraryPath);

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:

public static void SetOverrideSprites(SpriteLibraryAsset library, TextureAtlasInfo sourceInfo, TextureAtlasInfo targetInfo)
{
  string libraryPath = AssetDatabase.GetAssetPath(library);
  string filePath = Path.Combine(Application.dataPath, libraryPath).Replace("Assets/Assets", "Assets");
  // Read the file in.
  string[] lines = File.ReadAllLines(filePath);
  string spritePattern = @"(.*m_Sprite: {fileID: )(.*)(, guid: )(.*)(, type: 3}.*)";
  string overridePattern = @"(.*m_SpriteOverride: )({.*})(.*)";
  List<string> output = new List<string>();
  bool isLookingForOverride = false;
  SpriteInfo sourceSprite = null;
  // Read through them, replacing the sprite overide lines with their respective values from the new sprite target.
  for (int i = 0; i < lines.Length; i++)
  {
    string line = lines[i];
    if (isLookingForOverride)
    {
      // If we are looking for the override line.
      Match match = Regex.Match(line, overridePattern);
      if (match.Success)
      {
        if (sourceSprite != null)
        {
          if (targetInfo.nameLookup.ContainsKey(sourceSprite.name)) // Try and find the sprite name in the other atlas
          {
            SpriteInfo info = targetInfo.nameLookup[sourceSprite.name];
            string replacementString = $"$1/\{\{/fileID: {info.fileID}, guid: {targetInfo.guid}, type: 3/\}\}/ $3";
            string replaced = Regex.Replace(line, overridePattern, replacementString);
            line = replaced;
          }
        }
        else
        {
          Logger.LogError(1, $"Found an override match, but we don't have a corresponding SpriteInfo, something went wrong here.");
        }
        isLookingForOverride = false;
      }
    }
    else
    {
      // Otherwise look for a normal sprite defintion   match.
      Match match = Regex.Match(line, spritePattern);
      if (match.Success)
      {
        string textureGuid = match.Groups[4].ToString();
        string fileIdString = match.Groups[2].ToString();
        long fileId = System.Convert.ToInt64(fileIdString);
        if (sourceInfo.guid == textureGuid) // If the source guid is our source atlas.
        {
          if (sourceInfo.fileIdLookup.ContainsKey(fileId)) // Check if we found the sprite.
          {
            sourceSprite = sourceInfo.fileIdLookup[fileId];
            isLookingForOverride = true;
          }
          else
          {
            Logger.LogError(0, $"Atlas guid matched, but we couldn't find the sprite with id: {fileId}");
          }
        }
        else
        {
          Logger.LogError(0, $"Texture guid {textureGuid} did not match, skipping this one.");
        }
      }
    }
    output.Add(line);
  }
  File.WriteAllLines(filePath, output);
  AssetDatabase.ImportAsset(libraryPath);
}

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:

public void OnGUI()
{
  // Copying to a specifc file.
  GUILayout.BeginHorizontal();
  GUILayout.Label("Target Sprite Library:");
  _library = (SpriteLibraryAsset)EditorGUILayout.ObjectField(_library, typeof(SpriteLibraryAsset), false);
  GUILayout.EndHorizontal();
  GUILayout.BeginHorizontal();
  GUILayout.Label("Target Sprite Atlas:");
  _targetAtlas = (Texture2D)EditorGUILayout.ObjectField(_targetAtlas, typeof(Texture2D), false);
  GUILayout.EndHorizontal();
  if (GUILayout.Button("Swap Source Atlas"))
  {
    // Find the the first texture the existing library points to.
    string filePath = Path.Combine(Application.dataPath, AssetDatabase.GetAssetPath(_library)).Replace("Assets/Assets", "Assets");
    Texture2D spriteTexture = GetLibraryMasterSourceTexture(_library);
    TextureAtlasInfo sourceInfo = LoadAtlasInfo(spriteTexture);
    if (spriteTexture != null)
    {
      TextureAtlasInfo targetInfo = LoadAtlasInfo(_targetAtlas);
      SetOverrideSprites(_library, sourceInfo, targetInfo);
    }
    else
    {
      Logger.LogError(1, $"null texture after load.");
    }
  }
}

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:

// Use a source library and target a folder of 
GUILayout.BeginHorizontal();
GUILayout.Label("Source Sprite Library:");
_sourceLibrary = (SpriteLibraryAsset)EditorGUILayout.ObjectField(_sourceLibrary, typeof(SpriteLibraryAsset), false);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("Textures Directory: " + _texturesDirectoryPath);
if (GUILayout.Button("Choose"))
{
  _texturesDirectoryPath = EditorUtility.OpenFolderPanel("Textures Directory", _texturesDirectoryPath, "");
}
GUILayout.EndHorizontal();
GUIStyle textWrap = EditorStyles.label;
textWrap.wordWrap = true;
GUILayout.Label(
  @"This will go through the target directory, and for each texture create a sprite libary. Then it will modify those sprite library files to swap or add a sprite override for every sprite that exist in the target texture that also exists in the source texture of the library.", textWrap);
if (GUILayout.Button("Generate SpriteLibrary Files"))
{
  // Find the source library textre.
  Texture2D sourceTexture = GetLibraryMasterSourceTexture(_sourceLibrary);
  if (sourceTexture != null)
  {
    // Go through the directory
    List<string> filesInDir = new List<string>(Directory.EnumerateFiles(_texturesDirectoryPath, "*.png"));
    foreach(string file in filesInDir)
    {
      string relativePath = "Assets" + file.Replace(Application.dataPath, "");
      Texture2D targetTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(relativePath);
      if (targetTexture != null)
      {
        // Make a new copy of the Sprite Library
        string libraryPath = AssetDatabase.GetAssetPath(_sourceLibrary);
        string newLibraryName = targetTexture.name + "_library" + Path.GetExtension(libraryPath);
        string dirPath = Path.GetDirectoryName(relativePath);
        string newLibraryPath = Path.Combine(dirPath, newLibraryName);
        if (!File.Exists(newLibraryPath))
        {
          AssetDatabase.CopyAsset(libraryPath, newLibraryPath);
          AssetDatabase.SaveAssets();
          AssetDatabase.ImportAsset(newLibraryPath);
        }
        else
        {
          Logger.LogWarning(0, $"File already exists at {newLibraryPath}, will just be editing the existing one.");
        }
        // Load the Atlas Info from the textures.
        TextureAtlasInfo sourceInfo = LoadAtlasInfo(sourceTexture);
        TextureAtlasInfo targetInfo = LoadAtlasInfo(targetTexture);
        SpriteLibraryAsset newLibrary = AssetDatabase.LoadAssetAtPath<SpriteLibraryAsset>(newLibraryPath);
        if (newLibrary != null)
        {
          SetOverrideSprites(newLibrary, sourceInfo, targetInfo);
        }
      }
    }
  }
}

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:

public void OnGUI()
{
  GUILayout.BeginHorizontal();
  if (CommonEditor.ColoredButton("File: ", _targetingDirectory ? Color.grey : Color.blue))
  {
    _targetingDirectory = false;
  }
  if (CommonEditor.ColoredButton("Directory: ", _targetingDirectory ? Color.blue : Color.grey))
  {
    _targetingDirectory = true;
  }
  GUILayout.EndHorizontal();
        
  if (_targetingDirectory)
  {
    // Use a source library and target a folder of textures.
    // ...
  }
  else
  {
    // Changing a specifc file.
    // ...
  }
}

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:

public static bool ColoredButton(GUIContent content, Color color, params GUILayoutOption[] options)
{
  bool result = false;
  Color original = GUI.backgroundColor;
  GUI.backgroundColor = color;
  if (GUILayout.Button(content, options))
  {
    result = true;
  }
  GUI.backgroundColor = original;
  return result;
}

Full Source

The full source code for this file is available as a snippet on gitlab here.

In the future…

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.

© 2023 Michael Christensen-Calvin   •  Theme  Moonwalk