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

C# и .NET технологии, базовые сценарии Unity, продолжают получать обновления, так как Microsoft первоначально выпустили их в 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 в окне Inspector в Unity, выбрав Edit > Project Settings > Player > Other Settings.

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

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

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

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

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

  • .NET Framework. Этот профиль предоставляет доступ к последнему .NET 4 API. Он включает весь код из библиотек классов .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, необходимо вручную добавить ссылку на сборку. Сборки, поставляемые с Unity, можно просмотреть в каталоге 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. В корневой папке проекта Unity создайте новую папку с именем Подключаемые модули. Plugins — это специальное название папки в Unity. Дополнительные сведения см. в документации по Unity.

  6. Вставьте файлNewtonsoft.Json.dll в каталог подключаемых модулей проекта 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 используют другие пакеты 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));
}

Атрибуты вызывающего абонента

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

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

Использование статики

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

// .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 выше, Unity попытается удалить неиспользуемый код во время процесса экспорта IL2CPP. Хотя этот процесс обычно не вызывает проблем, в случае библиотек, использующих рефлексию, он может случайно удалить свойства или методы, вызовы которых происходят во время выполнения и которые невозможно определить на этапе экспорта. Чтобы устранить эти проблемы, добавьте в проект файл link.xml, содержащий список сборок и пространств имён, к которым не следует применять процесс удаления. Дополнительные сведения см. в документации Unity по разделению байтов.

Пример проекта Unity для .NET 4.x

Пример содержит примеры нескольких функций .NET 4.x. Вы можете скачать проект или просмотреть исходный код на GitHub.

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