Как стать автором
Обновить
650.96
OTUS
Цифровые навыки от ведущих экспертов

Трюки со скриптами в редакторе Unity, которые сэкономят вам кучу времени и нервов. Часть 1

Время на прочтение15 мин
Количество просмотров4.4K
Автор оригинала: Jordi Caballol

В большинстве проектов, которые мне довелось видеть, разработчикам приходится выполнять множество рутинных периодически повторяющихся задач, в которых очень легко оступиться и сделать ошибку, особенно когда речь идет об интеграции новых ассетов с артами. Например, добавление персонажа часто включает в себя перетаскивание множества ссылок на ассеты, установку кучи нужных флагов и прокликивание тучи кнопок: нужно установить риг модели на Humanoid, отключить sRGB у SDF-текстур, установить карты нормалей в качестве карт нормалей и текстуры пользовательского интерфейса в качестве спрайтов. Другими словами, на все это тратится драгоценное время, не говоря о том, что при этом могут быть пропущены какие-нибудь важные шаги.

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

Наш прототип выглядит следующим образом:

https://blog-api.unity.com/sites/default/files/videos/2022-10/00.%20InitialBuild.mp4

Трюк №1: Организация структуры и подготовка к автоматизации импорта ассетов

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

Самый простой способ узнать, для чего предназначен ассет и как он связан с другими частями проекта, — это придерживаться определенного соглашения об именовании и структуры папок, например:

  • Соглашение об именовании: мы можем добавлять определенные обозначения в название асета, например, Shield_BC.png — это базовый цвет (base color), а Shield_N.png — карта нормалей (normal map).

  • Структура папок: Knight/Animations/Walk.fbx очевидно представляет собой анимацию, а Knight/Models/Knight.fbx — модель, хотя оба эти файла имеют одинаковый формат (.fbx).

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

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

Если вы надумали автоматизировать эту последовательность, то вам очень пригодится класс под названием AssetPostprocessor. AssetPostprocessor получает различные сообщения, когда Unity импортирует ассеты. Одним из них является OnPostprocessAllAssets — метод, который вызывается всякий раз, когда Unity завершает импорт ассетов. С его помощью вы можете получить все пути к импортированным ассетам и впоследствии использовать их так, как вам будет нужно. Например, вы можете написать простой метод для их обработки, наподобие следующего:

private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, 
    string[] movedAssets, string[] movedFromAssetPaths)
{
    foreach (var asset in importedAssets)
        ProcessAssetLabels(asset);

    foreach (var asset in movedAssets)
        ProcessAssetLabels(asset);
}

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

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

Чтобы присвоить метки, загрузите ассет с помощью AssetDatabase.LoadAssetAtPath, затем присвойте их с помощью AssetDatabase.SetLabels.

var obj = AssetDatabase.LoadAssetAtPath<Object>(assetPath);
if (obj)
{
    if (labels.Count == 0)
    {
        AssetDatabase.ClearLabels(obj);
        return;
    }

    var oldLabels = AssetDatabase.GetLabels(obj);
    var labelsArray = new string[] { string.Join('-', labels) };
    if (HaveLabelsChanged(oldLabels, labelsArray))
    {
        AssetDatabase.SetLabels(obj, labelsArray);
    }
}

Но нужно отметить один важный момент – устанавливать метки нужно только в том случае, если они действительно изменились. Установка меток приведет к повторному импорту ассета, а вы бы не хотели, чтобы это произошло, если в этом нет крайней необходимости.

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

После выполнения вышеуказанных шагов все ассеты будут автоматически помечены, как в примере, приведенном ниже:

Пример материала с метками
Пример материала с метками

Трюк №2: Установка параметров и размеров текстур

Процесс импорта текстур в проект обычно включает в себя настройку параметров для каждой отдельной текстуры. Это обычная текстура? Это карта нормали? Спрайт? Линейная она или sRGB? Если вы хотите изменить настройки импортера ассетов, вы опять же можете воспользоваться AssetPostprocessor.

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

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

