HTML5 Game Development–High Performance o início

Game Development & HTML5 & JavaScript

Falando em games, existe um elemento que provavelmente seja mais importante que uma boa ideia e gráficos extraordinários: a fluidez e continuidade. Não tem nada pior que um jogo cheio de pausas e lags. Estudos recentes de usabilidade mostram um grande nível de frustração dos usuários em jogos onde há delay aparente. Mesmo um jogo realista e com uma ótima arte vai ser tornar chato e desinteressante se for lento e sem "fluidez".

Levando isso em consideração é importante ter em mente que mesmo aquela super ideia, arte ou conceito pode se perder se não for bem  implementada.

Dito isto, é fundamental para o desenvolvimento de games que você se preocupe com a performance, e já que estamos falando de games em HTML5 e JavaScript, precisamos entender como o JavaScript funciona, o que gera a falta de performance e as possíveis técnicas a serem utilizadas a fim de garantir games mais performáticos e com ótima fluidez.

capa

O que você consegue fazer em 16 milissegundos? Quando falamos de games essa pode ser a diferença entre o sucesso e fracasso. Nesta série sobre High Performance vou abordar alguns dos assuntos fundamentais para construção de games com HTML5 & JavaScript. Como este é um assunto longo, vou dividir os temas para abordar com mais detalhes e testes práticos referentes a performance.

Agenda deste artigo:

  • FPS
  • Gestão de memória em JavaScript
  • Garbage Collected (Coleta de lixo)
  • Memory Leak (Vazamento de memória)

 

FPS & Frame Rate (taxa de quadros)

Vale lembrar que animações são feitas quadro a quadro, ou seja, uma série de imagens que são passadas em uma determinada velocidade a fim de gerar a impressão de movimento, graças a ilusão de óptica conhecida como Fenômeno Phi. Enfim, o interessante é ficar claro que animações são feitas quadro a quadro.

animacao

Uma boa jogabilidade afirma que o game deve ser capaz de proporcionar ao jogador, ver as animações e conseguir reagir adequadamente.

A grosso modo FPS é a quantidade de vezes em que a tela é desenhada em um segundo. O usual para um jogo é que esta taxa esteja entre 30 e 60 FPS. Isso significa que se seu game estiver rodando a 60 FPS você terá apenas 16ms para realizar todo a lógica e atualizar a tela.

O importante aqui é criar seu jogo dentro de uma taxa de quadros constante a fim de gerar esta experiência contínua ao usuário. Até porque sua expectativa é obter as repostas dos comandos de forma imediata, em “tempo real”.  Logo, o objetivo é manter o frame rate sempre constante.

Quando falamos de performance, estamos nos preocupando com a perda de quadros(frames) que ocorre quando temos lags, pausas indevidas e lentidão na execução do jogo.

VEJA ESTE EXEMPLO NO JSFIDDLE.

O exemplo acima deixa claro a diferença entre animações iguais exibidas em taxas de 15, 30 e 60 FPS. Perceba que, no primeiro, o movimento existe, mas é pouco fluido e parece deixar rastros. Já a animação no segundo quadro é bem melhor, com uma boa fluidez. O último roda com uma taxa de 60 FPS e, praticamente, não deixa rastros no caminho da figura.

E o que pode causar essas percas de quadro??? Os próximos tópicos vão nos ajudar a elucidar esta questão…

 

Gestão de memória no JavaScript

Em JavaScript tudo é objeto. Logo, são os objetos que ocupam memória. Quando um objeto é criado, o JavaScript automaticamente aloca uma quantidade adequada de memória para ele. A partir desse ponto existe um mecanismo responsável por continuamente avaliar se o objeto é válido, e quando não for mais é descartado e seu espaço de memória fica disponível para ser reutilizado.

Um objeto é considerado válido enquanto houver uma referência a ele (vou explicar isso em detalhes). A partir do momento em que a memória não for mais referenciada fica candidata a ser reaproveitada.

Considere o jogo abaixo:

manyobjects

Cada elemento é um objeto que vai efetivamente ocupar espaço de memória. Considere ainda que seu game vai rodar em vários tipos de dispositivos como mobile.

A gestão de memória é um ponto sensível quando falamos de games com JavaScript porque afetam diretamente a execução do jogo. Como já falado existe um mecanismo próprio para realizar esta gestão, o famoso Garbage Collected.

 

Garbage Collected

O problema mais comum de desempenho é referente a alocação de memória. Isso ocorre em muito dado a falta de preocupação dos desenvolvedores sobre este tema.

heart-polish

JavaScript é um linguagem com Garbage Collected (coleta de lixo). Em ciência da computação, coleta de lixo é uma forma de gerenciamento automático de memória.

É justamente por isso que muitos programadores acabam tendo a falsa impressão que não é preciso se preocupar com a memória, é só ir criando os objetos conforme a necessidade e outorgar ao GC toda responsabilidade.

