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.

Postmortem de 2019

Apesar de ter postado pouco no blog, este ano participei de muitos eventos e palestras sobre desenvolvimento de jogos. Aprendi bastante s...… Continue lendo