С этими знаниями вы можете написать простой TexturePreprocessor:

private void OnPreprocessTexture()
{
    var labels = AssetDatabase.GetLabels(assetImporter);

    // Мы хотим захватить только ассеты с артами
    if (labels.Length == 0 || !labels[0].Contains(AssetClassifier.ART_LABEL))
        return;

    // Получаем импортер
    var textureImporter = assetImporter as TextureImporter;
    if (!textureImporter)
        return;

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

AssetPostprocessor имеет свойство context, по которому можно определить целевую платформу. Благодаря ему вы можете внести разные изменения для каждой конкретной платформы, например, установить для текстур на мобильных устройствах более низкое разрешение:

// Устанавливаем размер текстуры. Для android и iOS он будет поменьше
if (context.selectedBuildTarget == BuildTarget.iOS || context.selectedBuildTarget == BuildTarget.Android)
    textureImporter.maxTextureSize = 256;
else
    textureImporter.maxTextureSize = 512;

Затем проверьте по метке, не является ли эта текстура частью пользовательского интерфейса, потому что тогда для нее нужно внести отдельные настройки:

// Текстуры пользовательского интерфейса — это особый случай
if (labels[0].Contains(AssetClassifier.UI_LABEL))
{
    textureImporter.textureType = TextureImporterType.Sprite;
    textureImporter.sRGBTexture = true;
    return;
}

Для остальных текстур можно установить значения по умолчанию. Стоит отметить, что Альбедо — единственная текстура с поддержкой sRGB:

// Все наши текстуры, которые дошли сюда, являются стандартными текстурами, и если мы используем нормали, то их также можно задать здесь
    textureImporter.textureType = TextureImporterType.Default;
    textureImporter.textureShape = TextureImporterShape.Texture2D;

    // Сделаем их доступными для обработки в редакторах
    textureImporter.isReadable = true;

    // sRGB только для альбедо-текстур 
    var texName = Path.GetFileNameWithoutExtension(assetPath);    
    textureImporter.sRGBTexture = labels[0].Contains(AssetClassifier.BASE_COLOR_LABEL)
}

Благодаря приведенному выше скрипту, когда вы перетаскиваете новые текстуры в редактор, они автоматически получают корректные настройки.

https://blog-api.unity.com/sites/default/files/videos/2022-10/01.%20DropTextures.mp4

Текстуры с установленными настройками

Трюк №3: Беремся за упаковку текстур “по каналам” 

“Упаковка по каналам” подразумевает объединение различных текстур в разные каналы одной текстуры. Это распространенная практика, которая дает много преимуществ. Например, значение красного (Red) канала определяет металлические свойства (metalness), а значение зеленого (Green) канала — гладкость (smoothness).

Однако объединение всех текстур в одну требует от команды художников дополнительной работы. Если по какой-либо причине необходимо изменить упаковку (например, изменить шейдер), команде художников придется переделать все текстуры, которые используются с этим шейдером.

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

Сначала я создаю файл-болванку с определенным расширением, а затем использую Scripted Importer, который берет на себя тяжелую работу по импорту этого ассета. Вот как это работает:

  • Импортеры могут иметь в качестве параметров текстуры, которые вам нужно комбинировать.

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

  • У импортера есть версия. Если вам нужно изменить упаковку текстур, вы можете изменить импортер и инкрементировать версию. Это вызовет регенерацию всех уже упакованных текстур в вашем проекте, благодаря чему все немедленно будет упаковано по-новому.

  • Приятным побочным эффектом создания чего-либо в импортере является то, что сгенерированные ассеты находятся только в папке Library, т.е. они не засоряют вашу систему контроля версий.

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

После этого вам нужно будет объявить класс-импортер и добавить ScriptedImporterAttribute для определения версии и расширения, связанных с импортером:

[ScriptedImporter(0, PathHelpers.TEXTURE_PACK_EXTENSION)]
public class TexturePackImporter : ScriptedImporter
{
    …
}

В импортере вам нужно объявите поля, которые вы хотите использовать. Они появятся в инспекторе наряду с MonoBehaviour’ами и ScriptableObject’ами:

public LazyLoadReference<Texture2D> albedo;
public LazyLoadReference<Texture2D> playerMap;
public LazyLoadReference<Texture2D> metallic;
public LazyLoadReference<Texture2D> smoothness;
Инспектор импортера
Инспектор импортера

Когда параметры готовы, создайте новые текстуры из тех, которые вы установили в качестве параметров. Обратите внимание, однако, что в препроцессоре (из предыдущего раздела) мы устанавливаем для isReadable значение True, чтобы это все было возможным.

В этом прототипе вы можете заметить две текстуры: Albedo, которое имеет альбедо в RGB и маску для применения цвета игрока в альфа-канале (Alpha), и текстуру маски (Mask), которая включает металлические свойства в красном канале и гладкость в зеленом.

Хотя это, вполне вероятно, выходит за рамки этой статьи, давайте рассмотрим в качестве примера, как объединить альбедо и маску игрока. Во-первых, проверьте, заданы ли текстуры, и если да, то получите их цветовые данные. Затем установите текстуры как зависимости, используя AssetImportContext.DependsOnArtifact. Как упоминалось выше, это заставит объект вычисляться заново, если какая-либо из текстур в конечном итоге изменится.

