Usar o .NET 4.x no Unity

C# e .NET, as tecnologias subjacentes ao script do Unity, continuam a receber atualizações, uma vez que a Microsoft as lançou originalmente em 2002. Mas os desenvolvedores do Unity podem não saber do fluxo constante de novos recursos adicionados à linguagem C# e .NET Framework, pois antes do Unity 2017.1, o Unity estava usando um runtime de script equivalente ao .NET 3.5, faltando anos de atualizações.

Com o lançamento do Unity 2017.1, o Unity introduziu uma versão experimental do seu runtime de script atualizado para uma versão compatível com o .NET 4.6 e o C# 6.0. No Unity 2018.1, o runtime equivalente ao .NET 4.x não é mais considerado experimental, enquanto o runtime equivalente ao .NET 3.5 mais antigo agora é considerado como a versão herdada. E com o lançamento do Unity 2018.3, o Unity quer tornar o runtime de script atualizado na seleção padrão, além de atualizar mais ainda para o C# 7. Para obter mais informações e as atualizações mais recentes sobre este roteiro, leia a postagem no blog do Unity ou acesse seu Fórum de visualizações de script experimental. Enquanto isso, confira as seções abaixo para saber mais sobre os novos recursos disponíveis com o runtime de script do .NET 4.x.

Pré-requisitos

Habilitar o runtime de script do .NET 4.x no Unity

Para habilitar o runtime de script do .NET 4.x, execute as seguintes etapas:

  1. Abra PlayerSettings no Inspetor do Unity, selecionando Editar > Configurações do projeto > Player > Outras configurações.

  2. No título Configuração, clique na lista suspensa Nível de Compatibilidade da API e selecione .NET Framework. Você deve reiniciar o Unity.

Screenshot showing the Select .NET 4.x equivalent.

Escolher entre perfis do .NET 4.x e do .NET Standard 2.1

Depois de mudar para o runtime de script equivalente ao .NET 4.x, você pode especificar o Nível de compatibilidade de API usando o menu suspenso no PlayerSettings (Editar > Configurações do projeto > Player). Há duas opções:

  • .NET Standard 2.1 Esse perfil corresponde ao perfil do .NET Standard 2.1 publicado pela .NET Foundation. O Unity recomenda o .NET Standard 2.1 para novos projetos. Ele é menor do que o .NET 4.x, o que é vantajoso para plataformas com restrições de tamanho. Além disso, o Unity se comprometeu a dar suporte a esse perfil em todas as plataformas compatíveis com o Unity.

  • .NET Framework Esse perfil fornece acesso à mais recente API do .NET 4. Ele inclui todos os códigos disponíveis nas bibliotecas de classes do .NET Framework e também dá suporte a perfis do .NET Standard 2.1. Se seu projeto requer uma parte da API não incluída no perfil do .NET Standard 2.0, use o perfil do .NET 4.x. No entanto, algumas partes dessa API podem não ser compatíveis com todas as plataformas do Unity.

Você pode ler mais sobre essas opções na postagem de blog do Unity.

Adicionar referências de assembly ao usar o nível de compatibilidade de API do .NET 4.x

Ao usar a configuração do .NET Standard 2.1 no menu suspenso Nível de compatibilidade de API, todos os assemblies no perfil de API podem ser mencionados e utilizados. No entanto, ao usar o perfil maior do .NET 4.x, alguns dos assemblies que acompanham o Unity não são referenciados por padrão. Para usar essas APIs, você deve adicionar manualmente uma referência de assembly. Você pode exibir os assemblies com os quais o Unity é fornecido no diretório MonoBleedingEdge/lib/mono da instalação do editor do Unity:

Screenshot showing the MonoBleedingEdge directory.

Por exemplo, se estiver usando o perfil do .NET 4.x e desejar usar HttpClient, você deverá adicionar uma referência de assembly para System.Net.Http.dll. Sem ele, o compilador reclamará que uma referência de assembly está ausente:

Screenshot showing the missing assembly reference.

O Visual Studio regenera os arquivos .csproj e .sln para projetos do Unity cada vez que são abertos. Assim, não será possível adicionar referências de assembly diretamente no Visual Studio, porque elas serão perdidas ao reabrir o projeto. Em vez disso, é preciso usar um arquivo de texto especial denominado csc.rsp:

  1. Crie um novo arquivo de texto chamado csc.rsp no diretório Ativos da raiz do projeto do Unity.

  2. Na primeira linha no arquivo de texto vazio, digite: -r:System.Net.Http.dll e, em seguida, salve o arquivo. Você pode substituir "System.Net.Http.dll" com qualquer assembly incluído no qual uma referência pode estar ausente.

  3. Reinicie o editor do Unity.

