Dicas para teste técnico com Unity #2

Dicas para teste técnico com Unity #2

Este é o segundo post da série com dicas para teste técnico utilizando Unity e C#. No primeiro post, o foco foi como organizar o projeto para facilitar seu desenvolvimento, e como enviar após o término. Agora, vamos falar um pouco de código, cobrindo um dos erros comuns que vejo em testes técnicos, especialmente relacionado a API da Unity.

Nesta série de posts sobre dicas para teste técnico, o assunto código vai ser bem recorrente, mas não vamos deixar de lado tópicos relacionados a configuração da engine, e features que podemos usar sem necessariamente escrever código pra isso. A Unity possui diversas ferramentas, e meu objetivo aqui vai ser sempre ajudar a evitar alguns erros comuns e mostrar o que é interessante demonstrar em um teste técnico nesta engine.

Uso do GetComponent na Unity

Meu objetivo não é falar para usar ou não usar uma determinada chamada da API da Unity, mas sim mostrar o lugar ideal para colocá-la quando utilizada. Vamos começar por uma das mais comuns: GetComponent, utilizado para buscar um componente, de um tipo específico, no gameObject. O GetComponent possui algumas variações, como: GetComponents, para retornar uma lista de um tipo; o GetComponentInParent, que busca no pai do gameObject; e o GetComponentInChildren, que busca no filho do gameObject. Estes dois últimos também possuem uma variação para retornar uma lista do tipo desejado.

Vamos para alguns exemplos e, nos códigos a seguir, imagine que o script está em um gameObject na scene. No código abaixo, você consegue identificar qual é o problema?

public class MyGame : MonoBehaviour
{
  private void Awake()
  {
   GetComponent<SpriteRenderer>().enabled = false;
  }
}

Neste exemplo, estamos buscando o componente SpriteRenderer e assumindo que ele existe para, logo em seguida, mudar o valor de uma propriedade sua. Se o componente não existir, vamos gerar um NullReferenceException logo no Awake() do MonoBehaviour. Vamos modificar um pouco o código para corrigir isso.

public class MyGame : MonoBehaviour
{
  private void Awake()
  {
    if(GetComponent<SpriteRenderer>() != null) 
    {
      GetComponent<SpriteRenderer>().enabled = false;
    }
  }
}

Parece mais seguro agora, não? Evitamos o uso do NullReferenceException, mas ainda tem algo que não é o ideal ali. Estamos usando duas vezes a chamada do GetComponent(), podemos reduzir para uma chamada com a modificação abaixo.

public class MyGame : MonoBehaviour
{
  private void Awake()
  {
    var spriteRenderer = GetComponent<SpriteRenderer>();
    if(spriteRenderer != null) 
    {
      spriteRenderer.enabled = false;
    }
  }
}

Podemos, também, garantir que o objeto vai ter um determinado script anexado usando o RequireComponent, assim não precisamos verificar se o componente existe.

[RequireComponent(typeof(SpriteRenderer))]
public class MyGame : MonoBehaviour
{
  private void Awake()
  {
    var spriteRenderer = GetComponent<SpriteRenderer>();
    spriteRenderer.enabled = false;
  }
}

Voltando um pouco, vamos falar agora o que o GetComponent faz por trás da chamada. O código C# da Unity está no seu GitHub para ser usado como referência, assim fica fácil de ver o que alguma determinada API faz. Não é o código inteiro da engine, apenas a parte C#, mas já ajuda muito a aprender o que cada API faz. No caso, o GetComponent faz isso:

public unsafe T GetComponent<T>()
{
  var h = new CastHelper<T>();
  GetComponentFastPath(typeof(T), new System.IntPtr(&h.onePointerFurtherThanT));
  return h.t;
}

Não precisa ser muito experiente em C# para ver que existem duas alocações ali, um CastHelper e um System.IntPtr, além do que o GetComponentFastPath() deve executar no código C++ da Unity (essa parte não está disponível para consulta). Não é muita coisa, e não é algo que vai fazer muita diferença de performance, mas podemos evitar o uso do GetComponent em alguns casos, como o exemplo abaixo, onde a variável é recebida pelo Inspector, e podemos até usar o GetComponent caso ela não seja referenciada no gameObject.

public class MyGame : MonoBehaviour
{
  [SerializeField]
  private SpriteRenderer _spriteRenderer = null;
  private void Awake()
  {
    if(_spriteRenderer == null)
    {
      _spriteRenderer = GetComponent<SpriteRenderer>();
    }
    _spriteRenderer.enabled = false;
  }
}

A modificação acima não vai fazer muita diferença, o GetComponent é rápido o suficiente pra não causar problemas de performance. Porém, com base em tudo que escrevi neste post, o código abaixo não é recomendado. É um caso exagerado onde queremos garantir que o SpriteRenderer esteja desabilitado por qualquer motivo, usando um Update() para a validação.

public class MyGame : MonoBehaviour
{
  private void Update()
  {
    if(!GetComponent<SpriteRenderer>().enable)
    {
      GetComponent<SpriteRenderer>().enable = true;
    }
  }
}

O código acima, em um projeto médio/grande, pode causar alguns problemas de performance, especialmente se esse padrão estiver espalhado pelo projeto e a plataforma algo for Android ou iOS. Aparelhos mais antigos podem ter uma redução no FPS ou apresentar algumas travadas rápidas em transições. Então, sempre que possível, faça cache do GetComponent.

O post ficou um pouco longo, mas espero que eu tenha conseguido mostrar algumas maneiras de usar o GetComponent e, principalmente, recomendações do que deve ser evitado. Tudo isso é analisado em um teste técnico, independente do tamanho do projeto, pois ajuda a entender como o programador pensar e o quanto conhece da Unity, a game engine em questão. Deixei passar algo no post? Usa o GetComponent de alguma mandeira diferente? Comenta abaixo, sempre temos muito o que aprender, por isso compartilhar sempre acrescenta.

Bruno Cicanci

Bruno Cicanci
Desenvolvendo jogos desde 2009.

Abragames oferece Bolsa da Diversidade para a GDC 2020

A Associação Brasileira das Desenvolvedoras de Jogos Eletrônicos (Abragames) está oferecendo 10 bolsas para participar da GDC 2020, o mai...… Continue lendo