public Texture2D CombineAlbedoPlayer(AssetImportContext ctx)
{
    Color32[] albedoPixels = new Color32[0];
    bool albedoPresent = albedo.isSet;
    if (albedoPresent)
    {
        ctx.DependsOnArtifact(AssetDatabase.GetAssetPath(albedo.asset));
        albedoPixels = albedo.asset.GetPixels32();
    }

    Color32[] playerPixels = new Color32[0];
    bool playerPresent = playerMap.isSet;
    if (playerPresent)
    {
        ctx.DependsOnArtifact(AssetDatabase.GetAssetPath(playerMap.asset));
        playerPixels = playerMap.asset.GetPixels32();
    }

    if (!albedoPresent && !playerPresent)
        return null;

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

var size = TexturePreProcessor.GetMaxSizeForTarget(ctx.selectedBuildTarget);
var newTexture = new Texture2D(size, size, TextureFormat.RGBA32, true, false);
var pixels = new Color32[size * size];

Далее заполните все данные для новой текстуры. Этот этап можно значительно оптимизировать с помощью Jobs и Burst (но это уже тема для целой отдельной статьи). Здесь же мы будем использовать простой цикл:

Color32 tmp = new Color32();
Color32 white = new Color32(255, 255, 255, 255);
for (int i = 0; i < pixels.Length; ++i)
{
    var color = albedoPresent ? albedoPixels[i] : white;
    var alpha = playerPresent ? playerPixels[i] : white;
    tmp.r = color.r;
    tmp.g = color.g;
    tmp.b = color.b;
    tmp.a = alpha.r;
    pixels[i] = tmp;
}

Установите эти данные в текстуре:

newTexture.SetPixels32(pixels);
 // Примените изменения к mipmap
newTexture.Apply(true, false);
// Сжатие текстуры
newTexture.Compress(true);
newTexture.Apply(true, true);
newTexture.name = "AlbedoPlayer";
// Возврат результата
return newTexture;

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

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

public override void OnImportAsset(AssetImportContext ctx)
{
    var result = ScriptableObject.CreateInstance<TexturePack>();

    result.albedoPlayer = CombineAlbedoPlayer(ctx);
    if (result.albedoPlayer)
        ctx.AddObjectToAsset("albedoPlayer", result.albedoPlayer);

    result.mask = CombineMask(ctx);
    if (result.mask)
        ctx.AddObjectToAsset("mask", result.mask);

    ctx.AddObjectToAsset("result", result);
    ctx.SetMainObject(result);
}

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

С помощью атрибута MenuItem укажите полный путь к меню создания ассета, Assets/Create. Чтобы создать ассет, используйте ProjectWindowUtil.CreateAssetWithContent, который генерирует файл с указанным вами содержимым и позволяет пользователю ввести для него имя. Это будет выглядеть так:

[MenuItem("Assets/Create/Texture Pack", priority = 0)]
private static void CreateAsset()
{

Наконец, создайте результирующие многоканальные текстуры.

https://blog-api.unity.com/sites/default/files/videos/2022-10/02.%20TexturePack.mp4

Как будет выглядеть результирующая многоканальная текстура

Трюк 4: Использование кастомных шейдеров для материалов 

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

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

Для проверки того, что ассет соответствует определенным требованиям (в данном случае, что он использует правильный шейдер) есть еще один полезный класс - AssetModificationProcessor. С помощью AssetModificationProcessor.OnWillSaveAssets вы будете уведомлены, когда Unity собирается записать ассет на диск. Это даст вам возможность проверить правильность ассета и исправить его перед сохранением.

Кроме того, вы можете “указать” Unity не сохранять ассет, что очень полезно, когда обнаруженную вами проблему нельзя устранить автоматически. Для этого создайте метод OnWillSaveAssets:

private static string[] OnWillSaveAssets(string[] paths)
{
    foreach (string path in paths)
    {
        ProcessMaterial(path);
    }

    // Если вы не хотите сохранять ассет, удалите из списка его путь
    return paths;
}

В рамках обработки ассетов, проверьте, являются ли они материалами и имеют ли они правильные метки. Если они удовлетворяют приведенный ниже код, то у вас корректный шейдер:

private static void ProcessMaterial(string path)
{
    var mat = AssetDatabase.LoadAssetAtPath<Material>(path);
    // Проверьте, является ли ассет материалом
    if (!mat)
        return;

    // проверьте, является ли ассет юнитом или зданием
    var labels = AssetDatabase.GetLabels(mat);
    if (labels.Length == 0 || !(labels[0].Contains(AssetClassifier.UNIT_LABEL) 
        || labels[0].Contains(AssetClassifier.BUILDING_LABEL)))
        return;

    if (mat.shader.name != UNIT_SHADER_NAME)
    {
        mat.shader = Shader.Find(UNIT_SHADER_NAME);
    }
}

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

https://blog-api.unity.com/sites/default/files/videos/2022-10/03.%20Validation.mp4

Скрипт в действии

Мы также располагаем Material Variants, которые были представлены в качестве новой фичи Unity 2022. Варианты материалов невероятно полезны при создании материалов для юнитов. Фактически, вы можете создать базовый материал и отпочковать от него материалы для каждого юнита, переопределив соответствующие поля (например, текстуры) и унаследовав остальные свойства. Это позволяет использовать устойчивые дефолтные значения для наших материалов, которые можно обновлять по мере необходимости.

Трюк 5: Управление анимациями

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

Unity по умолчанию импортирует материалы всех FBX (.fbx) файлов. Что касается анимаций - материалы, которые вы хотите использовать, будут либо в проекте, либо в FBX меша. Вы будет обнаруживать лишние материалы из FBX анимации каждый раз при поиске материалов в проекте, что достаточно загрязняет проект, поэтому их стоит отключить.

Для настройки рига (то есть выбора между Humanoid и Generic, а в случаях, когда мы используем тщательно настроенный аватар, его назначении) применяем тот же подход, который применялся к текстурам. Но для анимации вы будете использовать сообщение AssetPostprocessor.OnPreprocessModel. Оно будет вызываться для всех FBX файлов, поэтому вам нужно отличать FBX файлы анимации от FBX файлов модели.

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

private void OnPreprocessModel()
{
    // Нам нужны только анимации
    var labels = AssetDatabase.GetLabels(assetImporter);
    if (labels.Length == 0 || !labels[0].Contains(AssetClassifier.ANIMATION_LABEL))
        return;

    // Получаем импортер
    var modelImporter = assetImporter as ModelImporter;
    if (!modelImporter)
        return;

    // Нам нужна новая анимация
    modelImporter.importAnimation = true;
    // И нам не нужен материал
    modelImporter.materialImportMode = ModelImporterMaterialImportMode.None;

Далее вам нужно использовать риг из FBX меша, поэтому вам нужно найти соответствующий ассет. Найти этот ассет можно опять же с помощью метки. Для нашего прототипа метки анимаций заканчиваются на “animation”, тогда как метки мешей заканчиваются на “model”. Вы можете выполнить простую замену, чтобы получить метку для своей модели. Получив метку, найдите свой ресурс с помощью AssetDatabase.FindAssets с параметром l:label-name.

При доступе к другим ассетам следует учитывать еще кое-что: возможно, что в середине процесса импорта аватар еще не был импортирован, когда вызывается этот метод. В этом случае LoadAssetAtPath вернет значение null, и вы не сможете установить аватар. Чтобы обойти эту проблему, установите зависимость с путем аватара. Анимация будет снова импортирована после импорта аватара, и вы сможете установить ее после этого.

В виде кода это все будет выглядеть примерно так:

// Пробуем получить аватар
var avatarLabel = labels[0].Replace(AssetClassifier.ANIMATION_LABEL, AssetClassifier.MODEL_LABEL);
var possibleModels = AssetDatabase.FindAssets("l:" + avatarLabel);
Avatar avatar = null;
if (possibleModels.Length > 0)
{
    var avatarPath = AssetDatabase.GUIDToAssetPath(possibleModels[0]);
    avatar = AssetDatabase.LoadAssetAtPath<Avatar>(avatarPath);

    if (!avatar)
        context.DependsOnArtifact(avatarPath);
}

modelImporter.animationType = ModelImporterAnimationType.Generic;
modelImporter.sourceAvatar = avatar;
modelImporter.avatarSetup = ModelImporterAvatarSetup.CopyFromOther;

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

Все это вы можете увидеть на видео ниже:

https://blog-api.unity.com/sites/default/files/videos/2022-10/04.%20DropAnimations.mp4

Пример добавления анимаций

Трюк 6: Настройка меша с помощью FBX импортеров 

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

Для нашего прототипа я настроил импортер так, чтобы он не генерировал материалы (я буду использовать те, которые я создал в проекте) и проверял, является ли модель юнитом или зданием (как всегда, проверив метку). Юниты настроены на генерацию аватара, но для зданий генерация аватара отключена, так как здания не анимированы.

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

Для этого используйте метод AssetPostprocessor.OnPostprocessModel. Этот метод вызывается после завершения импорта модели. Он получает сгенерированный префаб в качестве параметра, что позволяет нам изменять префаб так, как мы хотим.

В нашем прототипе я искал материал и Animation Controller, сопоставляя метку, точно так же, как я искал и аватар для анимации. С рендерером и аниматором в префабе я установил материал и контроллер, как в обычном геймплее.

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

https://blog-api.unity.com/sites/default/files/videos/2022-10/05.%20DropModel.mp4

Перетащите модель в свой проект, и она будет готова к использованию в любой сцене


Скоро состоится два практических открытых урока, посвященных созданию 2D-платформера на Unity. На первом занятии поговорим и попробуем на примерах 2D-платформеров собрать прототип 2D-уровня. Узнаем, с чего начинают делать игру, так чтобы несколько месяцев работы не прошли в пустую. Ознакомимся с основными принципами создания прототипов игр. Регистрация на урок открыта по ссылке для всех желающих.

Теги:
Хабы:
Всего голосов 6: ↑4 и ↓2+3
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS