Использование .NET 4.x в Unity

C# и .NET, технологии, лежащие в основе базовых сценариев Unity, получают обновления с тех пор, как корпорация Майкрософт выпустила их в 2002 году. Но разработчики Unity могут не учитывать устойчивый поток новых функций, добавленных на язык C# и платформа .NET Framework, так как до Unity 2017.1 Unity использовал среду выполнения сценариев .NET 3.5, отсутствующие годы обновления.

В выпуске Unity 2017.1 Unity представила экспериментальную версию среды выполнения сценариев, обновленную до совместимой версии .NET 4.6, C# 6.0. В Unity 2018.1 аналогичная среда выполнения .NET 4.x уже не считается экспериментальный, а вот более ранняя аналогичная среда выполнения .NET 3.5 теперь считается устаревшей. В выпуске Unity 2018.3 Unity проецируется, чтобы обновить среду выполнения скриптов по умолчанию и обновить еще больше до C# 7. Дополнительные сведения и последние обновления на этой дорожной карте см. в записи блога Unity или посетите свой форум по предварительной версии экспериментальных сценариев. А пока прочтите следующие разделы этой статьи и узнайте о новых функциях, появившихся в среде выполнения сценариев .NET 4.x.

Обязательные условия

Включение среды выполнения сценариев .NET 4.x в Unity

Чтобы включить среду выполнения сценариев .NET 4.x, выполните следующие действия.

  1. Откройте PlayerSettings в инспекторе Unity, выбрав "Изменить параметры > проекта", чтобы выбрать > другие параметры проигрывателя>.

  2. В разделе "Конфигурация" щелкните раскрывающийся список "Уровень совместимости API" и выберите платформа .NET Framework. Вам будет предложено перезапустить Unity.

Снимок экрана: эквивалент Select .NET 4.x.

Выбор между профилями .NET 4.x и .NET Standard 2.1

После перехода на эквивалентную среду выполнения сценариев .NET 4.x можно указать уровень совместимости API с помощью раскрывающегося меню в PlayerSettings (изменение > проигрывателя параметров > проекта). Существует два варианта.

  • .NET Standard 2.1. Этот профиль соответствует профилю .NET Standard 2.1 , опубликованному .NET Foundation. Unity рекомендует .NET Standard 2.1 для новых проектов. Эта версия ниже, чем .NET 4.x, что полезно для платформ ограниченного размера. Кроме того, Unity обеспечивает поддержку этого профиля на всех поддерживаемых Unity платформах.

  • платформа .NET Framework. Этот профиль предоставляет доступ к последней версии API .NET 4. Он включает весь код, доступный в библиотеках классов платформа .NET Framework и поддерживает профили .NET Standard 2.1. Выбирайте профиль .NET 4.x, если для вашего проекта требуется та часть API, которая не входит в профиль .NET Standard 2.0. При этом некоторые части этого API могут поддерживаться не на всех платформах Unity.

Дополнительные сведения об этих вариантах см. в этой записи блога Unity.

Добавление ссылок на сборки при использовании уровня совместимости API .NET 4.x

При использовании параметра .NET Standard 2.1 в раскрывающемся списке уровня совместимости API все сборки в профиле API ссылаются и используются. Однако при использовании более крупного профиля .NET 4.x некоторые сборки, поставляемые с Unity, по умолчанию не ссылаются. Чтобы использовать эти API, необходимо добавить ссылку на сборку вручную. Вы можете просмотреть сборки, которые входят в каталог MonoBleedingEdge/lib/mono вашей установки редактора Unity:

Снимок экрана: каталог MonoBleedingEdge.

Например, если вы используете профиль .NET 4.x и хотите использовать HttpClient, необходимо добавить ссылку на сборку System.Net.Http.dll. Если этого не сделать, компилятор выдаст сообщение об отсутствующей ссылке на сборку:

Снимок экрана: отсутствует ссылка на сборку.

