Статьи

Процедурное управление активами в Unity

Ранее мы рассматривали добавление наших собственных инструментов в редактор Unity ; Теперь, в этом коротком уроке, я познакомлю вас с обработкой ресурсов скриптом в Unity. Мы будем управлять путями, создавать готовые файлы, генерировать текстуру и сохранять ее в изображение. Наконец, мы также создадим файл материала, который использует сгенерированное изображение, и все это будет сделано с помощью кода.


Давайте посмотрим на конечный результат, к которому мы будем стремиться:

Материал с назначенной текстурой.

Создать пустой проект; мы не будем использовать здесь что-то необычное, поэтому нам не нужно ничего импортировать. Как только это будет сделано, создайте скрипт редактора. Unity позволит нам использовать его классы редактора, только если мы поместим наш скрипт в папку с именем Editor . Так как этого еще нет в нашем проекте, нам нужно его создать.

Папка редактора, созданная в виде проекта.

Теперь давайте создадим скрипт внутри него.

Редактор скрипта в нашей папке.

Давайте очистим наш скрипт. Помимо основной функциональности, мы также хотим иметь возможность использовать классы редактора. Нам нужно using UnityEditor а класс нашего скрипта должен расширять класс Editor а не MonoBehaviour как это MonoBehaviour обычные игровые объекты.

1
2
3
4
5
6
7
using UnityEngine;
using System.Collections;
using UnityEditor;
 
public class Examples : Editor
{
}

В нашей первой функции мы будем работать с префабами, давайте назовем это PrefabRoutine .

1
2
3
4
5
6
public class Examples : Editor
{
    void PrefabRoutine()
    {
    }
}

Чтобы легко выполнить эту функцию из редактора, давайте добавим ее как MenuItem .

1
2
3
4
5
6
7
public class Examples : Editor
{
    [MenuItem («Examples/Prefab Routine»)]
    void PrefabRoutine()
    {
    }
}

Помимо того, что нужно сообщить единице, что мы хотим, чтобы эта функция была выполнимой из Примера-> Предварительная сборка « , нам также нужно сделать эту функцию статической.

1
2
3
4
5
6
7
public class Examples : Editor
{
    [MenuItem («Examples/Prefab Routine»)]
    static void PrefabRoutine()
    {
    }
}

Если вы сейчас вернетесь в редактор (и обновите меню), вы заметите, что там есть новое меню под названием Примеры .

Пункт меню добавлен в редактор единства.

Если вы выберете Prefab Routine, ничего не произойдет, так как наша функция пуста.


Чтобы сформировать наш проект так, как мы хотим, нам нужно знать, как создавать папки, чтобы мы могли что-то перемещать. Создать папку из скрипта очень просто, все, что нам нужно сделать, это сообщить Unity, где должна быть расположена папка. Для создания папки нам нужно использовать класс AssetDatabase .

1
2
3
4
5
[MenuItem («Examples/Prefab Routine»)]
static void PrefabRoutine()
{
    AssetDatabase.CreateFolder(«Assets», «Prefab Folder»);
}

«Активы» — это имя родительской папки каталога, который мы хотим создать. В нашем случае это основная папка проекта, в которую импортируются / создаются все наши активы.

Обратите внимание, что вы также можете использовать класс .NET Directory . Это также позволит вам удалять, перемещать или получать доступ к файлам каталогов. Чтобы использовать этот класс, вы должны using System.IO .

Каждый раз, когда вы выбираете Prefab Routine из редактора, новая папка должна создаваться и быть видимой в представлении проекта.

Папки, созданные в нашем скрипте.

Для создания префаба нам нужно вызвать EditorUtility.CreateEmptyPrefab() . Функция принимает путь префаба в качестве аргумента.

1
2
3
4
5
6
7
[MenuItem («Examples/Prefab Routine»)]
static void PrefabRoutine()
{
    AssetDatabase.CreateFolder(«Assets», «Prefab Folder»);
     
    Object prefab = EditorUtility.CreateEmptyPrefab(«Assets/Prefab Folder/obj.prefab»);
}

Не забывайте о расширении! Кроме того, после того, как мы создадим файл, нам нужно вызвать AssetDatabase.Refresh() , чтобы единство смогло его увидеть.

