Uso di .NET 4.x in Unity

C# e .NET, le tecnologie sottostanti alle funzionalità di scripting Unity, continuano a essere aggiornate sin dal rilascio originale di Microsoft nel 2002. Tuttavia, gli sviluppatori Unity potrebbero non essere a conoscenza del flusso costante di nuove funzionalità aggiunte al linguaggio C# e .NET Framework, perché prima di Unity 2017.1, Unity usava un runtime di scripting equivalente .NET 3.5, senza anni di aggiornamenti.

Con il rilascio di Unity 2017.1, Unity ha introdotto una versione sperimentale del runtime di scripting aggiornata a una versione compatibile con .NET 4.6, C# 6.0. In Unity 2018.1, il runtime equivalente di .NET 4.x non è più considerato sperimentale, mentre il runtime equivalente a .NET 3.5 precedente viene ora considerato la versione legacy. Con il rilascio di Unity 2018.3, Unity prevede di eseguire il runtime di scripting aggiornato per la selezione predefinita e di aggiornare ulteriormente a C# 7. Per altre informazioni e gli aggiornamenti più recenti su questa roadmap, leggere il post di blog di Unity o visitare il forum Delle anteprime di scripting sperimentale. Nel frattempo, consultare le sezioni seguenti per altre informazioni sulle nuove funzionalità ora disponibili con il runtime di scripting .NET 4.x.

Prerequisiti

Abilitazione del runtime di scripting .NET 4.x in Unity

Per abilitare il runtime di scripting .NET 4.x, seguire questa procedura:

  1. Aprire Player Impostazioni in Unity Inspector selezionando Modifica > progetto Impostazioni > Lettore > Altro Impostazioni.

  2. Nell'intestazione Configurazione fare clic sull'elenco a discesa Livello di compatibilità api e selezionare .NET Framework. Verrà richiesto di riavviare Unity.

Screenshot showing the Select .NET 4.x equivalent.

Scelta tra profili .NET 4.x e .NET Standard 2.1

Dopo aver passato al runtime di scripting equivalente di .NET 4.x, è possibile specificare il livello di compatibilità api usando il menu a discesa in Player Impostazioni (Modifica > progetto Impostazioni > Player). Sono disponibili due opzioni:

  • .NET Standard 2.1. Questo profilo corrisponde al profilo .NET Standard 2.1 pubblicato da .NET Foundation. Unity consiglia .NET Standard 2.1 per i nuovi progetti. Ha dimensioni minori rispetto a .NET 4.x e ciò rappresenta un vantaggio per le piattaforme con vincoli di dimensioni. Inoltre, Unity si è impegnata a supportare questo profilo in tutte le piattaforme supportate da Unity.

  • .NET Framework. Questo profilo consente di accedere all'API .NET 4 più recente. Include tutto il codice disponibile nelle librerie di classi .NET Framework e supporta anche i profili .NET Standard 2.1. Usare il profilo .NET 4.x se il progetto richiede una parte dell'API non inclusa nel profilo .NET Standard 2.0. Tuttavia, alcune parti di questa API potrebbero non essere supportate in tutte le piattaforme di Unity.

Ulteriori informazioni su queste opzioni sono disponibili nel post di blog di Unity.

Aggiunta di riferimenti ad assembly quando si usa il livello di compatibilità delle API .NET 4.x

Quando si usa l'impostazione .NET Standard 2.1 nell'elenco a discesa Livello di compatibilità api, a tutti gli assembly nel profilo API viene fatto riferimento e utilizzabile. Tuttavia, quando si usa il profilo .NET 4.x più grande, alcuni degli assembly forniti con Unity non fanno riferimento per impostazione predefinita. Per usare queste API, è necessario aggiungere manualmente un riferimento all'assembly. È possibile visualizzare gli assembly forniti da Unity nella directory MonoBleedingEdge/lib/mono dell'installazione dell'editor di Unity:

Screenshot showing the MonoBleedingEdge directory.

Ad esempio, se si usa il profilo .NET 4.x e si vuole usare HttpClient, è necessario aggiungere un riferimento all'assembly per System.Net.Http.dll. In caso contrario, il compilatore segnalerà che manca un riferimento all'assembly:

Screenshot showing the missing assembly reference.

Visual Studio rigenera i file con estensione csproj e sln per i progetti Unity ogni volta che vengono aperti. Di conseguenza, non è possibile aggiungere riferimenti ad assembly direttamente in Visual Studio perché andranno persi alla riapertura del progetto. È invece necessario usare un file di testo speciale denominato csc.rsp :

  1. Creare un nuovo file di testo denominato csc.rsp nella directory radice Assets del progetto Unity.

  2. Nella prima riga nel file di testo vuoto immettere -r:System.Net.Http.dll e quindi salvare il file. È possibile sostituire "System.Net.Http.dll" con qualsiasi assembly incluso per cui potrebbe mancare un riferimento.

  3. Riavviare l'editor di Unity.

