Front End

27 out, 2011

Jogos em HTML5 para multiplayers com Node.js

Publicidade

Um dia, eu
recebi alguns amigos em casa, e eles me mostraram alguns jogos legais para
iPad. Um deles era o Osmos, desenvolvido por um estúdio indie do Canadá,
chamado Hemisphere Games. Você controla uma pequena bolha
que flutua em um espaço 2D, e a única coisa que ela consegue fazer é
atirar pedaços de si mesma em uma certa direção, que se propagam na direção oposta.
As regras do jogo são simples, e a principal delas diz que quando duas bolhas
colidem, a maior delas irá consumir a menor. As outras regras seguem
diretamente a conservação de massa e energia. Veja por você mesmo – é muito melhor do que parece!

O Osmos
realmente chamou minha atenção devido à sua jogabilidade simples – porém interessante, seu ritmo meditativo e a clara falta de suporte para multiplayers, o
que me pareceu um problema potencialmente interessante para abordar.
Assim, osMUs (mu significando multiplayer) nasceu
como um clone do Osmos, feito para múltiplos jogadores rodando no navegador.

Como ele funciona

Quando um
navegador se direciona para a página do Osmus, o servidor envia para o
novo cliente o estado atual do seu universo, que é composto por bolhas com
velocidades aleatórias. Nesse ponto, o cliente pode assistir ao progresso do jogo
passivamente, mas também pode, é claro, se tornar um jogador com uma bolha.
Quando o jogador se inscreve, ele pode clicar ou tocar (em dispositivos móveis) o canvas para criar uma nova bolha.

Na medida
em que o jogo progride, o servidor decide quando alguém (possivelmente uma das bolhas autônomas) é vitorioso, e então, os jogadores são notificados e o
jogo recomeça.

A outra
parte deste artigo é sobre os detalhes relacionados ao desenvolvimento. Portanto,
se você quiser apenas testar o jogo, vá em frente. Note, no entanto, que o Osmus funciona
no Chrome Stable (version 13) e no iPad.

Arquitetura do jogo

Eu
escrevi o Osmus para ser dividido em componentes agrupados de forma solta e
distinta, para deixar a base de código mais acessível para outros
contribuidores, e para facilitar a experimentação com tecnologias
intercambiáveis. 

O Osmus utiliza
um game engine compartilhado que roda tanto no navegador, quanto no
servidor. O engine é uma máquina de estado simples (simple machine state), cuja função primária é
computar o próximo estado do jogo em função de tempo, usando as regras definidas da
física.

Game.prototype.computeState = function(delta) {
var newState = {};
// Compute a bunch of stuff based on this.state
return newState;
}

Essa é
uma definição bastante restrita de um Game Engine. No mundo do
desenvolvimento de jogos, que é o que se entende, basicamente, quando se fala de game engine, inclui qualquer coisa, desde um renderizador, aparelho de som, camada de
networking etc. Nesse caso, eu fiz divisões bem
claras entre esses componentes, e o núcleo do jogo Osmus somente inclui o
estado físico da máquina, de modo que tanto o cliente quanto o servidor podem
computar os próximos estágios e estar razoavelmente sincronizados em relação ao tempo.

O cliente
é composto de três principais componentes: um renderizador, um gerente de
inputs e um de som. Eu criei um renderizador bastante simples, baseado em canvas,
que desenha as bolhas como círculos vermelhos, e os jogadores como
círculos verdes. Meu colega Arne Roomann-Kurrik escreveu um renderizador baseado
em three.js alternativo, com algumas sombras e sombreados épicos.

O gerente
de som lida com o playback em duas frentes: efeitos de som e música de fundo (retirados de 8-bit Magic). A implementação atual usa tags
de áudio, com dois elementos <audio>, um para o canal de música de fundo,
e outro para o de efeitos de som. Existem limitações conhecidas dessa abordagem, mas
dada a modularidade da minha implementação, a implementação de som pode ser
trocada por uma que usa a Web Audio API do Chrome, por exemplo.

Finalmente,
o gerente de input lida com eventos do mouse, mas pode ser substituído por um
que usa o toque, para uma versão móvel. E é nesse contexto que, provavelmente, fará
mais sentido usar transformações CSS3 em vez de canvas, uma vez que o CSS3 é
um hardware acelerado em iOS, enquanto o canvas de HTML5 ainda não é, e o WebGL
não está implementado.

Falando
em dispositivos móveis, eu fiquei surpreso ao ver que o Osmus funciona
muito bem no iPad, especialmente no iPad 2, rodando a última versão do iOS. Isso
é realmente ótimo, e um dos benefícios tangentes de escrever jogos para a web
aberta.

Networking é difícil