1
2
3
4
5
6
7
8
9
[MenuItem («Examples/Prefab Routine»)]
static void PrefabRoutine()
{
    AssetDatabase.CreateFolder(«Assets», «Prefab Folder»);
     
    Object prefab = EditorUtility.CreateEmptyPrefab(«Assets/Prefab Folder/obj.prefab»);
     
    AssetDatabase.Refresh();
}

Если мы оставляем постоянный путь в качестве аргумента, каждый раз, когда мы выбираем нашу подпрограмму, новый пустой префаб заменяет старый. Давайте назначим каждый сборный файл отдельной папке, чтобы противостоять этому. Для этого нам нужно сохранить последнюю созданную папку в строку, чтобы мы могли использовать ее в качестве аргумента пути позже. Функция CreateFolder возвращает GUID , который в основном является GUID файла (или каталога). Есть функция, которая извлекает путь, если мы отправляем этот идентификатор. Это называется GUIDToAssetPath ; давайте использовать его, чтобы получить путь к нашей папке.

1
2
3
4
5
6
7
8
9
[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();
}

Теперь давайте используем path чтобы направить префабы, которые мы собираемся создать, в самую последнюю созданную папку.

1
2
3
4
5
6
7
8
9
[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();
}

Вы можете проверить, упакованы ли созданные пустые префабы в папки сейчас.

Сборные в папках.

Если вы создаете сборный дом, то вы, вероятно, не захотите оставлять его пустым, потому что в этом случае он практически бесполезен. Давайте установим наш префаб, если во время выполнения нашей подпрограммы выбран какой-либо игровой объект. Мы будем готовить выбранный объект. Чтобы получить текущий выбранный объект, мы можем использовать класс Selection который имеет ссылку на него. Чтобы установить префаб, нам нужно вызвать ReplacePrefab() .

01
02
03
04
05
06
07
08
09
10
11
12
[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);
}

Если вы запустите процедуру с любым выбранным игровым объектом, то вы заметите, что созданный префаб автоматически устанавливается.

Prefab установить для выбранного объекта.

Вот и все, мы создали собственную подпрограмму для создания сборных, она не очень полезна, но вы должны быть в состоянии знать, как это сделать сейчас, если в вашем проекте возникнет необходимость в такой вещи.

В конце я также хочу упомянуть, что AssetDatabase также позволяет перемещать активы, перемещать их в корзину или удалять их, вызывая AssetDatabase.MoveAsset(), AssetDatabase.MoveAssetToTrash() и AssetDatabase.DeleteAsset() соответственно. Остальные функции можно найти на справочной странице сценария AssetDatabase .


Давайте перейдем к другому примеру, на этот раз мы создадим текстуру и материал программно. Давайте назовем этот пункт меню рутиной материала .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
[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()
{
}

Теперь у нас есть два пункта на выбор в меню « Примеры» .


Давайте создадим Texture2D и установим его размер (256, 256) для этого примера.

1
2
3
4
5
[MenuItem («Examples/Material Routine»)]
static void MaterialRoutine()
{
    Texture2D tex = new Texture2D(256, 256);
}

Теперь мы не должны допустить, чтобы все эти пиксели были потрачены впустую, поэтому давайте установим пиксели текстуры в соответствии с некой придуманной формулой. Для этого нам понадобится два цикла для прохождения каждого пикселя. Чтобы установить цвет каждого пикселя, нам нужно вызвать SetPixel() который принимает положение пикселя на текстуре и его цвет в качестве аргументов.

01
02
03
04
05
06
07
08
09
10
11
[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());
    }
}

Чтобы назначить цвет, мы будем использовать Mathf.Sin() . Класс Color может быть инициализирован тремя числами с плавающей запятой, соответствующими компонентам красного, зеленого и синего цветов соответственно. Максимально допустимое значение равно 1 а min равно 0 , поэтому функция Sin() идеально соответствует нашим потребностям.

1
2
3
4
5
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)));
}

Не имеет значения, что мы отправляем в функцию Sin() , но чтобы получить что-то более интересное, мы должны указать значение, которое меняется для каждого пикселя.