Vantaggi della compatibilità con .NET

Oltre a una nuova sintassi C# e a nuove funzionalità per il linguaggio, il runtime di scripting .NET 4.x consente agli utenti di Unity di accedere a un'enorme raccolta di pacchetti di .NET che non sono compatibili con il runtime di scripting .NET 3.5 legacy.

Aggiungere pacchetti da NuGet a un progetto Unity

NuGet è la gestione pacchetti per .NET. NuGet è integrato in Visual Studio. Tuttavia, i progetti Unity richiedono un processo speciale per aggiungere pacchetti NuGet perché quando si apre un progetto in Unity, i relativi file di progetto di Visual Studio vengono rigenerati, annullando le configurazioni necessarie. Per aggiungere un pacchetto da NuGet, al progetto Unity:

  1. Individuare un pacchetto compatibile da aggiungere in NuGet (.NET Standard 2.0 o .NET 4.x). In questo esempio verrà illustrata l'aggiunta di Json.NET, un pacchetto diffuso per l'utilizzo di JSON, a un progetto .NET Standard 2.0.

  2. Fare clic sul pulsante Download:

    Screenshot showing the download button.

  3. Individuare il file scaricato e modificarne l'estensione da .nupkg a .zip.

  4. All'interno del file ZIP passare alla directory lib/netstandard2.0 e copiare il file Newtonsoft.Json.dll.

  5. Nella cartella Assets radice del progetto Unity creare una nuova cartella denominata Plugins. Plugins è un nome di cartella speciale in Unity. Per altre informazioni, vedere la documentazione di Unity.

  6. Incollare il file Newtonsoft.Json.dll nella directory Plugins del progetto Unity.

  7. Creare un file denominato link.xml nella directory Assets del progetto Unity e aggiungere il codice XML seguente, assicurando che il processo di rimozione del bytecode di Unity non rimuove i dati necessari durante l'esportazione in una piattaforma IL2CPP. Anche se questo passaggio è specifico per questa libreria, è possibile riscontrare problemi con altre librerie che usano la reflection in modo analogo. Per altre informazioni, vedere la documentazione di Unity su questo articolo.

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

È ora tutto pronto per usare il pacchetto 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.
    }
}

Questo è un semplice esempio di uso di una libreria, che non ha dipendenze. Quando i pacchetti NuGet si basano su altri pacchetti NuGet, è necessario scaricare manualmente tali dipendenze e aggiungerle al progetto nello stesso modo.

Nuova sintassi e nuove funzionalità del linguaggio

L'uso del runtime di scripting aggiornato consente agli sviluppatori Unity di accedere a C# 8 e a una serie di nuove funzionalità e sintassi del linguaggio.

Inizializzatori di proprietà automatiche

Nel runtime di scripting .NET 3.5 di Unity, la sintassi per le proprietà automatiche consente di definire rapidamente le proprietà non inizializzate, ma l'inizializzazione deve avvenire altrove nello script. Con il runtime .NET 4.x è ora possibile inizializzare le proprietà automatiche nella stessa riga:

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

Interpolazione di stringa

Con il runtime .NET 3.5 precedente, per la concatenazione di stringhe era prevista una sintassi complicata. Con il runtime .NET 4.x, la funzionalità di interpolazione di stringhe $ consente di inserire espressioni nelle stringhe con una sintassi più diretta e leggibile:

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

Membri con corpo di espressione

Con la più recente sintassi C# disponibile nel runtime .NET 4.x, si possono usare espressioni lambda per sostituire il corpo delle funzioni per renderle più concise:

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

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

È anche possibile usare membri con corpo di espressione nelle proprietà di sola lettura:

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

Modello asincrono basato su attività (TAP)

La programmazione asincrona consente di eseguire operazioni che richiedono tempo senza che l'applicazione smetta di rispondere. Questa funzionalità consente anche al codice di attendere il completamento di operazioni di lunga durata prima di continuare con il codice che dipende dai risultati di queste operazioni. Si può ad esempio aspettare che un file venga caricato o il completamento di un'operazione di rete.

In Unity, la programmazione asincrona si ottiene in genere con coroutine. Tuttavia, da C# 5, il metodo preferito per la programmazione asincrona per lo sviluppo .NET è diventato il modello asincrono basato su attività TAP (Task-based Asynchronous Pattern) usando le parole chiave async e await con System.Threading.Task. In breve, in una funzione async è possibile usare await per attendere il completamento di un'attività senza bloccare l'aggiornamento del resto dell'applicazione:

// 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 è un argomento complesso, con varie sfumature specifiche per Unity di cui devono tenere conto gli sviluppatori. Di conseguenza, TAP non è una sostituzione universale per le coroutine in Unity; tuttavia, è un altro strumento da usare. L'ambito di questa funzionalità esula dagli scopi di questo articolo, ma di seguito vengono forniti alcuni suggerimenti e procedure consigliate generali.

Informazioni di riferimento introduttive per TAP con Unity

Questi suggerimenti possono essere utili per iniziare a usare il modello TAP in Unity:

  • Le funzioni asincrone destinate all'attesa devono avere il tipo restituito Task oppure Task<TResult>.
  • Le funzioni asincrone che restituiscono un'attività devono avere il suffisso "Async" aggiunto ai relativi nomi. Il suffisso "Async" consente di indicare che una funzione deve sempre essere attesa.
  • Usare solo il tipo restituito async void per le funzioni che attivano funzioni asincrone dal codice sincrono tradizionale. Tali funzioni non possono essere attese e non devono avere il suffisso "Async" nei nomi.
  • Unity usa UnitySynchronizationContext per assicurarsi che le funzioni asincrone vengano eseguite sul thread principale per impostazione predefinita. L'API di Unity non è accessibile all'esterno del thread principale.
  • È possibile eseguire le attività in thread in background con metodi come Task.Run e Task.ConfigureAwait(false). Questa tecnica è utile per l'offload delle operazioni onerose dal thread principale per migliorare le prestazioni. Tuttavia, l'uso di thread in background può causare problemi di cui è difficile eseguire il debug, ad esempio situazioni di race condition.
  • L'API di Unity non è accessibile all'esterno del thread principale.
  • Le attività che usano i thread non sono supportate in build WebGL Unity.

Differenze tra coroutine e TAP

Esistono alcune importanti differenze tra le coroutine e TAP/async-await:

  • Le coroutine non possono restituire valori, ma Task<TResult> possono.
  • Non è possibile inserire un'istruzione yield try-catch, rendendo difficile la gestione degli errori con le coroutine. Try-catch, tuttavia, funziona con TAP.
  • La funzionalità delle coroutine di Unity non è disponibile nelle classi non derivate da MonoBehaviour. TAP è un'ottima soluzione per la programmazione asincrona in tali classi.
  • Attualmente, Unity non consiglia TAP come sostituzione generale per le coroutine. La profilatura è l'unico modo per conoscere i risultati specifici di un approccio rispetto all'altro per ogni progetto.

Operatore nameof

L'operatore nameof ottiene il nome di stringa di una variabile, un tipo o un membro. Alcuni casi in cui nameof risulta utile sono la registrazione degli errori e il recupero del nome della stringa di un enum:

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

Attributi informativi sul chiamante

Gli attributi informativi sul chiamante forniscono informazioni relative al chiamante di un metodo. È necessario specificare un valore predefinito per ogni parametro da usare con un attributo informativo sul chiamante:

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

Direttiva using static

La direttiva using static consente di usare funzioni statiche senza digitare il nome della classe. Con using static è possibile risparmiare spazio e tempo se è necessario usare diverse funzioni statiche dalla stessa classe:

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

Considerazioni su IL2CPP

Quando si esporta il gioco in piattaforme come iOS, Unity userà il motore IL2CPP per "transpile" il codice in C++ che viene quindi compilato usando il compilatore nativo della piattaforma di destinazione. In questo scenario sono disponibili diverse funzionalità .NET non supportate, ad esempio parti di Reflection e l'utilizzo della dynamic parola chiave . Anche se è possibile controllare l'uso di queste funzionalità nel proprio codice, è possibile che si verifichino problemi usando DLL e SDK di terze parti che non sono stati scritti con Unity e IL2CPP in mente. Per altre informazioni su questo articolo, vedere la documentazione sulle restrizioni di scripting nel sito di Unity.

Inoltre, come accennato nell'esempio Json.NET precedente, Unity proverà a rimuovere il codice inutilizzato durante il processo di esportazione IL2CPP. Anche se questo processo in genere non è un problema, con le librerie che usano Reflection, può rimuovere accidentalmente proprietà o metodi che verranno chiamati in fase di esecuzione che non possono essere determinati in fase di esportazione. Per risolvere questi problemi, aggiungere un file link.xml al progetto contenente un elenco di assembly e spazi dei nomi per non eseguire il processo di rimozione. Per altre informazioni, vedere la documentazione di Unity sulla rimozione del bytecode.

Progetto Unity di esempio di .NET 4.x

L'esempio contiene esempi di diverse funzionalità di .NET 4.x. È possibile scaricare il progetto o visualizzare il codice sorgente in GitHub.

Risorse aggiuntive