De uma
perspectiva de networking, um jogo é um projeto ambicioso que requer uma
sincronização perfeita em tempo real entre clientes. Por causa disso, a
comunicação bi-direcional cliente-servidor é essencial. Com os novos recursos da web, Web Sockets é que são responsáveis por gerenciar
uma fina camada acima do TCP e esconder todos os detalhes sórdidos para quem
deseja implementar tal comunicação. Para abstrair ainda mais, eu utilizo a
biblioteca socket.io, que me dá uma abstração simples baseada em eventos para
toda a parte de comunicação. Infelizmente, não há suporte para dados binários, que seria
bem interessante para compactar o tamanho dos dados transmitidos (talvez pela
metade por curiosidade).

A partir
de um pouco de pesquisa, que inclui esta ótima palestra de Rob Hawkes, tornou-se claro que para ter
qualquer tipo de experiência compartilhada, o modelo mais simples é ter o
estado verdadeiro do jogo no servidor, e ter clientes sincronizando com ele
periodicamente. A principal troca aqui é a qualidade de sincronização versus o
tráfego de rede necessário.  

Em um
extremo, um jogo pode ser escrito tendo toda sua lógica no servidor e enviando
atualizações (ou até simplesmente
screenshots
) para o
cliente a 60 FPS, mas isso geralmente não é possível, devido à quantidade de sheer de banda requerida para este modelo. No outro extremo, você pode imaginar
uma arquitetura de rede nas quais os clientes se conectam, conseguem o estado inicial,
e então, são, na maior parte, autônomos.

Na
prática, existe um meio feliz no qual muitos jogos com multiplayer caem, o que
significa replicar o código não-trivial no cliente e no servidor. Por sorte, nós estamos em um momento em que o Javascript está presente em todos
os cantos, então, não é necessário duplicar funcionalidade. Ao invés disso,
pode-se compartilhar o código JavaScript do game engine tanto no lado do servidor
com node.js, quanto no navegador do cliente.

Existe
muito mais a se falar sobre escrever os bits de multiplayer para Osmus, o que
esperançosamente irá ser tornar um artigo mais detalhado em futuro próximo.

Módulos JS compartilhados

Como
mencionado antes, o Osmus usa um physics engine que é compartilhado entre
clientes e servidor. Você pode imaginar que compartilhar o código JavaScript
entre os dois pode ser super fácil, mas não é tão simples.

Os
carregadores de módulos são uma bagunça. Existe o CommonJS
spec
, o RequireJS
library
e o
node.js require system, mas nenhum deles se dá bem juntos. Se você quiser
compartilhar o código entre cliente e servidor (uma das grandes vitórias do JS
no sevidor) sem um carregador de módulo, você pode usar a maneira a seguir, que, apesar de não ser muito bonita, funciona!

(function(exports) {

var MyClass = function() { /* ... */ };
var myObject = {};

exports.MyClass = MyClass;
exports.myObject = MyObject;

})(typeof global === "undefined" ? window : exports);

Essa opção
se apoia no fato de que o node.js define um objeto global, enquanto o navegador não o
faz. Com ela, o require() do node.js ficará feliz, e você também pode incluir
o arquivo na tag <script> sem poluir sua namespace, pressupondo, é claro,
que nenhum outro JS polua sua namespace com um objeto window.global.

Infelizmente,
essa abordagem somente funciona bem para um módulo compartilhado. Assim que você tiver módulos múltiplos dependendo um dos outros (através do requires do lado do servidor, e globais do lado do navegador), a diferença entre a inclusão do namespace
e do navegador se torna dolorosamente aparente e requer mais trabalho dessa opção.

Outra
abordagem é usar o browserify para juntar todo o JS e a
emulação requerida no navegador. Essa abordagem se apoia no node.js para servir
o JS gerado, o que não é ideal, uma vez que os arquivos estáticos deveriam ser
servidos por um webserver otimizado para esse objetivo. No entanto, o node.js
+ o browserify podem ser configurados para compilar o JS que pode ser servido
estaticamente sem se apoiar no node para servi-lo. Essa abordagem introduz um
certo overhead:

  1. Um passo extra construído
    para implementação;
  2. Overhead de performance de
    qualquer engine que o browserify usa para suportar os chamados
    require().

No geral,
essa abordagem parece melhor para mim, e espero testá-la em versões futuras do Osmus.

Sua vez

Lancei o o Osmus como um jogo totalmente open source em HTML5.
Sinta-se à vontade para modificá-lo da maneira que você quiser. E, para a melhoria de outros jogos relacionados, leia este artigo
sobre HTML5 canvas performance, recentemente postado
no html5rocks.

***

Texto original disponível em http://smus.com/multiplayer-html5-games-with-node