Jump to content
Search In
  • More options...
Find results that contain...
Find results in...
  • Chatbox

    Lembre-se de ler as nossas regras!
    Load More
    You don't have permission to chat.

Recommended Posts

Como Funciona?

A maioria dos jogos online multiplayer utilizam a arquitetura server-client. Cada player roda um cliente que no nosso caso será o Phaser.IO que é responsável por exibir e manipular a entrada do player, e cada cliente troca dados com um servidor (NodeJS) central e autoritário, que valida as ações dos jogadores e o transmite para os outros clientes.

Configurando o Servidor

Server.js

Instale todos os Pacotes em seu NodeJs (Socket.IO e Express). Para instalar é simples basta você executar ‘npm install express’ e ‘npm install socket.io’.

var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io').listen(server);

Express é o módulo que usaremos para servir arquivos para os clientes. Criamos uma nova instância chamada app e, combinando-a ao módulo http , garantimos que nosso aplicativo expresso funcionará como um servidor http. Por fim, exigimos o módulo socket.io e o fazemos ouvir as conexões com esse servidor. Este é mais ou menos código clichê para começar com aplicativos simples.

O próximo passo é entregar os arquivos, dependendo do caminho solicitado.

app.use('/css',express.static(__dirname + '/css'));
app.use('/js',express.static(__dirname + '/js'));
app.use('/assets',express.static(__dirname + '/assets'));

app.get('/',function(req,res){
    res.sendFile(__dirname+'/index.html');
});

Essas linhas são necessárias para poder fornecer arquivos estáticos , como folhas de estilo CSS ou recursos de jogos, que não serão acessíveis diretamente, mas precisam ser acessados pelo seu jogo. Por conveniência, o segundo argumento do app.use () permite que você especifique os caminhos virtuais, que não são os caminhos reais para seus recursos, mas que serão os caminhos usados pelos seus scripts para acessá-los.

Concluímos a configuração do servidor, indicando qual porta o servidor deve escutar e especificando qual arquivo será exibido como a página raiz.

app.get('/',function(req,res){
    res.sendFile(__dirname+'/index.html'); 
});

server.listen(8081,function(){ // Listens to port 8081
    console.log('Listening on '+server.address().port);
});

Configurando o Cliente

Index.html

Index.html é a página do gateway que será exibida ao se conectar ao seu servidor, e também neste caso a página na qual o jogo será exibido. Cabe a você estruturá-lo como quiser, mas deve conter pelo menos um elemento div com o id ' game '. Ele também deve incluir os arquivos Javascript necessários para o seu jogo, incluindo o socket.io. Aqui está como incluí-lo:

<script src="/socket.io/socket.io.js"></script>

Isso funciona porque o caminho virtual /socket.io/socket.io.js é criado automaticamente ao instalar o socket.io.

