We've looked previously at adding our own tools to Unity's editor; now, in this short tutorial, I'll introduce you to handling the assets by script in Unity. We'll manage paths, create prefab files, generate a texture and save it to an image. Finally we'll also create a material file that uses the generated image, and all this will be done by code.
Final Result Preview
Let's take a look at the final result we will be working towards:
Step 1: Set Up the Project
Create an empty project; we won't be using anything fancy here so we shouldn't bother to import anything at all. Once that's done, create an editor script. Unity will let us use its editor classes only if we place our script in a folder named Editor. Since that doesn't exist in our project yet, we need to create it.
Now let's create a script inside it.
Step 2: Add a MenuItem
Let's clean up our script. Aside from the basic functionality, we also want to be able to use the editor classes. We need to be using UnityEditor
and our script's class should extend the Editor
class instead of MonoBehaviour
like normal game objects do.
using UnityEngine; using System.Collections; using UnityEditor; public class Examples : Editor { }
In our first function we'll be working with prefabs, let's call it a PrefabRoutine
.
public class Examples : Editor { void PrefabRoutine() { } }
To easly execute this function from the editor, let's add it as a MenuItem
.
public class Examples : Editor { [MenuItem ("Examples/Prefab Routine")] void PrefabRoutine() { } }
Aside from letting the unity know that we want this function to be executable from the Examples->Prefab Routine", we also need to make this function static.
public class Examples : Editor { [MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { } }
If you go back to the editor now (and refresh the menu), you'll notice that there's a new menu named Examples there.
If you select the Prefab Routine nothing will happen since our function is empty.
Step 3: Create a Folder
To shape our project the way we want we need to know how to create folders so we can move stuff around. Creating a folder from the script is very straightforward, all we need to do is to let unity know where the folder should be placed. To create a folder we need to use AssetDatabase
class.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { AssetDatabase.CreateFolder("Assets", "Prefab Folder"); }
"Assets" is the name of the parent folder of the directory we want to create. In our case it's the main project folder where all our assets are imported/created.
Note that you can also use the .NET Directory
class. This will also let you delete, move or access the directories' files. To use this class you need to be using System.IO
.
Each time you select the Prefab Routine from the editor, a new folder should be created and be visible in the project view.
Step 4: Create a Prefab
To create a prefab we need to call EditorUtility.CreateEmptyPrefab()
. The function takes the prefab's path as an argument.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { AssetDatabase.CreateFolder("Assets", "Prefab Folder"); Object prefab = EditorUtility.CreateEmptyPrefab("Assets/Prefab Folder/obj.prefab"); }
Don't forget about the extension! Also, after we create the file we need to call AssetDatabase.Refresh()
, so the unity is able to see it.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { AssetDatabase.CreateFolder("Assets", "Prefab Folder"); Object prefab = EditorUtility.CreateEmptyPrefab("Assets/Prefab Folder/obj.prefab"); AssetDatabase.Refresh(); }
If we leave a constant path as an argument, each time we select our routine a new empty prefab will replace the old one. Let's assign each prefab to separate folder to counter that. To do this we need to save the most recently created folder to a string so we can use it as a path argument later. The CreateFolder
function returns a GUID
, which basically is the file's (or directory's) ID. There's a function that retrieves the path if we submit this ID. It's called GUIDToAssetPath
; let's use it to get our folder's path.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { string path = AssetDatabase.GUIDToAssetPath(AssetDatabase.CreateFolder("Assets", "Prefab Folder")); Object prefab = EditorUtility.CreateEmptyPrefab("Assets/Prefab Folder/obj.prefab"); AssetDatabase.Refresh(); }
Now let's use the path
to direct the prefabs we are going to create to the most recently created folder.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { string path = AssetDatabase.GUIDToAssetPath(AssetDatabase.CreateFolder("Assets", "Prefab Folder")); Object prefab = EditorUtility.CreateEmptyPrefab(path + "/obj.prefab"); AssetDatabase.Refresh(); }
You can test whether the created empty prefabs are packed in folders now.
Step 5: Set the Prefab
If you create a prefab then you probably don't want to leave it empty because in that case it's pretty much useless. Let's set our prefab if there's any game object selected while our routine is executing. We'll the prefab to the selected object. To get the currently selected object we can use the Selection
class which has a reference to it. To set the prefab we need to call ReplacePrefab()
.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { string path = AssetDatabase.GUIDToAssetPath(AssetDatabase.CreateFolder("Assets", "Prefab Folder")); Object prefab = EditorUtility.CreateEmptyPrefab(path + "/obj.prefab"); AssetDatabase.Refresh(); if (Selection.activeObject) EditorUtility.ReplacePrefab(Selection.activeGameObject, prefab); }
If you run the routine with any game object selected now then you'll notice that the created prefab is automatically set.
That's it, we have created a custom routine for prefab creation, it's not very useful but you should be able to know how to do that now if there will be a need for such a thing in your project.
At the end I also want to mention that AssetDatabase
also lets you move assets around, move them to trash or delete them by calling AssetDatabase.MoveAsset(), AssetDatabase.MoveAssetToTrash()
and AssetDatabase.DeleteAsset()
respectively. The rest of the functionality can be found at the AssetDatabase
script reference page.
Step 6: Add Another Menu Item
Let's go to another example, this time we'll create a texture and a material programmatically. Let's call this menu item Material Routine.
[MenuItem ("Examples/Prefab Routine")] static void PrefabRoutine() { string path = AssetDatabase.GUIDToAssetPath(AssetDatabase.CreateFolder("Assets", "Prefab Folder")); Object prefab = EditorUtility.CreateEmptyPrefab(path + "/obj.prefab"); AssetDatabase.Refresh(); if (Selection.activeObject) EditorUtility.ReplacePrefab(Selection.activeGameObject, prefab); } [MenuItem ("Examples/Material Routine")] static void MaterialRoutine() { }
Now we have two items to choose from in the Examples menu.
Step 7: Create a Texture
Let's create a Texture2D
and set its size to (256, 256)
for this example.
[MenuItem ("Examples/Material Routine")] static void MaterialRoutine() { Texture2D tex = new Texture2D(256, 256); }
Now we shouldn't let all those pixels go to waste, so let's set the texture's pixels according to some kind of thought-up formula. For that we'll need two for
loops to go through every pixel. To set the each pixel's color we need to call SetPixel()
which takes the position of the pixel on a texture and its color as the arguments.
[MenuItem ("Examples/Material Routine")] static void MaterialRoutine() { Texture2D tex = new Texture2D(256, 256); for (int y = 0; y < 256; ++y) { for (int x = 0; x < 256; ++x) tex.SetPixel(x, y, new Color()); } }
To assign the color we'll use the Mathf.Sin()
function. The Color
class can be initialized with three floats, corresponding to the red, green and blue color components, respectively. The max allowed value is 1
and min is 0
, so the Sin()
function suits our needs perfectly.
for (int y = 0; y < 256; ++y) { for (int x = 0; x < 256; ++x) tex.SetPixel(Mathf.Sin(x*y), Mathf.Sin(x*y), Mathf.Sin(x*y))); }
It doesn't matter what we submit to the Sin()
function, but to get something more interesting we should give a value that changes for each pixel.
Step 8: Create an Image
Now let's create an image from the texture we just created. Since we'll be writing to a file in binary mode, we need to be using System.IO
, so let's add it to the top of our script.
using UnityEngine; using System.Collections; using UnityEditor; using System.IO; public class Examples : Editor
To save our texture as a PNG image we first need to call EncodeToPNG()
which will return an array of bytes that the PNG file consists of.
for (int y = 0; y < 256; ++y) { for (int x = 0; x < 256; ++x) tex.SetPixel(x, y, new Color(Mathf.Sin(x*y), Mathf.Sin(x*y), Mathf.Sin(x*y))); } byte[] pngData = tex.EncodeToPNG();
Now that we've got our pngData
we can write it to a file and create a PNG image in this way.
byte[] pngData = tex.EncodeToPNG(); if(pngData != null) File.WriteAllBytes("Assets/texture.png", pngData);
Since we create the file at a constant path, each time we'll run MaterialRoutine()
, the texture will get overwritten.
And since we've got our image, we don't need the generated texture anymore as it won't be referencing an image anyway. Let's destroy it.
byte[] pngData = tex.EncodeToPNG(); if(pngData != null) File.WriteAllBytes("Assets/texture.png", pngData); DestroyImmediate(tex);
Also, we need to let Unity update the project view and file references; to do that we need to call AssetDatabase.Refresh()
.
byte[] pngData = tex.EncodeToPNG(); if(pngData != null) File.WriteAllBytes("Assets/texture.png", pngData); DestroyImmediate(tex); AssetDatabase.Refresh();
Let's test whether the texture gets created when we execute our routine.
Step 9: Create a Material
We've got an image and now we can create a material that uses it as a texture. Let's create a new Material
.
AssetDatabase.Refresh(); new Material(Shader.Find("Diffuse"));
The created material will use a Diffuse shader. To save this material to the file, we can call AssetDatabase.CreateAsset()
. This function takes an asset as the first argument, and the path as the second one.
AssetDatabase.Refresh(); AssetDatabase.CreateAsset(new Material(Shader.Find("Diffuse")), "Assets/New Material.mat");
If you run our routine now, you'll see that the material is created.
As you can see everything is correct, its name is New Material and it uses Diffuse shader, but there's no texture assigned to it.
Step 10: Assign the Texture
First we need to get a reference to the material we just created. We can get that by calling AssetDatabase.LoadAssetAtPath()
which loads the asset and returns its reference.
AssetDatabase.CreateAsset(new Material(Shader.Find("Diffuse")), "Assets/New Material.mat"); Material material = (Material) (AssetDatabase.LoadAssetAtPath("Assets/New Material.mat",typeof(Material)));
Now let's assign our generated texture as the main texture of the material. We can get the texture reference of the generated texture in the same way we got the material reference.
Material material = (Material) (AssetDatabase.LoadAssetAtPath("Assets/New Material.mat",typeof(Material))); material.mainTexture = (Texture2D) (AssetDatabase.LoadAssetAtPath("Assets/texture.png", typeof(Texture2D)));
To see the results, run the Material Routine.
As you can see, the material has the texture assigned now.
Conclusion
That's the end of the introduction to manage your assets using scripts. If you want to expand your knowladge on the topic you can visit the Unity Editor Classes reference page, particularly the AssetDatabase script reference is worth looking into. If you need to work at a low level, you should also read the docs on System.IO to get more information on its classes and how you can use them. Thanks for your time!
Comments