Uso de .NET 4.x en Unity

C# y .NET, las tecnologías subyacentes de scripting de Unity, han seguido recibiendo actualizaciones desde que Microsoft las publicó originalmente en 2002. Pero es posible que los desarrolladores de Unity no conozcan el flujo estable de nuevas características agregadas al lenguaje C# y a .NET Framework, ya que antes de Unity 2017.1, Unity usaba un runtime de scripting equivalente de .NET 3.5 y faltan años de actualizaciones.

Con el lanzamiento de Unity 2017.1, Unity introdujo una versión experimental de su runtime de scripting actualizado a una versión compatible de .NET 4.6 y C# 6.0. En Unity 2018.1, el runtime equivalente a .NET 4.x ya no se considera experimental, mientras que el runtime equivalente a .NET 3.5 se considera ahora la versión heredada. Con el lanzamiento de Unity 2018.3, se el runtime de scripting actualizado será la selección predeterminada en Unity y que se podrá realizar la actualización incluso a versiones posteriores a C# 7. Para más información y para obtener las actualizaciones más recientes de esta guía, lea la entrada de blog de Unity o visite el foro de versiones preliminares del scripting experimental. Mientras tanto, consulte las secciones siguientes para saber más sobre las nuevas características que ya están disponibles con el runtime de scripting de .NET 4.x.

Requisitos previos

Habilitar el runtime de scripting de .NET 4.x en Unity

Para habilitar el runtime de scripting de .NET 4.x, siga estos pasos:

  1. Abra Configuración del reproductor en el inspector de Unity. Para ello, seleccione Editar > Configuración del proyecto > Reproductor > Otra configuración.

  2. En el encabezado Configuración, haga clic en la lista desplegable Nivel de compatibilidad de API y seleccione .NET Framework. Se le solicitará que reinicie Unity.

Screenshot showing the Select .NET 4.x equivalent.

Elección entre los perfiles de .NET 4.x y .NET Standard 2.1

Una vez que haya cambiado al runtime de scripting equivalente a .NET 4.x, puede especificar el nivel de compatibilidad de API mediante el menú desplegable en Configuración del reproductor (Editar > Configuración del proyecto > Reproductor). Hay dos opciones:

  • .NET Standard 2.1. Este perfil coincide con el perfil de .NET Standard 2.1 publicado por .NET Foundation. Unity recomienda .NET Standard 2.1 para los nuevos proyectos. Es menor que .NET 4.x, lo que supone una ventaja para plataformas de tamaño limitado. Además, Unity se ha comprometido a admitir este perfil en todas las plataformas compatibles con Unity.

  • .NET Framework. Este perfil proporciona acceso a la API de .NET 4 más reciente. Incluye todo el código disponible en las bibliotecas de clases de .NET Framework y admite también los perfiles de .NET Standard 2.1. Si el proyecto necesita parte de la API que no está incluida en el perfil de .NET Standard 2.0, use el perfil de .NET 4.x. Pero tenga en cuenta que algunas partes de esta API no se admiten en todas las plataformas de Unity.

Encontrará más información sobre estas opciones en esta entrada del blog de Unity.

Agregar referencias de ensamblado al usar el nivel de compatibilidad de la API de .NET 4.x

Si se selecciona la opción .NET Standard 2.1 en el menú desplegable Nivel de compatibilidad de API, no solo se puede hacer referencia a todos los ensamblados del perfil de API, sino que también se pueden usar. Pero cuando se usa el perfil de .NET 4.x mayor, no se hace referencia de manera predeterminada a algunos de los ensamblados que Unity incluye. Para usar estas API, debe agregar manualmente una referencia de ensamblado. Puede ver los ensamblados que Unity envía en el directorio MonoBleedingEdge/lib/mono de la instalación del editor de Unity:

Screenshot showing the MonoBleedingEdge directory.

Por ejemplo, si está usando el perfil de .NET 4.x y quiere usar HttpClient, debe agregar una referencia de ensamblado para System.Net.Http.dll. Sin ella, el compilador se quejará de que falta una referencia de ensamblado:

Screenshot showing the missing assembly reference.

Visual Studio vuelve a generar los archivos .csproj y .sln para los proyectos de Unity cada vez que se abren. En consecuencia, no se pueden agregar referencias de ensamblado directamente en Visual Studio, ya que se perderán al volver a abrir el proyecto. En su lugar, se debe usar un archivo de texto especial denominado csc.rsp:

  1. Cree un archivo de texto denominado csc.rsp en el directorio raíz Assets del proyecto de Unity.

  2. En la primera línea del archivo de texto vacío, escriba -r:System.Net.Http.dll y guarde el archivo. Puede reemplazar "System.Net.Http.dll" con cualquier ensamblado incluido al que pueda faltarle una referencia.

  3. Reinicie el editor de Unity.

