Starling Framework's spritesheets are described by the TextureAtlas XML files.
Recently I have donated to the KenneyLand crowdfund and Kenney (www.kenney.nl) has sent me an asset pack containing a lot of spritesheets and other great assets. The spritesheets also came with XML files next to them. I currently use Unity3D as my main development tool and wanted to use these spritesheets for some of my game ideas.
I will now explain how I wrote a script to let me easily slice 2D sprites.
We will use the UI pack: Space extension by Kenney as an example. The spritesheet looks like this:
As you can see, it is not evenly divided, so the grid slicer will not work. Also, the pixels are mostly touching, so the automatic slicing will not work either. To our luck, it comes with an XML file:
<TextureAtlas imagePath="sheet.png">
<SubTexture name="barHorizontal_blue_left.png" x="400" y="78" width="6" height="26"/>
<SubTexture name="barHorizontal_blue_mid.png" x="388" y="420" width="16" height="26"/>
<SubTexture name="barHorizontal_blue_right.png" x="400" y="0" width="6" height="26"/>
<SubTexture name="barHorizontal_green_left.png" x="400" y="104" width="6" height="26"/>
<SubTexture name="barHorizontal_green_mid.png" x="386" y="366" width="16" height="26"/>
<SubTexture name="barHorizontal_green_right.png" x="400" y="26" width="6" height="26"/>
<!-- ... -->
<SubTexture name="squareWhite.png" x="384" y="476" width="19" height="26"/>
<SubTexture name="squareYellow.png" x="380" y="236" width="19" height="26"/>
<SubTexture name="square_shadow.png" x="386" y="288" width="19" height="26"/>
</TextureAtlas>
We could use this XML file to manually create the rectangles... just kidding.Implementation
Here is what the script will do:- Add a context menu item to TextureImporter inspector
- This item will open a window to configure slicing
- Then the window will close itself after slicing is completed.
If you do not care about the implementation details, you can skip to the script.
New Script
In your project explorer, create an 'Editor' folder for the script, if it does not exist already. This folder needs to be named 'Editor' because that's how Unity3D knows that these scripts will be used only for editor-specific functions. More information can be found here.In there, create a C# script. I named mine 'TextureAtlasSlicer'. Here is what it looks like now:
using UnityEditor; | |
public class TextureAtlasSlicer : EditorWindow { | |
// TODO | |
} |
Adding a custom context menu item
We need to use the MenuItem attribute. This menuitem's name should start with"CONTEXT/TextureImporter/"
, because that is how Unity3D understands that it is a context menu, and it is for the TextureImporter inspector.using UnityEditor; | |
using UnityEngine; | |
public class TextureAtlasSlicer : EditorWindow { | |
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML")] | |
public static void SliceUsingXML(MenuCommand command) | |
{ | |
TextureImporter textureImporter = command.context as TextureImporter; | |
TextureAtlasSlicer window = ScriptableObject.CreateInstance<TextureAtlasSlicer>(); | |
window.importer = textureImporter; | |
window.ShowUtility(); | |
} | |
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML", true)] | |
public static bool ValidateSliceUsingXML(MenuCommand command) | |
{ | |
TextureImporter textureImporter = command.context as TextureImporter; | |
//valid only if the texture type is 'sprite' or 'advanced'. | |
return textureImporter && textureImporter.textureType == TextureImporterType.Sprite || | |
textureImporter.textureType == TextureImporterType.Advanced; | |
} | |
public TextureImporter importer; | |
public TextureAtlasSlicer() | |
{ | |
title = "Texture Atlas Slicer"; | |
} | |
} |
The
ValidateSliceUsingXML
function tells Unity3D to enable the menu item only for 'sprite' or 'advanced' texture types.The actual function,
SliceUsingXML
is used to open a window and save the reference to the TextureImporter
, as it will be used by the window to do the slicing. The TextureImporter is extracted out of the MenuCommand.Here is what it looks like:
Configuring the slicing
[SerializeField] private TextAsset xmlAsset; | |
public SpriteAlignment spriteAlignment = SpriteAlignment.Center; | |
public Vector2 customOffset = new Vector2(0.5f, 0.5f); | |
public void OnGUI() { | |
xmlAsset = EditorGUILayout.ObjectField("XML Source", xmlAsset, typeof (TextAsset), false) as TextAsset; | |
spriteAlignment = (SpriteAlignment) EditorGUILayout.EnumPopup("Pivot", spriteAlignment); | |
bool enabled = GUI.enabled; | |
if (spriteAlignment != SpriteAlignment.Custom) { | |
GUI.enabled = false; | |
} | |
EditorGUILayout.Vector2Field("Custom Offset", customOffset); | |
GUI.enabled = enabled; | |
if (xmlAsset == null) { | |
GUI.enabled = false; | |
} | |
if (GUILayout.Button("Slice")) { | |
//PerformSlice(); | |
} | |
GUI.enabled = enabled; | |
} |
The
xmlAsset
field is used to choose which file will be used for slicing.We can also customize the
spriteAlignment
and customOffset
, which will set the default pivot for the slices.And finally, the
Slice
button, that's why we're here!Slicing
private void PerformSlice() | |
{ | |
XmlDocument document = new XmlDocument(); | |
document.LoadXml(xmlAsset.text); | |
XmlElement root = document.DocumentElement; | |
if (root.Name == "TextureAtlas") { | |
bool failed = false; | |
Texture2D texture = AssetDatabase.LoadMainAssetAtPath(importer.assetPath) as Texture2D; | |
int textureHeight = texture.height; | |
List<SpriteMetaData> metaDataList = new List<SpriteMetaData>(); | |
foreach (XmlNode childNode in root.ChildNodes) | |
{ | |
if (childNode.Name == "SubTexture") { | |
try { | |
int width = Convert.ToInt32(childNode.Attributes["width"].Value); | |
int height = Convert.ToInt32(childNode.Attributes["height"].Value); | |
int x = Convert.ToInt32(childNode.Attributes["x"].Value); | |
int y = textureHeight - (height + Convert.ToInt32(childNode.Attributes["y"].Value)); | |
SpriteMetaData spriteMetaData = new SpriteMetaData | |
{ | |
alignment = (int)spriteAlignment, | |
border = new Vector4(), | |
name = childNode.Attributes["name"].Value, | |
pivot = GetPivotValue(spriteAlignment, customOffset), | |
rect = new Rect(x, y, width, height) | |
}; | |
metaDataList.Add(spriteMetaData); | |
} | |
catch (Exception exception) { | |
failed = true; | |
Debug.LogException(exception); | |
} | |
} | |
else | |
{ | |
Debug.LogError("Child nodes should be named 'SubTexture' !"); | |
failed = true; | |
} | |
} | |
if (!failed) { | |
importer.spriteImportMode = SpriteImportMode.Multiple; | |
importer.spritesheet = metaDataList.ToArray(); | |
EditorUtility.SetDirty(importer); | |
try | |
{ | |
AssetDatabase.StartAssetEditing(); | |
AssetDatabase.ImportAsset(importer.assetPath); | |
} | |
finally | |
{ | |
AssetDatabase.StopAssetEditing(); | |
Close(); | |
} | |
} | |
} | |
else | |
{ | |
Debug.LogError("XML needs to have a 'TextureAtlas' root node!"); | |
} | |
} |
We use the XmlDocument#LoadXml method in order to create an XmlDocument object in c#.
As specified above, the XML document has a
TextureAtlas
node as root, and SubTexture
child nodes. Each SubTexture has five attributes: the name and the rectangle definitions (x, y, width, height).In Unity3D, for textures, the y coordinate is 0 at the bottom. This is not the case for
SubTexture
rectangles, so we need to fix that. In order to get the texture height, we need to load the image file as a texture (line 10 in gist). Then, the y value for the rectangle is (texture height - (SubTexture.height + SubTexture.y))
(line 22 in gist).In order to get the pivot, we can use the GetPivotValue function (borrowed from the Unity3D Editor's internal
SpriteEditorUtility
class):public static Vector2 GetPivotValue(SpriteAlignment alignment, Vector2 customOffset) | |
{ | |
switch (alignment) | |
{ | |
case SpriteAlignment.Center: | |
return new Vector2(0.5f, 0.5f); | |
case SpriteAlignment.TopLeft: | |
return new Vector2(0.0f, 1f); | |
case SpriteAlignment.TopCenter: | |
return new Vector2(0.5f, 1f); | |
case SpriteAlignment.TopRight: | |
return new Vector2(1f, 1f); | |
case SpriteAlignment.LeftCenter: | |
return new Vector2(0.0f, 0.5f); | |
case SpriteAlignment.RightCenter: | |
return new Vector2(1f, 0.5f); | |
case SpriteAlignment.BottomLeft: | |
return new Vector2(0.0f, 0.0f); | |
case SpriteAlignment.BottomCenter: | |
return new Vector2(0.5f, 0.0f); | |
case SpriteAlignment.BottomRight: | |
return new Vector2(1f, 0.0f); | |
case SpriteAlignment.Custom: | |
return customOffset; | |
default: | |
return Vector2.zero; | |
} | |
} |
We can use this information to create a
SpriteMetaData
, which basically describe the slices for a sprite (line 24).If there were no errors, we can slice the sprite: set the
spriteImportMode
to Multiple
, and then assign the spritesheet
array. Afterwards, we just need to tell the AssetDatabase
to reimport the texture, and we're done!Done!
Here's the whole code for the script:using System; | |
using System.Collections.Generic; | |
using System.Xml; | |
using UnityEditor; | |
using UnityEngine; | |
public class TextureAtlasSlicer : EditorWindow { | |
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML")] | |
public static void SliceUsingXML(MenuCommand command) | |
{ | |
TextureImporter textureImporter = command.context as TextureImporter; | |
TextureAtlasSlicer window = ScriptableObject.CreateInstance<TextureAtlasSlicer>(); | |
window.importer = textureImporter; | |
window.ShowUtility(); | |
} | |
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML", true)] | |
public static bool ValidateSliceUsingXML(MenuCommand command) | |
{ | |
TextureImporter textureImporter = command.context as TextureImporter; | |
//valid only if the texture type is 'sprite' or 'advanced'. | |
return textureImporter && textureImporter.textureType == TextureImporterType.Sprite || | |
textureImporter.textureType == TextureImporterType.Advanced; | |
} | |
public TextureImporter importer; | |
public TextureAtlasSlicer() | |
{ | |
title = "Texture Atlas Slicer"; | |
} | |
[SerializeField] private TextAsset xmlAsset; | |
public SpriteAlignment spriteAlignment = SpriteAlignment.Center; | |
public Vector2 customOffset = new Vector2(0.5f, 0.5f); | |
public void OnGUI() { | |
xmlAsset = EditorGUILayout.ObjectField("XML Source", xmlAsset, typeof (TextAsset), false) as TextAsset; | |
spriteAlignment = (SpriteAlignment) EditorGUILayout.EnumPopup("Pivot", spriteAlignment); | |
bool enabled = GUI.enabled; | |
if (spriteAlignment != SpriteAlignment.Custom) { | |
GUI.enabled = false; | |
} | |
EditorGUILayout.Vector2Field("Custom Offset", customOffset); | |
GUI.enabled = enabled; | |
if (xmlAsset == null) { | |
GUI.enabled = false; | |
} | |
if (GUILayout.Button("Slice")) { | |
PerformSlice(); | |
} | |
GUI.enabled = enabled; | |
} | |
private void PerformSlice() | |
{ | |
XmlDocument document = new XmlDocument(); | |
document.LoadXml(xmlAsset.text); | |
XmlElement root = document.DocumentElement; | |
if (root.Name == "TextureAtlas") { | |
bool failed = false; | |
Texture2D texture = AssetDatabase.LoadMainAssetAtPath(importer.assetPath) as Texture2D; | |
int textureHeight = texture.height; | |
List<SpriteMetaData> metaDataList = new List<SpriteMetaData>(); | |
foreach (XmlNode childNode in root.ChildNodes) | |
{ | |
if (childNode.Name == "SubTexture") { | |
try { | |
int width = Convert.ToInt32(childNode.Attributes["width"].Value); | |
int height = Convert.ToInt32(childNode.Attributes["height"].Value); | |
int x = Convert.ToInt32(childNode.Attributes["x"].Value); | |
int y = textureHeight - (height + Convert.ToInt32(childNode.Attributes["y"].Value)); | |
SpriteMetaData spriteMetaData = new SpriteMetaData | |
{ | |
alignment = (int)spriteAlignment, | |
border = new Vector4(), | |
name = childNode.Attributes["name"].Value, | |
pivot = GetPivotValue(spriteAlignment, customOffset), | |
rect = new Rect(x, y, width, height) | |
}; | |
metaDataList.Add(spriteMetaData); | |
} | |
catch (Exception exception) { | |
failed = true; | |
Debug.LogException(exception); | |
} | |
} | |
else | |
{ | |
Debug.LogError("Child nodes should be named 'SubTexture' !"); | |
failed = true; | |
} | |
} | |
if (!failed) { | |
importer.spriteImportMode = SpriteImportMode.Multiple; | |
importer.spritesheet = metaDataList.ToArray(); | |
EditorUtility.SetDirty(importer); | |
try | |
{ | |
AssetDatabase.StartAssetEditing(); | |
AssetDatabase.ImportAsset(importer.assetPath); | |
} | |
finally | |
{ | |
AssetDatabase.StopAssetEditing(); | |
Close(); | |
} | |
} | |
} | |
else | |
{ | |
Debug.LogError("XML needs to have a 'TextureAtlas' root node!"); | |
} | |
} | |
//SpriteEditorUtility | |
public static Vector2 GetPivotValue(SpriteAlignment alignment, Vector2 customOffset) | |
{ | |
switch (alignment) | |
{ | |
case SpriteAlignment.Center: | |
return new Vector2(0.5f, 0.5f); | |
case SpriteAlignment.TopLeft: | |
return new Vector2(0.0f, 1f); | |
case SpriteAlignment.TopCenter: | |
return new Vector2(0.5f, 1f); | |
case SpriteAlignment.TopRight: | |
return new Vector2(1f, 1f); | |
case SpriteAlignment.LeftCenter: | |
return new Vector2(0.0f, 0.5f); | |
case SpriteAlignment.RightCenter: | |
return new Vector2(1f, 0.5f); | |
case SpriteAlignment.BottomLeft: | |
return new Vector2(0.0f, 0.0f); | |
case SpriteAlignment.BottomCenter: | |
return new Vector2(0.5f, 0.0f); | |
case SpriteAlignment.BottomRight: | |
return new Vector2(1f, 0.0f); | |
case SpriteAlignment.Custom: | |
return customOffset; | |
default: | |
return Vector2.zero; | |
} | |
} | |
} |
Thank you. Works on Unity 2019.4 LTS
Works like charm on Unity 5.6!.. Coincidentally I am using Kenney's asset too and got curious about those xmls... a quick google brought me to this page..
The script continues to work, even on Unity 5.1.0f3 Personal. There was only one warning and I changed the line 34 to: titleContent.text = "Texture Atlas Slicer"; Thank you.
You scsript just works as promised! Thanks so much!
Just found Kenny's art, poked around the game icons pack and found some XML. Googled how I would use this with unity, and this pops up. Thank you.
Thanks a lot
Great job bro! I'm using some Kenney asset for Unity. With this script, i can do Spritesheet slicing faster and efficient!
Hey,
I also made one for handling the two types of xml formats kenney exports to automatically. Here's the gist:
https://gist.github.com/anonymous/655731c176d3031689a5
Not saying it's better or worse, but it's just another reference for folks!
Cool, will check it out :)
Hey man!
Thanks for this script. I was having the EXACT same problem :D I have the sprites from Kenney and I was really looking on how to import them considering that as you said... the sprites are touching and the auto slicer was not working. I also notice the XML... so I did a google search for "Import Unity sprites form XML" or something like that and BAM... I got here
It's funny that you use the exact same sprites and you ran into the exact same problem.
Thanks for the script!
Thank you for this!
Thanks this is exactly what I needed.
this was exactly what i was looking for, appreciate you sharing your efforts m8
It's awesome, THANK YOU!
This script has a little shortcoming but it's nothing major. I tried importing tileset of width 2048px but Unity's default max width is 1024 which caused the script to read wrong values. Then I changed this values and script worked like charm. I know this is not directly connected to script but someone might stumble upon same problem and they can easily fix it if they read this comment. Thank you for this nice script.
Interesting, could you please link to the tileset or a similar sized one? I can try to alter the script :)
I have just fixed this issue in the update (check top of page ) :D