Neste ponto, se você executar o servidor (digitando 'node server.js' em um terminal) e navegar para o seu aplicativo (por exemplo, em http: // localhost: 8081 / se você estiver executando localmente e mantido a porta padrão) , você deve ver o seu index.html servido para você, com qualquer conteúdo que você tenha colocado neste ponto (talvez um “Hello World”, para ter certeza de que funciona). Vamos agora incluir o game.js para configurar o jogo. Vou me referir a este processo doravante como "executando o jogo".

js/game.js

Agora podemos continuar configurando a tela do jogo (assumindo que temos um bloco div com o id ' game ') e declarando um único estado do jogo, chamado ' Game ', correspondente a um objeto Javascript com o mesmo nome.

var game = new Phaser.Game(16*32, 600, Phaser.AUTO, document.getElementById('game'));
game.state.add('Game',Game);
game.state.start('Game');
var Game = {};

No Game.init () , existe apenas um parâmetro para definir:

Game.init = function(){
    game.stage.disableVisibilityChange = true;
};

Isso não é obrigatório, mas é útil, pois fará com que o jogo continue reagindo às mensagens do servidor, mesmo quando a janela do jogo não tiver foco (o que é um comportamento desejado para a maioria dos jogos).

No Game.preload () , nós carregamos os recursos que precisaremos, incluindo o mapa de blocos no formato JSON (exportado do Tiled). Eu não vou entrar em muitos detalhes aqui sobre como criar e lidar com tilemaps lado a lado, mas se você quiser que eu faça um tutorial para cobrir esses aspectos, não hesite em me avisar.

Game.preload = function() {
    game.load.tilemap('map', 'assets/map/example_map.json', null, Phaser.Tilemap.TILED_JSON);
    game.load.spritesheet('tileset', 'assets/map/tilesheet.png',32,32);
    game.load.image('sprite','assets/sprites/sprite.png'); // this will be the sprite of the players
};

Em Game.create () , começamos criando e exibindo nosso mapa.

Game.create = function(){
    var map = game.add.tilemap('map');
    map.addTilesetImage('tilesheet', 'tileset'); // tilesheet is the key of the tileset in map's JSON file
    var layer;
    for(var i = 0; i < map.layers.length; i++) {
        layer = map.createLayer(i);
    }
    layer.inputEnabled = true; // Allows clicking on the map
};

Observe que, embora os cliques estejam ativados no mapa, não há código no momento para lidar com isso; isso ocorrerá quando o servidor estiver em execução e a comunicação entre o cliente e o servidor estiver funcionando.

Neste ponto, ao executar o jogo, você deve ver o mapa exibido, sem que nada mais aconteça:

empty_map-300x213.png

js/client.js

Em index.html, inclua um novo arquivo Javascript, client.js , que conterá um objeto Clientque atuará como a interface entre o servidor e o próprio jogo.

var Client = {};
Client.socket = io.connect();

O bit importante aqui é a segunda linha, onde iniciamos uma conexão com o servidor ( localhost se você não especificar de outra forma entre os parênteses). Cada vez que um jogador navega para seu aplicativo, uma conexão será estabelecida com o servidor. Isso irá criar um soquete. Soquetes são pontos de extremidade no fluxo de comunicação entre o servidor e o cliente. Com o socket.io, podemos enviar e receber mensagens através do soquete, o que constitui a maneira básica pela qual o cliente e o servidor irão interagir. Aqui, o soquete do cliente é armazenado no Client.socket para uso futuro.

 

Interações em tempo real

Agora a parte interessante começa. Precisamos deixar o servidor ciente do que os jogadores fazem, assim como fazer os clientes reagirem às mensagens vindas do servidor. Quando um jogador executa uma ação (conectando, desconectando ou movendo), usaremos a API Socket.io para enviar uma mensagem ao servidor para notificá-lo dessa ação. Em troca, o servidor usará a mesma API para enviar mensagens para os clientes quando precisarem ser informados sobre a ação de outro jogador. O restante deste tutorial ilustra como enviar e receber essas mensagens e como integrá-las a um jogo Phaser.

 

Exibindo os jogadores conectados

Quando um novo jogador se conecta, um novo sprite deve aparecer no jogo para todos os jogadores conectados, incluindo o recém-conectado. As coordenadas dos sprites são determinadas aleatoriamente pelo servidor.

js/game.js

Primeiro, vamos modificar o Game.create () no game.js para que o cliente notifique o servidor de que um novo player deve ser criado. Para esse fim, adicionamos Client.askNewPlayer (); no final de Game.create () . No começo, nós também adicionamos Game.playerMap = {}; : este objeto vazio será útil mais tarde para acompanhar os jogadores.

Game.create = function(){
    Game.playerMap = {};
    var map = game.add.tilemap('map');
    map.addTilesetImage('tilesheet', 'tileset'); // tilesheet is the key of the tileset in map's JSON file
    var layer;
    for(var i = 0; i < map.layers.length; i++) {
        layer = map.createLayer(i);
    }
    layer.inputEnabled = true; // Allows clicking on the map
    Client.askNewPlayer();
};

js/client.js

Agora em client.js , precisamos definir o método Client.askNewPlayer () :

Client.askNewPlayer = function(){
    Client.socket.emit('newplayer');
};

Esse método usará nosso objeto de soquete e enviará uma mensagem para o servidor. Esta mensagem terá o rótulo 'newplayer', que é auto-explicativo. Um segundo argumento pode ser adicionado para passar dados adicionais, mas não será necessário neste caso.

server.js

No server.js , precisamos reagir às mensagens do cliente. Adicione o seguinte código:


server.lastPlayderID = 0; // Keep track of the last id assigned to a new player

io.on('connection',function(socket){
    socket.on('newplayer',function(){
        socket.player = {
            id: server.lastPlayderID++,
            x: randomInt(100,400),
            y: randomInt(100,400)
        };
        socket.emit('allplayers',getAllPlayers());
        socket.broadcast.emit('newplayer',socket.player);
    });
});

function getAllPlayers(){
    var players = [];
    Object.keys(io.sockets.connected).forEach(function(socketID){
        var player = io.sockets.connected[socketID].player;
        if(player) players.push(player);
    });
    return players;
}

function randomInt (low, high) {
    return Math.floor(Math.random() * (high - low) + low);
}

Dizemos ao Socket.io para ouvir o evento 'connection', que é acionado toda vez que um cliente se conecta ao servidor (usando io.connect () ). Quando isso acontece, ele deve chamar o retorno de chamada especificado como o segundo argumento. Este retorno de chamada recebe como primeiro argumento o soquete usado para estabelecer a conexão, que, assim como o soquete do cliente, pode ser usado para passar mensagens.

Usando o método socket.on () dos objetos de soquete, é possível especificar retornos de chamada para manipular mensagens diferentes. Portanto, cada vez que um cliente específico envia uma mensagem específica através de seu soquete, um retorno de chamada específico será chamado em reação. Nesse caso, definimos um retorno de chamada para reagir à mensagem 'newplayer'. Vamos decompor o que é feito lá:

socket.on('newplayer',function(){
        socket.player = {
            id: server.lastPlayderID++,
            x: randomInt(100,400),
            y: randomInt(100,400)
        };
        socket.emit('allplayers',getAllPlayers());
        socket.broadcast.emit('newplayer',socket.player);
    });

Primeiro, criamos um novo objeto personalizado, usado para representar um player, e armazená-lo no objeto de soquete. Como você pode ver, é possível adicionar propriedades arbitrárias específicas do cliente ao objeto de soquete, tornando-as convenientes para o acesso. Neste objeto, damos ao jogador um ID único (que será usado no lado do cliente) e determinamos aleatoriamente a posição do sprite. Então, queremos enviar para o novo jogador a lista de jogadores já conectados:

socket.emit('allplayers',getAllPlayers());

Socket.emit () envia uma mensagem para um soquete específico. Aqui, enviamos para o cliente recém-conectado uma mensagem chamada 'allplayers' e, como segundo argumento, a saída de Client.getAllPlayers (), que será uma matriz dos players conectados no momento. Isso permite que os jogadores recém-conectados se atualizem com a quantidade e as posições dos jogadores já conectados. Vamos dar uma olhada rápida em Client.getAllPlayers () :

function getAllPlayers(){
    var players = [];
    Object.keys(io.sockets.connected).forEach(function(socketID){
        var player = io.sockets.connected[socketID].player;
        if(player) players.push(player);
    });
    return players;
}

O io.sockets.connected é uma matriz interna do Socket.io dos soquetes atualmente conectados ao servidor. Podemos usá-lo para iterar todos os sockets, obter a propriedade do player que adicionamos a eles (se houver) e enviá-los para uma lista, listando efetivamente os players conectados. E finalmente:

socket.broadcast.emit('newplayer',socket.player);

socket.emit.broadcast () envia uma mensagem para todos os sockets conectados, exceto o soquete que acionou o retorno de chamada. Ele permite transmitir eventos de um cliente para todos os outros clientes, sem que eles sejam retornados ao cliente iniciante. Aqui, nós transmitimos a mensagem 'newplayer' e enviamos como dados o novo objeto de jogador.

Para resumir o que fizemos nestes últimos passos: 
- Ouvimos as conexões dos clientes e definimos as chamadas de retorno para processar as mensagens enviadas pelos sockets 
- Quando recebemos a mensagem 'newplayer' de um cliente, criamos um objeto small player que armazenamos no socket do cliente 
- Para o novo cliente, enviamos uma lista de todos os outros players, para que ele possa exibi-los 
- Para os outros clientes, enviamos as informações sobre o recém-chegado

Até agora, nosso servidor reage a uma mensagem dos clientes. Agora precisamos adaptar os clientes para que eles possam processar as mensagens 'allplayers' e 'newplayer' do servidor em retorno, completando assim o loop `connect - notificar o servidor que estou aqui - obter informações em retorno - display it`. Note que a mensagem 'newplayer' enviada pelo cliente e aquela enviada pelo servidor não é a mesma; Eu escolhi dar a eles o mesmo rótulo porque eles transmitem o mesmo tipo de informação, mas eles serão tratados separadamente, uma vez que eles têm endpoints diferentes (o servidor para o primeiro, o cliente para o último).

js/client.js

Em client.js , adicione este código:

Client.socket.on('newplayer',function(data){
    Game.addNewPlayer(data.id,data.x,data.y);
});

Client.socket.on('allplayers',function(data){
    console.log(data);
    for(var i = 0; i < data.length; i++){
        Game.addNewPlayer(data[i].id,data[i].x,data[i].y);
    }
});

Como você pode ver, a mesma sintaxe para manipular mensagens pode ser usada no lado do cliente. Quando os dados são enviados ao longo de uma mensagem, eles podem ser recuperados como o primeiro argumento do retorno de chamada no terminal de recebimento. Portanto, o objeto ' data ' alimentado para o callback 'newplayer' corresponde aos dados do socket.player enviados pelo servidor. Para a mensagem 'allplayers', é uma lista de objetos socket.player . Em ambos os casos, esses dados são processados chamando Game.addNewPlayer () , que agora podemos definir em game.js :

js/game.js

Game.addNewPlayer = function(id,x,y){
    Game.playerMap[id] = game.add.sprite(x,y,'sprite');
};

Esse método cria um novo sprite nas coordenadas especificadas e armazena o objeto Sprite correspondente em um array associativo declarado em Game.create () , com o id fornecido como a chave. Isso permite acessar facilmente o sprite correspondente a um jogador específico, por exemplo, quando precisamos movê-lo ou removê-lo (veja abaixo).

Neste ponto, se você reiniciar o servidor (para que as últimas modificações sejam levadas em conta) e navegue até o seu jogo, você verá um pequeno caractere exibido, correspondente ao seu sprite. Se você se conectar com outros navegadores, caracteres adicionais deverão aparecer na tela.

player.png

Manipulando desconexões

Como é, quando um jogador se desconecta, seu sprite permanecerá na tela dos outros jogadores, o que não é desejável. Isso pode ser corrigido ao processar a mensagem 'desconectar' que o servidor recebe automaticamente quando um cliente se desconecta ou atinge o tempo limite ativamente. Esta mensagem pode ser tratada como qualquer outra, da mesma forma que fizemos para o 'newplayer', por exemplo: ligando um callback a ele dentro do método io.on () :

server.js

io.on('connection',function(socket){

    socket.on('newplayer',function(){
        socket.player = {
            id: server.lastPlayderID++,
            x: randomInt(100,400),
            y: randomInt(100,400)
        };
        socket.emit('allplayers',getAllPlayers());
        socket.broadcast.emit('newplayer',socket.player);

        socket.on('disconnect',function(){
            io.emit('remove',socket.player.id);
        });
    });
});

Em reação à mensagem 'desconectar', usamos io.emit () , que envia uma mensagem para todos os clientes conectados. Enviamos a mensagem 'remove' e enviamos o id do jogador desconectado para remover. 
Nota: Obrigado ao Kaundur por apontar que o callback 'desconectado' deve ser registrado dentro do callback 'newplayer'; se não, e 'desconectar' é de alguma forma chamado antes de 'newplayer', o servidor irá travar!

js/client.js

Client.socket.on('remove',function(id){
    Game.removePlayer(id);
});

E no game.js :

js/game.js

Game.removePlayer = function(id){
    Game.playerMap[id].destroy();
    delete Game.playerMap[id];
};

Isso ilustrou o uso da estrutura de dados do Game.playerMap , a propósito. Não há necessidade de repetir sprites; o id permite buscar imediatamente. Agora tudo o que resta a fazer é processar e transmitir os movimentos dos jogadores.

Movendo os movimentos do jogador

js/game.js

Chegou a hora de completar o Game.create () . Basicamente, queremos que, quando o mapa for clicado, as coordenadas sejam enviadas para o servidor, para que a posição do jogador que clicou possa ser atualizada para todos. Adicione a seguinte linha ao Game.create () :

layer.events.onInputUp.add(Game.getCoordinates, this);

Agora, o mapa reagirá aos cliques chamando o método Game.getCoordinates () , que podemos definir da seguinte forma:

Game.getCoordinates = function(layer,pointer){
    Client.sendClick(pointer.worldX,pointer.worldY);
};

Callback de onInputUp eventos em Phaser receber como segundo argumento do correspondente objeto ponteiro, que contém duas propriedades worldX e worldy que podemos usar para saber onde, no mapa de jogo, fez o clique ocorrer. Podemos então passar essas coordenadas para Client.sendClick () em client.js :

js/client.js

Client.sendClick = function(x,y){
  Client.socket.emit('click',{x:x,y:y});
};

Que simplesmente envia as coordenadas para o servidor, com o rótulo 'clique'. Não há necessidade de enviar qualquer ID de jogador, já que o socket é específico do cliente e associado a apenas um jogador.

server.js

Em server.js , aqui está a lista final de retornos de chamada de mensagem:

io.on('connection',function(socket){

    socket.on('newplayer',function(){
        socket.player = {
            id: server.lastPlayderID++,
            x: randomInt(100,400),
            y: randomInt(100,400)
        };
        socket.emit('allplayers',getAllPlayers());
        socket.broadcast.emit('newplayer',socket.player);

        socket.on('click',function(data){
            console.log('click to '+data.x+', '+data.y);
            socket.player.x = data.x;
            socket.player.y = data.y;
            io.emit('move',socket.player);
        });

        socket.on('disconnect',function(){
            io.emit('remove',socket.player.id);
        });
    });
});

Os campos x e y da propriedade player do soquete são atualizados com as novas coordenadas e transmitidos imediatamente para todos, para que possam ver a alteração. Agora, o objeto socket.player completo é enviado, porque os outros clientes precisam saber o id do jogador que está se movendo, a fim de mover o sprite correto na tela (embora, nesse jogo minimalista, não exista um caminho real para distinguir os jogadores).

js/client.js

De volta ao client.js , precisamos lidar com a mensagem 'mover' do servidor, para que os clientes possam reagir a outro jogador em movimento:

Client.socket.on('move',function(data){
    Game.movePlayer(data.id,data.x,data.y);
});

O processo deve começar a se tornar familiar para você. Em game.js :

js/game.js

Game.movePlayer = function(id,x,y){
    var player = Game.playerMap[id];
    var distance = Phaser.Math.distance(player.x,player.y,x,y);
    var duration = distance*10;
    var tween = game.add.tween(player);
    tween.to({x:x,y:y}, duration);
    tween.start();
};

Nós novamente fazemos uso da estrutura Game.playerMap para recuperar o sprite correto, e então nós o interpolamos para tornar o movimento progressivo.

 

CÓDIGO FONTE COMPLETO (COMENTE PARA VER):  

Hidden Content

    Give reaction or reply to this topic to see the hidden content.

Bjos Tio Unk ..

 

  • Like 4

Share this post


Link to post
Share on other sites

Achei interessante a dinâmica utilizada e os conhecimentos aprendidos aqui, obrigado por nos ensinar algo incrível!

Share this post


Link to post
Share on other sites

Já havia conhecido a Engine, acessado o site, visto alguns exemplos, até tentei criar alguma coisa. Depois vou ler com mais calma o seu tutorial e tentar criar algo melhor. Espero que esse tutorial ajude as pessoas que querem iniciar no ramo de desenvolvimento de games. 

Share this post


Link to post
Share on other sites
9 minutos atrás, T0E disse:

Já havia conhecido a Engine, acessado o site, visto alguns exemplos, até tentei criar alguma coisa. Depois vou ler com mais calma o seu tutorial e tentar criar algo melhor. Espero que esse tutorial ajude as pessoas que querem iniciar no ramo de desenvolvimento de games. 

É um tutorial bem explicado, porem eu não recomendo para iniciantes. 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×