Aprovechar la compatibilidad de .NET

Además de la nueva sintaxis de C# y las características de idioma, el runtime de scripting de .NET 4.x ofrece a los usuarios de Unity acceso a una gran biblioteca de paquetes de .NET que no son compatibles con el runtime de scripting de .NET 3.5 heredado.

Agregar paquetes de NuGet a un proyecto de Unity

NuGet es el administrador de paquetes para .NET. NuGet está integrado en Visual Studio, Sin embargo, los proyectos de Unity requieren un proceso especial para agregar paquetes NuGet porque al abrir un proyecto en Unity, sus archivos de proyecto de Visual Studio se vuelven a generar y se deshacen las configuraciones necesarias. Para agregar un paquete de NuGet al proyecto de Unity:

  1. Examine NuGet para encontrar un paquete compatible que quiera agregar (.NET Standard 2.0 o .NET 4.x). En este ejemplo se muestra la adición de Json.NET, un conocido paquete para trabajar con JSON, a un proyecto de .NET Standard 2.0.

  2. Haga clic en el botón Descargar:

    Screenshot showing the download button.

  3. Busque el archivo descargado y cambie la extensión de .nupkg a .zip.

  4. En el archivo zip, vaya al directorio lib/netstandard2.0 y copie el archivo Newtonsoft.Json.dll.

  5. En la carpeta raíz Assets del proyecto de Unity, cree una carpeta nueva denominada Plugins. Plugins es un nombre de carpeta especial en Unity. Para más información, consulte la documentación sobre Unity.

  6. Pegue el archivo Newtonsoft.Json.dll en el directorio Plugins del proyecto de Unity.

  7. Cree un archivo denominado link.xml en el directorio Assets del proyecto de Unity y agregue el siguiente XML, lo que garantiza que el proceso de eliminación de código de bytes de Unity no quita los datos necesarios al realizar la exportación a una plataforma IL2CPP. Aunque este paso es específico para esta biblioteca, es posible que experimente problemas con otras bibliotecas que usen Reflexión de manera similar. Para más información,vea los documentos de Unity de este artículo.

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

Ahora que todo está donde le corresponde, ya puede usar el paquete 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.
    }
}

Esto es un ejemplo sencillo de cómo usar una biblioteca que no tiene dependencias. Cuando los paquetes de NuGet dependen de otros paquetes de NuGet, tendrá que descargar estas dependencias manualmente y agregarlas al proyecto de la misma manera.

Nuevas características de idioma y sintaxis

El uso del runtime de scripting actualizado permite a los desarrolladores de Unity acceder a C# 8 y un sinfín de nuevas características de idioma y a su sintaxis.

Inicializadores de propiedades automáticas

En el runtime de scripting de .NET 3.5 de Unity, la sintaxis de propiedad automática facilita la tarea de definir rápidamente las propiedades sin inicializar, pero la inicialización tiene que producirse en cualquier otro lugar en el script. Ahora con el runtime de .NET 4.x, es posible inicializar propiedades automáticas en la misma línea:

// .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;

Interpolación de cadenas

Con el runtime de .NET 3.5 anterior, para la concatenación de cadenas se necesitaba una sintaxis extraña. Ahora con el runtime de .NET 4.x, la característica de $interpolación de cadenas permite insertar expresiones en las cadenas en una sintaxis más legible y directa:

// .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}");

Miembros con forma de expresión

Con la nueva sintaxis de C# disponible en el runtime de .NET 4.x, las expresiones lambda pueden reemplazar el cuerpo de las funciones para que sean más concisas:

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

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

También se pueden usar miembros con cuerpo de expresión en propiedades de solo lectura:

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

Modelo asincrónico basado en tareas (TAP)

Programación asincrónica permite que las operaciones que tardan mucho en completarse se lleven a cabo sin provocar que la aplicación deje de responder. Esta función también permite que el código espere a que finalicen las operaciones que tardan mucho antes de seguir con el código que depende de los resultados de estas operaciones. Por ejemplo, puede esperar a que se cargue un archivo o a que se complete una operación de red.

En Unity, la programación asincrónica suele completarse con corrutinas. Pero desde C# 5, el método preferido de la programación asincrónica en el desarrollo de .NET ha sido el modelo asincrónico basado en tareas (TAP) usando las palabras clave async y await con System.Threading.Task. En resumen, en una función async puede usar await para esperar a que finalice una tarea sin impedir que el resto de la aplicación se actualice:

// 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 es un tema complejo que presenta matices específicos de Unity que los desarrolladores deben tener en cuenta. En consecuencia, TAP no es una sustitución universal para corrutinas en Unity, sino que se trata de otra herramienta que se puede usar. La descripción de esta característica va más allá del ámbito de este artículo, pero aquí ofrecemos algunas prácticas recomendadas y consejos generales.

Referencia de introducción a TAP con Unity

Estas sugerencias pueden ayudarle a empezar a trabajar con TAP en Unity:

  • Las funciones asincrónicas destinadas a ser esperadas deben tener el tipo de valor devuelto Task o Task<TResult>.
  • Las funciones asincrónicas que devuelven una tarea deben tener el sufijo "Async" anexado a sus nombres. El sufijo "Async" le ayuda a indicar que siempre se debe esperar una función.
  • Use solo el tipo de valor devuelto async void para las funciones que activan funciones asincrónicas desde el código sincrónico tradicional. No se puede esperar a estas funciones y no deberían tener el sufijo "Async" en sus nombres.
  • Unity usa UnitySynchronizationContext para asegurarse de que se ejecutarán las funciones asincrónicas en el subproceso principal de forma predeterminada. No se puede acceder a la API de Unity fuera del subproceso principal.
  • Es posible ejecutar tareas en subprocesos en segundo plano con métodos como Task.Run y Task.ConfigureAwait(false). Esta técnica es útil para la descarga de operaciones costosas desde el subproceso principal para mejorar el rendimiento. Pero el uso de subprocesos en segundo plano puede provocar problemas que son difíciles de depurar, como las condiciones de carrera.
  • No se puede acceder a la API de Unity fuera del subproceso principal.
  • Las tareas que usan subprocesos no se admiten en las compilaciones WebGL de Unity.

Diferencias entre corrutinas y TAP

Hay algunas diferencias importantes entre las corrutinas y TAP/async-await:

  • Las corrutinas no pueden devolver valores, pero Task<TResult> sí que puede.
  • yield no se puede incluir en una instrucción try-catch, ya que dificultaría el control de errores con corrutinas. Pero try-catch sí funciona con TAP.
  • La característica de corrutina de Unity no está disponible en las clases que no derivan de MonoBehaviour. TAP es excelente para la programación asincrónica en esas clases.
  • En este momento, Unity no sugiere que TAP sea el sustituto principal de corrutinas. La creación de perfiles es la única manera de conocer los resultados específicos de un enfoque con respecto a los de un proyecto determinado.

operador nameof

El operador nameof obtiene el nombre de cadena de una variable, tipo o miembro. Algunos casos en los que nameof resulta útil es en los registros de errores y al obtener el nombre de cadena de una enumeración:

// 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));
}

Atributos de información del llamador

Los atributos de información del llamador proporcionan información sobre el llamador de un método. Debe proporcionar un valor predeterminado para cada parámetro que quiera usar con un atributo de información del llamador:

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

Uso de versión estática

Uso de versión estática permite usar funciones estáticas sin necesidad de escribir su nombre de clase. Con el uso de versión estática, puede ahorrar espacio y tiempo si necesita usar varias funciones estáticas de la misma clase:

// .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
    }
}

Consideraciones sobre IL2CPP

Al exportar un juego a plataformas como iOS, Unity usará su motor IL2CPP para "transpilar" el lenguaje intermedio al código de C++, que luego se compila mediante el compilador nativo de la plataforma de destino. En este caso, hay varias características de .NET que no son compatibles, como partes de Reflexión y el uso de la palabra clave dynamic. Aunque puede controlar el uso de estas características en su propio código, es posible que tenga problemas al usar archivos DLL y SDK de terceros que no se escribieron pensando en Unity e IL2CPP. Para más información sobre este artículo, vea los documentos sobre restricciones de scripting en el sitio de Unity.

Además, como se mencionó en el ejemplo anterior de Json.NET, Unity intentará eliminar código no utilizado durante el proceso de exportación de IL2CPP. Aunque este proceso normalmente no es un problema, con las bibliotecas que usan Reflexión, puede que se eliminen accidentalmente las propiedades o los métodos que se llamarán en tiempo de ejecución y que no se pueden determinar en tiempo de exportación. Para corregir estos problemas, agregue un archivo link.xml al proyecto que contiene una lista de ensamblados y espacios de nombres para que no se ejecute el proceso de extracción en ellos. Para más información, consulte la documentación de Unity sobre la eliminación de código de bytes.

Proyecto de Unity de .NET 4.x de ejemplo

La muestra contiene ejemplos de varias características de .NET 4.x. Puede descargar el proyecto o ver el código fuente en GitHub.

Recursos adicionales