Visual Studio повторно создает CSPROJ-файлы и SLN-файлы для проектов Unity при каждом открытии. В результате нельзя добавлять ссылки на сборки непосредственно в Visual Studio, так как они будут потеряны при повторном открытии проекта. Вместо этого необходимо использовать специальный текстовый файл csc.rsp :

  1. Создайте текстовый файл с именем csc.rsp в корневом каталоге ресурсов проекта Unity.

  2. В первой строке пустого текстового файла введите: -r:System.Net.Http.dll, а затем сохраните файл. Вместо System.Net.Http.dll можно указать любую добавленную сборку с отсутствующей ссылкой.

  3. Перезапустите редактор Unity.

Преимущества совместимости с .NET

Помимо новых возможностей синтаксиса и языка C#, среда выполнения сценариев .NET 4.x предоставляет пользователям Unity доступ к огромной библиотеке пакетов .NET, несовместимых с устаревшей средой выполнения сценариев .NET 3.5.

Добавление пакетов из NuGet в проект Unity

NuGet — это диспетчер пакетов для .NET. NuGet интегрирована в Visual Studio. Однако для проектов Unity требуется специальный процесс добавления пакетов NuGet, так как при открытии проекта в Unity файлы проектов Visual Studio создаются повторно, отменяя необходимые конфигурации. Чтобы добавить пакет из NuGet, в проект Unity:

  1. Откройте NuGet и найдите совместимый пакет, который вам нужно добавить (.NET Standard 2.0 или .NET 4.x). В этом примере показано добавление Json.NET (популярного пакета для работы с JSON) в проект .NET Standard 2.0.

  2. Нажмите кнопку Загрузить:

    Снимок экрана: кнопка скачивания.

  3. Найдите загруженный файл и измените его расширение с .nupkg на .zip.

  4. В ZIP-файле зайдите в каталог lib/netstandard2.0 и скопируйте файл Newtonsoft.Json.dll.

  5. В папке Assets в корневом каталоге проекта Unity создайте папку с именем Plugins (Подключаемые модули). В Unity "Plugins" — это имя специальной папки. Дополнительные сведения см. в документации по Unity.

  6. Скопируйте файл Newtonsoft.Json.dll в каталог Plugins проекта Unity.

  7. Создайте файл с именемlink.xml в каталоге ресурсов проекта Unity и добавьте следующий XML-код, чтобы процесс удаления байт-кода Unity не удалял необходимые данные при экспорте на платформу IL2CPP. Несмотря на то что это действие относится только к данной библиотеке, проблемы могут возникнуть и с другими библиотеками, которые используют отражение подобным образом. Дополнительные сведения см. в документации по Unity в этой статье.

    <linker>
      <assembly fullname="System.Core">
        <type fullname="System.Linq.Expressions.Interpreter.LightLambda" preserve="all" />
      </assembly>
    </linker>
    

Теперь пакет Json.NET готов к работе.

using Newtonsoft.Json;
using UnityEngine;

public class JSONTest : MonoBehaviour
{
    class Enemy
    {
        public string Name { get; set; }
        public int AttackDamage { get; set; }
        public int MaxHealth { get; set; }
    }
    private void Start()
    {
        string json = @"{
            'Name': 'Ninja',
            'AttackDamage': '40'
            }";

        var enemy = JsonConvert.DeserializeObject<Enemy>(json);

        Debug.Log($"{enemy.Name} deals {enemy.AttackDamage} damage.");
        // Output:
        // Ninja deals 40 damage.
    }
}

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

Новые возможности синтаксиса и языка

Использование обновленной среды выполнения сценариев предоставляет разработчикам Unity доступ к C# 8 и множество новых функций языка и синтаксиса.

Инициализаторы автосвойств

Синтаксис автосвойств в среде выполнения сценариев Unity .NET 3.5 позволял быстро определить неинициализированные свойства, но саму инициализацию нужно прописывать в сценарии отдельно. В среде выполнения .NET 4.x автосвойства можно инициализировать в той же строке:

// .NET 3.5
public int Health { get; set; } // Health has to be initialized somewhere else, like Start()

// .NET 4.x
public int Health { get; set; } = 100;

Интерполяция строк

В более ранней среде выполнения .NET 3.5 синтаксис объединения строк был громоздким. В новой среде выполнения .NET 4.x есть функция $интерполяции строк, которая позволяет вставлять выражения в строки, используя более прямой и удобочитаемый синтаксис:

// .NET 3.5
Debug.Log(String.Format("Player health: {0}", Health)); // or
Debug.Log("Player health: " + Health);

// .NET 4.x
Debug.Log($"Player health: {Health}");

Элементы, воплощающие выражение

Новый синтаксис C# в среде выполнения .NET 4.x позволяет заменять тело функций на лямбда-выражения и, таким образом, делать их более краткими:

// .NET 3.5
private int TakeDamage(int amount)
{
    return Health -= amount;
}

// .NET 4.x
private int TakeDamage(int amount) => Health -= amount;

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

// .NET 4.x
public string PlayerHealthUiText => $"Player health: {Health}";

Асинхронный шаблон, основанный на задачах (TAP)

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

В Unity асинхронное программирование обычно выполняется с соподпрограммами. Однако начиная с C# 5 предпочтительным методом асинхронного программирования в среде разработки .NET стал Асинхронный шаблон, основанный на задачах (TAP), с использованием ключевых слов async и await в System.Threading.Task. Таким образом, для функции async можно задать ожидание завершения задачи (await) без запрета обновлений в остальной части приложения:

// Unity coroutine
using UnityEngine;
public class UnityCoroutineExample : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(WaitOneSecond());
        DoMoreStuff(); // This executes without waiting for WaitOneSecond
    }
    private IEnumerator WaitOneSecond()
    {
        yield return new WaitForSeconds(1.0f);
        Debug.Log("Finished waiting.");
    }
}
// .NET 4.x async-await
using UnityEngine;
using System.Threading.Tasks;
public class AsyncAwaitExample : MonoBehaviour
{
    private async void Start()
    {
        Debug.Log("Wait.");
        await WaitOneSecondAsync();
        DoMoreStuff(); // Will not execute until WaitOneSecond has completed
    }
    private async Task WaitOneSecondAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Debug.Log("Finished waiting.");
    }
}

TAP — это сложная тема с определенными характерными для Unity нюансами, которые разработчикам необходимо учитывать, В результате TAP не является универсальной заменой сопрограмм в Unity; однако это еще один инструмент для использования. Сфера применения этого компонента выходит за рамки данной статьи, но некоторые общие рекомендации и советы вы найдете ниже.

Начало работы с TAP в Unity

Эти советы помогут вам приступить к работе с TAP в Unity.

  • Асинхронные функции, выполнения которых код будет дожидаться, должны иметь тип возвращаемого значения Task или Task<TResult>.
  • К именам асинхронных функций, возвращающих задачу, необходимо добавлять суффикс Async. Суффикс Async означает, что эта функция требует ожидания.
  • Используйте тип возвращаемого значения async void только для тех функций, которые запускают асинхронные функции из традиционного синхронного кода. Такие функции не могут ждать себя и не должны иметь суффикс Async в именах.
  • Чтобы асинхронные функции по умолчанию выполнялись в основном потоке, в Unity используется unitySynchronizationContext. API Unity за пределами основного потока недоступен.
  • Можно выполнять задачи в фоновых потоках с помощью таких методов, как Task.Run и Task.ConfigureAwait(false). Этот прием пригодится для выгрузки ресурсоемких операций из основного потока и повышения производительности. При этом использование фоновых потоков может привести к возникновению трудно устраняемых проблем, например привести к состоянию гонки.
  • API Unity за пределами основного потока недоступен.
  • Задачи, использующие потоки, в сборках Unity WebGL не поддерживаются.

