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.