Aproveitando a compatibilidade do .NET

Além dos novos recursos de linguagem de programação e de sintaxe do C#, o runtime de script do .NET 4.x oferece aos usuários do Unity acesso a uma enorme biblioteca de pacotes do .NET que são incompatíveis com o runtime de script do .NET 3.5 herdado.

Adicionar pacotes do NuGet a um projeto do Unity

O NuGet é o gerenciador de pacotes para o .NET. O NuGet é integrado ao Visual Studio. No entanto, os projetos do Unity exigem um processo especial para adicionar pacotes NuGet porque quando você abre um projeto no Unity, seus arquivos de projeto do Visual Studio são regenerados, desfazendo as configurações necessárias. Para adicionar um pacote NuGet ao seu projeto Unity:

  1. Procure no NuGet para localizar um pacote compatível que você deseja adicionar (.NET Standard 2.0 ou .NET 4.x). Este exemplo demonstra a adição do Json.NET, um pacote popular para trabalhar com JSON, a um projeto do .NET Standard 2.0.

  2. Clique no botão Download:

    Screenshot showing the download button.

  3. Localize o arquivo baixado e altere a extensão dele de .nupkg para .zip.

  4. Dentro do arquivo zip, navegue até o diretório lib/netstandard2.0 e copie o arquivo Newtonsoft.Json.dl.

  5. Na pasta Ativos na raiz do projeto do Unity, crie uma nova pasta chamada Plugins. Plugins é um nome de pasta especial no Unity. Para obter mais informações, confira a documentação do Unity.

  6. Cole o arquivo Newtonsoft.Json.dll no diretório Plugins do projeto do Unity.

  7. Crie um arquivo chamado link.xml no diretório Ativos do projeto do Unity e adicione o XML a seguir, garantindo que o processo de remoção de código de bytes do Unity não remova os dados necessários ao exportar para uma plataforma IL2CPP. Embora esta etapa seja específica para essa biblioteca, você poderá encontrar problemas com outras bibliotecas que usam reflexão de maneiras semelhantes. Para obter mais informações, consulte a documentação do Unity nesse artigo.

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

Com tudo pronto, agora você pode usar o pacote 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.
    }
}

Esse é um exemplo simples do uso de uma biblioteca sem dependências. Quando os pacotes do NuGet dependem de outros pacotes do NuGet, você precisa baixar essas dependências manualmente e adicioná-las ao projeto da mesma maneira.

Novos recursos de linguagem e de sintaxe

Usar o runtime de script atualizado oferece aos desenvolvedores do Unity acesso a C# 8 e a uma série de novos recursos de linguagem e de sintaxe.

Inicializadores de propriedade automática

No runtime de script do Unity .NET 3.5, a sintaxe de propriedade automática torna mais fácil definir rapidamente as propriedades não inicializadas, mas a inicialização precisa ocorrer em outro lugar no seu script. Agora, com o runtime .NET 4.x, é possível inicializar as propriedades automáticas na mesma linha:

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

Interpolação de cadeia de caracteres

Com o runtime mais antigo do .NET 3.5, a concatenação de cadeia de caracteres exigia uma sintaxe estranha. Agora, com o runtime .NET 4.x, o recurso de interpolação de cadeia de caracteres de $ permite que as expressões sejam inseridas em cadeias de caracteres em uma sintaxe mais legível e direta:

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

Membros aptos para expressão

Com a mais nova sintaxe C# disponível no runtime do .NET 4.x, expressões lambda podem substituir o corpo de funções para torná-las mais sucintas:

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

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

Você também pode usar membros aptos para expressão em propriedades somente leitura:

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

Padrão assíncrono baseado em tarefa (TAP)

A programação assíncrona permite que operações demoradas sejam realizadas sem que isso faça com que o aplicativo pare de responder. Essa funcionalidade também permite que seu código aguarde a conclusão de operações demoradas antes de continuar com código que dependa dos resultados dessas operações. Por exemplo, você poderia esperar o carregamento de um arquivo ou a conclusão de uma operação de rede.

No Unity, a programação assíncrona normalmente é feita com corrotinas. No entanto, desde o C# 5, o método preferencial de programação assíncrona no desenvolvimento do .NET é o TAP (Padrão Assíncrono Baseado em Tarefa) usando o async e palavras-chave await com System.Threading.Task. Em resumo, em uma função async, você pode await a conclusão de uma tarefa sem bloquear a atualização do resto do seu aplicativo:

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