Различия между соподпрограммами и TAP

Между соподпрограммами и TAP (async-await) есть несколько важных различий.

  • Сопрограммы не могут возвращать значения, но Task<TResult> могут.
  • Вы не можете поместить инструкцию yield try-catch, что затрудняет обработку ошибок с сопрограммами. При этом try-catch работает с TAP.
  • Функция соподпрограмм в Unity недоступна в классах, которые не являются производными от MonoBehaviour. Для асинхронного программирования в таких классах отлично подходит TAP.
  • В настоящее время Unity не предлагает TAP как полную замену соподпрограмм. Узнать результаты применения одного или другого подхода для конкретного проекта можно только путем профилирования.

Оператор nameof

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

// Get the string name of an enum:
enum Difficulty {Easy, Medium, Hard};
private void Start()
{
    Debug.Log(nameof(Difficulty.Easy));
    RecordHighScore("John");
    // Output:
    // Easy
    // playerName
}
// Validate parameter:
private void RecordHighScore(string playerName)
{
    Debug.Log(nameof(playerName));
    if (playerName == null) throw new ArgumentNullException(nameof(playerName));
}

Информационные атрибуты вызывающего объекта

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

private void Start ()
{
    ShowCallerInfo("Something happened.");
}
public void ShowCallerInfo(string message,
        [System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
        [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
        [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
    Debug.Log($"message: {message}");
    Debug.Log($"member name: {memberName}");
    Debug.Log($"source file path: {sourceFilePath}");
    Debug.Log($"source line number: {sourceLineNumber}");
}
// Output:
// Something happened
// member name: Start
// source file path: D:\Documents\unity-scripting-upgrade\Unity Project\Assets\CallerInfoTest.cs
// source line number: 10

Директива using static

Директива using static позволяет использовать статические функции, не указывая имя класса. Директива using static помогает сэкономить место и время при использовании нескольких статических функций одного класса.

// .NET 3.5
using UnityEngine;
public class Example : MonoBehaviour
{
    private void Start ()
    {
        Debug.Log(Mathf.RoundToInt(Mathf.PI));
        // Output:
        // 3
    }
}
// .NET 4.x
using UnityEngine;
using static UnityEngine.Mathf;
public class UsingStaticExample: MonoBehaviour
{
    private void Start ()
    {
        Debug.Log(RoundToInt(PI));
        // Output:
        // 3
    }
}

Рекомендации по IL2CPP

При экспорте игры на такие платформы, как iOS, Unity будет использовать его подсистему IL2CPP для транспилирования IL в код C++, который затем компилируется с помощью собственного компилятора целевой платформы. В этом сценарии существует несколько функций .NET, которые не поддерживаются, такие как части отражения и использование ключевого dynamic слова. Хотя вы можете управлять использованием этих функций в собственном коде, вы можете столкнуться с проблемами с использованием сторонних библиотек DLL и пакетов SDK, которые не были написаны с помощью Unity и IL2CPP. Дополнительные сведения об этой статье см. в документации по ограничениям сценариев на сайте Unity.

Кроме того, как уже говорилось в приведенном выше примере Json.NET, во время экспорта IL2CPP Unity попытается удалить неиспользуемый код. Хотя этот процесс обычно не является проблемой, с библиотеками, используюющими отражение, он может случайно удалить свойства или методы, которые будут вызываться во время выполнения, которые не могут быть определены во время экспорта. Чтобы устранить эти проблемы, добавьте в проект файлlink.xml , содержащий список сборок и пространств имен, для того чтобы не запускать процесс удаления. Дополнительные сведения см. в документации Unity по разделению байт-кода.

Образец проекта Unity в .NET 4.x

Данный образец содержит примеры использования нескольких функций .NET 4.x. Загрузить проект или просмотреть исходный код можно в GitHub.

Дополнительные ресурсы