Dev (Back & Front)ARTIGO

Criando um MMO 3D utilizando WebSockets

Com este artigo, eu gostaria de te dar uma melhor ideia de MMOs e de deixar você com menos medo em relação à complexidade de se criar um.

Olá, pessoal! Meu nome é Nick Janssen, criador do Ironbane, um MMO 3D que utiliza WebGL e WebSockets. Com este artigo, eu gostaria de te dar uma melhor ideia de MMOs e de deixar você com menos medo em relação à complexidade de se criar um. A partir da minha experiência, vejo que as pessoas acham que eles são muito difíceis de serem feitos, apesar de serem na verdade bem fáceis de fazer utilizando-se tecnologias web de hoje.

Um MMO? Não dá para você fazer!

MMOs são legais. Na verdade, eles são considerados uma das coisas mais difíceis de serem criadas quando o assunto é desenvolvimento de software. Acredito que MMOs são intimidadores principalmente por razões históricas.

No passado, programação em rede costumava ser muito complicada. Complexas chamadas de sockets estavam em todo lugar, eram necessárias múltiplas threads e JSON ainda era desconhecido. Muita coisa mudou desde então com o surgimento de Node.js, seu loop de eventos e a facilidade para se utilizar bibliotecas de sockets.

Além disso, escrever jogos 3D era um desafio em si. Você tinha que incluir as bibliotecas certas, instalar as dependências no cliente e escrever complicadas chamadas de engine para fazer coisas triviais, tal como criar uma textura. Conseguir mostrar um triângulo na tela já era uma conquista e tanto.

Criando uma textura com DirectX10

D3DX10_IMAGE_LOAD_INFO loadInfo;
ZeroMemory( &loadInfo, sizeof(D3DX10_IMAGE_LOAD_INFO) );
loadInfo.BindFlags = D3D10_BIND_SHADER_RESOURCE;

ID3D10Resource *pTexture = NULL;
D3DX10CreateTextureFromFile( pDevice, L"crate.gif", &loadInfo, NULL, &pTexture, NULL );

Criando uma textura com Three.js

var texture = THREE.ImageUtils.loadTexture('crate.gif'),

O começo

Para nosso MMO Ironbane, fiz uma coisa de cada vez e me esforcei bem. Lembre-se, Roma não foi construída em um dia. Mas, com a tecnologia de hoje, você pode alcançar coisas em um ritmo muito mais rápido do que jamais foi possível.

Comecei com um demo de terreno do three.js e o modifiquei passo-a-passo. Com alguns dias, tive um avião andando pelo espaço com uma textura como as costas pixeladas de uma pessoa.

mmo-3d

O próximo passo foi fazer o jogador se conectar ao servidor central. Usando Socket.IO, configurei um backend Node.js simples que respondia às conexões dos jogadores e punha-os em uma lista global gerenciada por um serviço chamado worldHandler:

io.sockets.on("connection", function (socket) {
  socket.unit = null;
  socket.on("connectServer", function (data, reply) {
      var unit = new IB.Player(data);
      worldHandler.addUnit(unit);
  });
});

Anunciando aos jogadores outros jogadores por perto

Para que os jogadores saibam dos outros jogadores por perto, o servidor precisa saber, a todo momento, quais jogadores podem ver os outros. Para isso, cada instância de jogador no servidor faz uso de um array otherUnity. Esse array é simplesmente preenchido com instâncias de outras entidades que estão atualmente nos arredores.

Quando um novo jogador é acrescentado ao worldHandler, sua lista otherUnits é atualizada dependendo de onde eles estão no mundo. Mais tarde, quando se moverem, a lista é avaliada novamente, e qualquer mudança nessa lista é enviada ao cliente na forma de um evento de socket addUnit e removeUnit.

Agora, gostaria de lembrar que a primeira letra de MMO significa Massivo (Massive). Para jogos massivos, cada jogador não pode saber sobre cada um dos outros jogadores porque isso iria derreter o seu servidor.

Particionamento espacial

Para remediar isso, você precisa de particionamento espacial. De maneira resumida, isso significa que você divide o seu mundo em um grid. Para visualizar, pense em seu servidor fazendo uso de um plano cartesiano para posicionar os jogadores em um grid. As posições dos jogadores não são alteradas; em vez disso, o servidor apenas calcula qual será a nova posição do jogador.

mmo-3d-1

Com muitos jogadores passando por muitas posições diferentes, alguns terão a mesma posição. Um jogador então deve saber apenas sobre os jogadores que estão na mesma posição dele ou a apenas uma célula de distância. Você pode facilmente converter a posição do grid com a do mundo com estas funções:

function worldToGridCoordinates(x, y, gridsize) {
  if ( gridsize % 2 != 0 ) console.error("gridsize not dividable by 2!");

  var gridHalf = gridsize / 2;

  x = Math.floor((x + gridHalf)/gridsize);
  y = Math.floor((y + gridHalf)/gridsize);

  return {
    x: x,
    y: y
  };
}

function gridToWorldCoordinates(x, y, gridsize) {
  if ( gridsize % 2 != 0 ) console.error("gridsize not dividable by 2!");

  x = (x * gridsize);
  y = (y * gridsize);<p></p>

  return {
    x: x,
    y: y
  };
}

Quando um novo jogador é criado no servidor, eles são automaticamente acrescentados a um array multidimensional de unidades pelo worldHandler, usando a posição no grid. No Ironbane, utilizamos ainda um índice de zonas adicional, uma vez que a maior parte dos MMOs possui múltiplas áreas onde os jogadores podem estar.

worldHandler.world[this.zone][this.cellX][this.cellY].units.push(this);

Atualização da lista de jogadores próximos

Uma vez que eles são acrescentados à lista de unidades no servidor, o próximo passo é calcular quais jogadores estão próximos.

// We have two lists
// There is a list of units we currently have, and a list that we will have once we recalculate
// If an item is in the first list, but no longer in the second list, do removeOtherUnit
// If an item is in the first & second list, don't do anything
// If an item is only in the last list, do addOtherUnit
var firstList = this.otherUnits;
var secondList = [];

// Check for all players that are nearby and add them to secondList
var gridPosition = worldToGridPosition(this.x, this.y, 50);

var cx = gridPosition.x;
var cy = gridPosition.y;

for (var x = cx - 1; x <= cx + 1; x++) {
  for (var y = cy - 1; y <= cy + 1; y++) {
    _.each(worldHandler.units[this.zone][x][y], function(unit) {
        if (unit !== this) {
            secondList.push(unit);
        }
    }, this);
  }
}

for (var i = 0; i < firstList.length; i++) {
  if (secondList.indexOf(firstList[i]) === -1) {
    // Not found in the second list, so remove it
    this.removeOtherUnit(firstList[i]);
  }
}
for (var i = 0; i < secondList.length; i++) {
    if (firstList.indexOf(secondList[i]) === -1) {
        // Not found in the first list, so add it
        this.addOtherUnit(secondList[i]);
    }
}

Aqui, addOtherUnit() acrescenta o jogador a seu array otherUnits e envia um pacote para o cliente, informando que um novo jogador entrou nos seus arredores. Esse pacote conterá posição inicial, velocidade, nome e qualquer outro metadado que precisa ser enviado apenas uma vez. removeOtherUnit() simplesmente remove jogadores de seus arrays e dize ao cliente para destruir aquele jogador.

var packet = {
    id: id,
    position: unit.position,
    name: unit.name,
    isGameMaster: true
};

this.socket.emit("addUnit", packet);

Envio de pacotes aos jogadores

Agora temos o coração de um MMO. O passo final é informar regularmente aos jogadores das posições de outros jogadores por perto. Fazemos isso apenas duas vezes por segundo, porque não queremos sobrecarregar o servidor.

_.each(this.world, function(zone) {
    _.each(zone, function(cellX) {
        _.each(cellX, function(cellY) {
            _.each(cellY.units, function(unit) {

                var snapshot = [];

                _.each(unit.otherUnits, function(otherUnit) {
                    var packet = {
                        id:otherUnit.id,
                        x:otherUnit.x,
                        y:otherUnit.y
                    };

                    snapshot.push(packet);
                ));

                if ( snapshot.length ) {
                    unit.socket.emit("snapshot", snapshot);    
                }

            ));
        });
    });
});

Conclusão

Isso é tudo que é necessário para se construir um MMO. As únicas coisas que restaram agora são criar os recursos são próprios do seu jogo, efetuar ajustes finos e cuidar da segurança.

Espero que tenha conseguido te dar uma boa ideia da programação envolvida em um MMO e, acima de tudo, coragem para começar a trabalhar neles. No Ironbane, estamos sempre procurando por colaboradores! Você pode encontrar o código fonte completo do Ironbane diretamente no Github e deve ser capaz de instalá-lo na sua máquina com facilidade.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://davidwalsh.name/3d-websockets

É um desenvolvedor web que tem paixão por criar coisas novas. Nascido na Bélgica, ele tem graduação de bacharel em Digital Arts &amp; Entertainment. Criou o Ironbane, um MMO 3D que usa WebGL e WebSockets.

Ver perfil