O fato é que o GC é muito mais importante do que imaginamos. Ele é praticamente o coração do seu game/aplicação. Efetivamente o GC é responsável por:

  • Alocar memória para novos objetos
  • Identificar os objetos que devem ser coletados (descartados)
  • Recuperar a memória dos objetos coletados

A vantagem

Libera o programador de ter que lidar com a gestão de memória. Linguagens de baixo nível como C/C++ tem primitivas como malloc() e free() para a gestão de memória. Em JavaScript não precisamos alocar e desalocar memória diretamente.

E tem Desvantagem?

A primeira coisa a se falar sobre a coleta de lixo é que ela consome recursos computacionais quando acionada. O mais interessante é que o GC é acionado pelo JavaScript Runtime que decide quando o mesmo deve fazer a coleta. Neste momento sua execução é prioritária então seu processamento é interrompido durante o tempo necessário para completar a coleta.

Não há como forçar a coleta de lixo. A coleta pode ocorrer em qualquer momento da execução do seu código.

 

Então a culpa é do GC?

Muitos jogos em HTML5 & Javascript, tem como grande obstáculo para uma experiência contínua e suave as famosas pausas geradas pela coleta de lixo (GC). Esse processo pode ser demorado especialmente em um celular, o que pode gerar a perda de frames.

Imagine que seu jogo está rodando a 60 FPS, o que te dá 16 milissegundos para toda a lógica e renderização de um quadro. Mesmo usando uma taxa menor como 30 FPS, você terá 32ms para fazer tudo. Dependendo do navegador e do número de objetos que você esteja usando, o processo de coleta de lixo pode demorar de 20ms a 200ms. Quanto maior a limpeza maior será o tempo, ou seja, o GC vai gastar mais tempo na coleta do que o disponível para um frame, o que resulta em uma pausa visível, ou em situações ainda piores, uma experiência de jogo constantemente falha.

Respondendo a pergunta: NÃO!!! A culpa não é só do GC… Vamos ver que para games mais performáticos o ideal é você mesmo “cuidar” do gerenciamento de memória para evitar pagar um imposto muito alto com o GC.

 

JavaScript internals

Em uma linguagem com coleta de lixo, todo objeto que não for referenciado será coletado. Sendo assim enquanto o objeto estiver referenciado o JavaScript runtime vai deixa-lo intocado.

Em JavaScript o gerenciamento de memória é um conceito de acessibilidade.

Levando em consideração o que já falamos sobre o escopo, se definirmos objetos no escopo global, enquanto deixarmos o browser aberto estes objetos estão referenciados. Objetos do escopo global só são destruídos quando se atualiza ou fecha o browser. Observere a imagem abaixo:

memory organization

Ela representa o esquema da memória baseada no encadeamento dos objetos. A cadeia de referência dos objetos se inicia com o Window Object que é o escopo global. O GC vai iniciar dai a busca de todos os objetos que não estiverem nessa raiz, como é o caso dos objetos 10 e 11.

Mesmo não podendo forçar a coleta de lixo, você pode forçar uma variável a ser coletada quando o GC for executado. É só excluir as referências do mesmo. Abaixo segue a representação de uma coleta. A figura abaixo representa uma cadeia de memória.

LCM1

Para que um objeto seja candidato a coleta de lixo é necessário não possuir referências com o Root Node. Na figura abaixo estamos destruindo a referência de um objeto root.

LCM2

Com isso, todos os objetos que estiverem abaixo na herança serão também candidatos a coleta. No exemplo abaixo ao destruir a referência do objeto pai, os filhos se tornam candidatos com exceção do último objeto a direita que tem uma referência direta ao root.

LCM3

Após a coleta de lixo os objetos restantes serão apenas os que tinham referência ao root. Neste momento teremos a liberação da memória.

LCM4

Vazamento de memória

Mesmo assim ainda é possível termos problemas em relação a gestão de memória. Estes são conhecidos como vazamento de memória. Vazamento de memória ocorre quando uma porção de memória, alocada para uma determinada operação, não é liberada mesmo não sendo mais necessária.

Existem muitas maneiras em que você pode reter objetos indesejados na memória. É bem verdade que os browsers tem investido e melhorado no tratamento de coleta de lixo, porém este ainda vai ser um ponto em que vamos ter de nos preocupar quando falamos de games e desempenho.

 

Esta foi a primeira parte do artigo, apenas com a parte “teórica” a fim de equalizar o conhecimento. Nas próximas partes vou traduzir todo este conteúdo em código e apresentar algumas técnicas para evitar os vazamentos de memória bem como diminuir o trabalho do GC, focando na programação defensiva em relação a  performance.

 

Bons estudos e até a próxima pessoal  ;)


Author's profile picture

Vitor is a computer scientist who is passionate about creating software that will positively change the world we live in.

MVP Azure - Cloud Architect - Data science enthusiast


9 minutes to read