O TAP é um assunto complexo, com nuances específicas do Unity que os desenvolvedores devem considerar. Como resultado, o TAP não é uma substituição universal para corrotinas no Unity; no entanto, é outra ferramenta para uso. O escopo desse recurso está além do abordado por este artigo, mas algumas dicas e práticas recomendadas gerais são fornecidas abaixo.

Referência de introdução ao TAP com o Unity

Estas dicas podem ajudar você a começar a usar o TAP no Unity:

  • Funções assíncronas destinadas a serem aguardadas devem ter o tipo de retorno Task ou Task<TResult>.
  • Funções assíncronas que retornam uma tarefa devem ter o sufixo "Async" acrescentado aos seus nomes. O sufixo "Async" ajuda a indicar que uma função deve sempre ser aguardada.
  • Use somente o tipo de retorno async void para funções que disparam funções assíncronas do código síncrono tradicional. Essas funções em si não podem ser esperadas e não devem ter o sufixo "Async" em seus nomes.
  • O Unity usa o UnitySynchronizationContext para garantir que funções assíncronas sejam executadas no thread principal por padrão. A API do Unity não está acessível fora do thread principal.
  • É possível executar tarefas em threads em segundo plano com métodos como Task.Run e Task.ConfigureAwait(false). Essa técnica é útil para o descarregamento de operações onerosas do thread principal para aprimorar o desempenho. No entanto, o uso de threads em segundo plano pode levar a problemas difíceis de depurar, tais como condições de corrida.
  • A API do Unity não está acessível fora do thread principal.
  • Tarefas que usam threads não são compatíveis com builds WebGL do Unity.

Diferenças entre corrotinas e TAP

Há algumas diferenças importantes entre corrotinas e TAP/async-await:

  • As corrotinas não podem retornar valores, mas Task<TResult> pode.
  • Não é possível colocar um yield em uma instrução try-catch, tornando o tratamento de erro difícil com as corrotinas. No entanto, o try-catch funciona com o TAP.
  • O recurso de co-rotina do Unity não está disponível nas classes que não derivam de MonoBehaviour. TAP é ótimo para programação assíncrona nessas classes.
  • Atualmente, o Unity não sugere TAP como uma substituição completa de corrotinas. A criação de perfil é a única maneira de saber os resultados específicos de uma abordagem em relação à outra para qualquer projeto.

operador nameof

O operador nameof obtém o nome de cadeia de caracteres de uma variável, tipo ou membro. Alguns casos em que nameof é útil são o registro de erros e a obtenção do nome da cadeia de caracteres de uma enumeração:

// 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 informações do chamador

Os atributos de informações do chamador fornecem informações sobre o chamador de um método. Você deve fornecer um valor padrão para cada parâmetro que você deseja usar com um atributo de informações do chamador:

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 permite que você use funções estáticas sem digitar seu nome de classe. Com o using static, você poderá economizar espaço e tempo se precisar usar várias funções estáticas da mesma 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
    }
}

Considerações de IL2CPP

Ao exportar seu jogo para plataformas como iOS, o Unity usará seu mecanismo IL2CPP para "transcompilar" o IL para código C++ que é então compilado usando o compilador nativo da plataforma de destino. Nesse cenário, há vários recursos do .NET que não são compatíveis, tais como partes de reflexão e o uso da palavra-chave dynamic. Embora você possa controlar o uso desses recursos em seu próprio código, é possível encontrar problemas ao usar DLLs de terceiros e SDKs que não foram escritos com o Unity e o IL2CPP em mente. Para obter mais informações sobre esse tópico, veja a documentação sobre Restrições de script no site do Unity.

Além disso, conforme mencionado no exemplo de Json.NET acima, o Unity tentará retirar o código não utilizado durante o processo de exportação de IL2CPP. Embora esse processo normalmente não seja um problema, com bibliotecas que usam a Reflexão, ele pode acidentalmente retirar propriedades ou métodos que são chamados em runtime e que não podem ser determinados em tempo de exportação. Para corrigir esses problemas, adicione um arquivo link.xml ao seu projeto que contém uma lista de assemblies e namespaces nos quais não executar o processo de remoção. Para obter mais informações, consulte a documentação do Unity sobre a remoção de código de bytes.

Projeto do Unity de exemplo do .NET 4.x

A amostra contém exemplos de vários recursos do .NET 4.x. Você pode baixar o projeto ou exibir o código-fonte no GitHub.

Recursos adicionais