Теперь давайте создадим изображение из текстуры, которую мы только что создали. Поскольку мы будем записывать в файл в двоичном режиме, нам нужно using System.IO , поэтому давайте добавим его в начало нашего скрипта.

1
2
3
4
5
6
using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
 
public class Examples : Editor

Чтобы сохранить нашу текстуру как изображение PNG, нам сначала нужно вызвать EncodeToPNG() который вернет массив байтов, из которого состоит файл PNG .

1
2
3
4
5
6
7
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();

Теперь, когда у нас есть pngData мы можем записать его в файл и создать изображение PNG таким образом.

1
2
3
4
byte[] pngData = tex.EncodeToPNG();
 
if(pngData != null)
    File.WriteAllBytes(«Assets/texture.png», pngData);

Поскольку мы создаем файл с постоянным путем, каждый раз, когда мы запускаем MaterialRoutine() , текстура будет перезаписываться.

И так как у нас есть наше изображение, нам больше не нужна сгенерированная текстура, так как она все равно не будет ссылаться на изображение. Давайте уничтожим это.

1
2
3
4
5
6
byte[] pngData = tex.EncodeToPNG();
 
if(pngData != null)
    File.WriteAllBytes(«Assets/texture.png», pngData);
     
DestroyImmediate(tex);

Кроме того, нам нужно позволить Unity обновить вид проекта и ссылки на файлы; Для этого нам нужно вызвать AssetDatabase.Refresh() .

1
2
3
4
5
6
7
byte[] pngData = tex.EncodeToPNG();
 
if(pngData != null)
    File.WriteAllBytes(«Assets/texture.png», pngData);
     
DestroyImmediate(tex);
AssetDatabase.Refresh();

Давайте проверим, создается ли текстура при выполнении нашей процедуры.

Сгенерированная текстура.

У нас есть изображение, и теперь мы можем создать материал, который использует его в качестве текстуры. Давайте создадим new Material .

1
2
3
AssetDatabase.Refresh();
 
new Material(Shader.Find(«Diffuse»));

Созданный материал будет использовать Diffuse shader. Чтобы сохранить этот материал в файл, мы можем вызвать AssetDatabase.CreateAsset() . Эта функция принимает актив в качестве первого аргумента, а путь — в качестве второго.

1
2
3
AssetDatabase.Refresh();
 
AssetDatabase.CreateAsset(new Material(Shader.Find(«Diffuse»)), «Assets/New Material.mat»);

Если вы запустите нашу процедуру сейчас, вы увидите, что материал создан.

Материал без назначенной текстуры.

Как вы видите, все правильно, его имя — New Material. Он использует Diffuse shader, но ему не присвоена текстура.


Сначала нам нужно получить ссылку на материал, который мы только что создали. Мы можем получить это, вызвав AssetDatabase.LoadAssetAtPath() который загружает актив и возвращает его ссылку.

1
2
3
AssetDatabase.CreateAsset(new Material(Shader.Find(«Diffuse»)), «Assets/New Material.mat»);
         
Material material = (Material) (AssetDatabase.LoadAssetAtPath(«Assets/New Material.mat»,typeof(Material)));

Теперь давайте назначим нашу сгенерированную текстуру в качестве основной текстуры материала. Мы можем получить ссылку на текстуру сгенерированной текстуры так же, как мы получили ссылку на материал.

1
2
Material material = (Material) (AssetDatabase.LoadAssetAtPath(«Assets/New Material.mat»,typeof(Material)));
material.mainTexture = (Texture2D) (AssetDatabase.LoadAssetAtPath(«Assets/texture.png», typeof(Texture2D)));

Чтобы увидеть результаты, запустите рутину материалов .

Материал с заданной текстурой.

Как вы видите, материал теперь имеет назначенную текстуру.

Это конец введения в управление вашими активами с помощью сценариев. Если вы хотите расширить свои знания по этой теме, вы можете посетить справочную страницу классов Unity Editor , особенно справку по сценарию AssetDatabase , которую стоит изучить . Если вам нужно работать на низком уровне, вам также следует прочитать документы по System.IO, чтобы получить больше информации о его классах и о том, как вы можете их использовать. Спасибо за ваше время!