Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Learn why MongoDB was selected as a leader in the 2024 Gartner® Magic Quadrant™
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
MongoDBchevron-right

Chat em tempo real em um jogo de Phaser com MongoDB e Socket.io

Nic Raboy10 min read • Published Jan 27, 2022 • Updated Feb 03, 2023
Node.jsMongoDBJavaScript
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Ao criar um jogo multijogador, você provavelmente vai querer implementar uma maneira de interagir com outros jogadores além da experiência geral do jogo. Isso pode ser na forma de vídeo, áudio ou bate-papo escrito dentro de um jogo.
Então, como você gerenciaria essa interação em tempo real e como a armazenaria em seu banco de dados?
Neste tutorial, vamos ver como criar um jogo simples com Faser e JavaScript, como adicionar um componente de bate-papo em tempo real que usa Socket.ioe como salvar cada mensagem em nosso banco de dados NoSQL doMongoDB.
Para ter uma ideia melhor do que esperamos realizar, dê uma olhada na imagem animada a seguir:
Fale em tempo real com o Socket.io
O aspecto real do jogo na animação acima é um pouco fraco, mas o mais importante é a funcionalidade de bate-papo. No exemplo acima, as mensagens de chat e a entrada de chat são incorporadas ao jogo Faser. Quando inserimos uma mensagem, ela é enviada ao servidor por meio de soquetes e o servidor salva as mensagens no MongoDB. Além de salvar, o servidor também transmite as mensagens de volta para cada cliente e recupera todas as mensagens para novos clientes.

Os requisitos

Existem algumas partes móveis quando se trata deste exemplo, no entanto, não há muitos requisitos. Precisaremos do seguinte para ter sucesso:
  • Um cluster do MongoDB Atlas
  • Node.js 12+
O backend será desenvolvido com Node.js e usará o driver MongoDB Node.js para se comunicar com MongoDB e Socket.io para se comunicar com nossos clientes. O frontend, que é o jogo, usará Faser e Socket.io.
A maior parte do que fazemos será realizada usando pacotes JavaScript prontamente disponíveis.

Desenvolvendo o backend para orquestração e persistência de mensagens

Começaremos criando o backend do nosso jogo. Ele fará todo o trabalho pesado para nós que não esteja relacionado ao visual.
No seu computador, crie um novo diretório e execute os seguintes comandos de dentro do diretório:
1npm init -y
2npm install mongodb express cors socket.io --save
Os comandos acima instalarão o driver MongoDB Node.js, Express, Socket.io e uma biblioteca para lidar com o compartilhamento de recursos de origem cruzada entre o jogo e o servidor.
Em seguida, crie um arquivoprincipal.js dentro do diretório do projeto e adicione o seguinte código JavaScript:
1const express = require("express")();
2const cors = require("cors");
3const http = require("http").createServer(express);
4const io = require("socket.io")(http);
5const { MongoClient } = require("mongodb");
6
7const client = new MongoClient(process.env["ATLAS_URI"]);
8
9express.use(cors());
10
11var collection;
12
13io.on("connection", (socket) => {
14 socket.on("join", async (gameId) => {});
15 socket.on("message", (message) => {});
16});
17
18express.get("/chats", async (request, response) => {});
19
20http.listen(3000, async () => {
21 try {
22 await client.connect();
23 collection = client.db("gamedev").collection("chats");
24 console.log("Listening on port :%s...", http.address().port);
25 } catch (e) {
26 console.error(e);
27 }
28});
Boa parte do código acima é padronizada quando se trata de configurar o Express e o MongoDB. Faremos um rápido detalhamento das peças que são importantes a partir de agora.
Primeiro, você notará a seguinte linha:
1const client = new MongoClient(process.env["ATLAS_URI"]);
O ATLAS_URI é uma variável de ambiente no meu computador. Você pode obter o valor para esta variável dentro do painel do MongoDB Atlas. O valor será algo assim:
1mongodb+srv://<username>:<password>@cluster0-yyarb.mongodb.net/<database>?retryWrites=true&w=majority
Você pode optar por codificar seu valor ou usar uma variável de ambiente como eu. Não importa, desde que você saiba o que está escolhendo. O uso de uma variável de ambiente é vantajoso, pois facilita o compartilhamento do projeto sem o risco de expor informações potencialmente confidenciais que possam estar codificadas.
A próxima coisa que você notará é o seguinte:
1http.listen(3000, async () => {
2 try {
3 await client.connect();
4 collection = client.db("gamedev").collection("chats");
5 console.log("Listening on port :%s...", http.address().port);
6 } catch (e) {
7 console.error(e);
8 }
9});
Depois que nos conectamos ao MongoDB Atlas cluster, obtemos a coleção que planejamos usar. Nesse caso, o banco de dados que planejamos usar é gamedev e a coleção é chats, nenhum dos quais precisa existir antes de iniciar seu aplicativo.
Com os fundamentos adicionados ao aplicativo, vamos nos concentrar nas coisas mais importantes, começando pelo endpoint da REST API:
1express.get("/chats", async (request, response) => {
2 try {
3 let result = await collection.findOne({ "_id": request.query.room });
4 response.send(result);
5 } catch (e) {
6 response.status(500).send({ message: e.message });
7 }
8});
Embora estejamos usando o Socket.io para a maior parte de nossa comunicação, faz sentido ter um endpoint para obter inicialmente quaisquer dados de chat. Não é acessado com frequência e evitará muito estresse na camada de soquete.
O que estamos dizendo no endpoint é que queremos encontrar um único documento com base no valorroom que foi passado com a solicitação. Esse valor representará nossa sala de jogos ou de bate-papo, conforme você quiser interpretá-lo. Esse único documento terá todas as nossas conversas de chat anteriores para a sala específica. Esses dados serão usados para atualizar os clientes quando eles entrarem.
Agora podemos entrar nas coisas realmente interessantes!
Vamos revisar a lógica Socket.io dentro do arquivo principal.js:
1io.on("connection", (socket) => {
2 socket.on("join", async (gameId) => {
3 try {
4 let result = await collection.findOne({ "_id": gameId });
5 if(!result) {
6 await collection.insertOne({ "_id": gameId, messages: [] });
7 }
8 socket.join(gameId);
9 socket.emit("joined", gameId);
10 socket.activeRoom = gameId;
11 } catch (e) {
12 console.error(e);
13 }
14 });
15 socket.on("message", (message) => {});
16});
Você perceberá que, desta vez, temos alguma lógica para o eventojoin.
Quando uma carga útiljoin é recebida do cliente, o gameId que o cliente fornece com a carga é usado para tentar localizar um documento MongoDB existente. Se um documento existir, significa que a sala de chat existe. Se não funcionar, devemos criar um. Depois de recuperar ou criar um documento no MongoDB, podemos ingressar na sala de soquete com Socket.io, emitir um evento de volta ao cliente ao qual nos associamos e especificar que a sala ativa é a do gameId que acabamos de passar .
Um soquete pode fazer parte de várias salas, mas só nos preocupamos com uma, a ativa.
Agora vamos dar uma olhada no eventomessage para nossos soquetes:
1io.on("connection", (socket) => {
2 socket.on("join", async (gameId) => {
3 // Logic here ...
4 });
5 socket.on("message", (message) => {
6 collection.updateOne({ "_id": socket.activeRoom }, {
7 "$push": {
8 "messages": message
9 }
10 });
11 io.to(socket.activeRoom).emit("message", message);
12 });
13});
Quando o cliente envia uma mensagem, queremos anexá-la ao documento definido pela sala ativa. Lembre-se de que a sala, o ID do jogo e o ID do documento são a mesma coisa e cada soquete também mantém essas informações.
Depois que a mensagem é salva, ela é transmitida para todos os soquetes que fazem parte da mesma sala.
O backend agora é totalmente capaz de lidar com o sistema de chat do nosso jogo.

Desenvolvendo um jogo de faser com soquetes e interação em tempo real

Com o backend sob controle, agora podemos nos concentrar no jogo que podemos distribuir aos clientes, também conhecidos como outros jogadores.
Crie um novo diretório em seu computador e, dentro desse diretório, crie um arquivoindex.html com a seguinte marcação HTML:
1<!DOCTYPE html>
2<html>
3 <head></head>
4 <body>
5 <div id="game"></div>
6 <script src="//cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
7 <script src="//cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.min.js"></script>
8 <script>
9
10 const phaserConfig = {
11 type: Phaser.AUTO,
12 parent: "game",
13 width: 1280,
14 height: 720,
15 backgroundColor: "#E7F6EF",
16 physics: {
17 default: "arcade",
18 arcade: {
19 gravity: { y: 200 }
20 }
21 },
22 dom: {
23 createContainer: true
24 },
25 scene: {
26 init: initScene,
27 preload: preloadScene,
28 create: createScene
29 }
30 };
31
32 const game = new Phaser.Game(phaserConfig);
33
34 function initScene() {}
35 function preloadScene() {}
36 function createScene() {}
37
38 </script>
39 </body>
40</html>
Pode parecer que muita coisa está acontecendo no HTML acima, mas na verdade é apenas a configuração do Phaser.
Primeiro, você notará as duas linhas a seguir:
1<script src="//cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
2<script src="//cdn.jsdelivr.net/npm/phaser@3.24.1/dist/phaser.min.js"></script>
Nas linhas acima, estamos incluindo a estrutura de desenvolvimento de jogos Phaser e a biblioteca cliente Socket.io. Essas bibliotecas podem ser usadas diretamente de uma CDN ou baixadas para o diretório do seu projeto.
A maior parte do nosso padrão concentra-se no objetophaserConfig:
1const phaserConfig = {
2 type: Phaser.AUTO,
3 parent: "game",
4 width: 1280,
5 height: 720,
6 backgroundColor: "#E7F6EF",
7 physics: {
8 default: "arcade",
9 arcade: {
10 gravity: { y: 200 }
11 }
12 },
13 dom: {
14 createContainer: true
15 },
16 scene: {
17 init: initScene,
18 preload: preloadScene,
19 create: createScene
20 }
21};
Como se trata de um jogo, vamos ativar a física do jogo. Em particular, usaremos a física de arcade e a gravidade do ambiente será específica no eixo y. Embora não façamos muito em termos de física neste jogo, você pode aprender mais sobre física de arcade em um tutorial anterior que escrevi, intituladoHandle Collisions Between Sprites in Phaser with Arcade Physics.
No objeto phaserConfig, você notará o campodom. Isso nos permitirá aceitar a entrada de texto do usuário diretamente em nosso jogo Phaser. Faremos algo semelhante ao que vimos em meu tutorial, Maintaining a Geolocation Specific Game Leaderboard with Phaser and MongoDB.
Isso nos leva às nossas funções de ciclo de vida da cena.
A funçãoinitSceneé onde inicializamos nossas variáveis e nossa conexão com o servidor backend:
1function initScene() {
2 this.socket = io("http://localhost:3000", { autoConnect: false });
3 this.chatMessages = [];
4}
Estamos especificando autoConnect como falso porque não queremos nos conectar até que nossa cena termine de ser criada. Faremos a conexão manualmente em uma função de ciclo de vida diferente.
A próxima função do ciclo de vida é a função preloadScene, mas antes de chegarmos lá, provavelmente devemos criar nosso formulário de entrada.
No diretório do seu projeto de jogo, crie um novo arquivo form.html com o seguinte HTML:
1<!DOCTYPE html>
2<html>
3 <head>
4 <style>
5 #input-form input {
6 padding: 10px;
7 font-size: 20px;
8 width: 250px;
9 }
10 </style>
11 </head>
12 <body>
13 <div id="input-form">
14 <input type="text" name="chat" placeholder="Enter a message" />
15 </div>
16 </body>
17</html>
As informações de estilo não são muito importantes, mas a tag<input>real é. Preste atenção ao atributonameem nossa tag, pois isso será importante quando quisermos trabalhar com os dados fornecidos pelo usuário no formulário.
Na função preloadScene do arquivoindex.html, adicione o seguinte:
1function preloadScene() {
2 this.load.html("form", "form.html");
3}
Agora podemos usar nosso formulário de entrada do usuário dentro do jogo!
Para exibir texto e interagir com o formulário de entrada do usuário, precisamos alterar a createScene função no arquivoindex.html :
1function createScene() {
2
3 this.textInput = this.add.dom(1135, 690).createFromCache("form").setOrigin(0.5);
4 this.chat = this.add.text(1000, 10, "", { lineSpacing: 15, backgroundColor: "#21313CDD", color: "#26924F", padding: 10, fontStyle: "bold" });
5 this.chat.setFixedSize(270, 645);
6
7 this.enterKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER);
8
9 this.enterKey.on("down", event => {
10 let chatbox = this.textInput.getChildByName("chat");
11 if (chatbox.value != "") {
12 this.socket.emit("message", chatbox.value);
13 chatbox.value = "";
14 }
15 })
16
17 this.socket.connect();
18
19 this.socket.on("connect", async () => {
20 this.socket.emit("join", "mongodb");
21 });
22
23 this.socket.on("joined", async (gameId) => {
24 let result = await fetch("http://localhost:3000/chats?room=" + gameId).then(response => response.json());
25 this.chatMessages = result.messages;
26 this.chatMessages.push("Welcome to " + gameId);
27 if (this.chatMessages.length > 20) {
28 this.chatMessages.shift();
29 }
30 this.chat.setText(this.chatMessages);
31 });
32
33 this.socket.on("message", (message) => {
34 this.chatMessages.push(message);
35 if(this.chatMessages.length > 20) {
36 this.chatMessages.shift();
37 }
38 this.chat.setText(this.chatMessages);
39 });
40
41}
Há muita coisa acontecer na função createSceneacima e ela pode ser um pouco difícil de entender. Vamos descriptografá-lo para facilitar.
Dê uma olhada nas seguintes linhas:
1this.textInput = this.add.dom(1135, 690).createFromCache("form").setOrigin(0.5);
2this.chat = this.add.text(1000, 10, "", { lineSpacing: 15, backgroundColor: "#21313CDD", color: "#26924F", padding: 10, fontStyle: "bold" });
3this.chat.setFixedSize(270, 645);
4
5this.enterKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.ENTER);
A primeira linha diz que estamos carregando e posicionando o formulário de entrada que adicionamos ao nosso projeto na funçãopreloadScene. Em seguida, estamos criando uma caixa de bate-papo para conter nosso texto. Estamos posicionando e estilizando a caixa de bate-papo a nosso gosto. Por fim, estamos dando ao Phaser o controle da tecla enter, algo que usaremos para enviar o formulário em nosso jogo.
Antes de analisarmos o que acontece com a tecla enter, dê uma olhada nas seguintes linhas:
1this.socket.connect();
2
3this.socket.on("connect", async () => {
4 this.socket.emit("join", "mongodb");
5});
Nas linhas acima, estamos nos conectando manualmente ao nosso servidor de soquete. Quando o soquete do cliente diz que nos conectamos, tentamos entrar em um jogo. Lembra-se do join que estamos esperando no servidor? É para lá que o estamos enviando e, neste caso, estamos dizendo que queremos entrar no jogomongodb.
Depois que o servidor diz que entramos, ele nos envia de volta uma mensagem que podemos pegar aqui:
1this.socket.on("joined", async (gameId) => {
2 let result = await fetch("http://localhost:3000/chats?room=" + gameId).then(response => response.json());
3 this.chatMessages = result.messages;
4 this.chatMessages.push("Welcome to " + gameId);
5 if (this.chatMessages.length > 20) {
6 this.chatMessages.shift();
7 }
8 this.chat.setText(this.chatMessages);
9});
Quando soubermos que ingressamos, podemos executar uma solicitação HTTP no endpoint da API e fornecer a sala, que novamente é o ID do nosso jogo. A resposta será todas as nossas mensagens de chat que adicionaremos a uma array. Para eliminar o risco de mensagens exibidas fora da tela, estamos exibindo itens da parte superior da matriz se o comprimento for maior que nosso limite.
Quando chamamos setText usando um valor de array, o Faser separará automaticamente cada item da array com um novo caractere de linha.
Até o momento, entramos em um jogo e recebemos todas as mensagens anteriores. Agora queremos enviar mensagens:
1this.enterKey.on("down", event => {
2 let chatbox = this.textInput.getChildByName("chat");
3 if (chatbox.value != "") {
4 this.socket.emit("message", chatbox.value);
5 chatbox.value = "";
6 }
7})
Quando a tecla Enter é pressionada, obtemos a <input> marcação do arquivoformulário.html pelo name atributo . Se o valor não estiver vazio, podemos enviar uma mensagem para o servidor e limpar o campo.
Em nossa configuração, nossas próprias mensagens não são exibidas imediatamente. Deixamos o servidor decidir o que deve ser exibido. Isso significa que precisamos ouvir as mensagens que chegam:
1this.socket.on("message", (message) => {
2 this.chatMessages.push(message);
3 if(this.chatMessages.length > 20) {
4 this.chatMessages.shift();
5 }
6 this.chat.setText(this.chatMessages);
7});
Se uma mensagem chegar do servidor, nós a colocamos em nossa array. Se essa array for maior do que nosso limite, retiramos um item do topo antes de renderizá-lo na tela. Estamos fazendo isso porque não queremos a rolagem dentro do jogo e não queremos que as mensagens saiam da tela. Não é à prova de erros, mas funcionará bem o suficiente para nós.
Se você executasse o jogo agora, você teria algo assim:
faser com MongoDB chat
Como você pode ver, temos apenas uma área de bate-papo à direita da tela. Não é particularmente interessante para um jogo, se você me perguntar.
O que vamos fazer é adicionar um pouco de brilho ao jogo. Nada exorbitante, mas apenas um pouco para nos lembrar que, na verdade, estamos desenvolvendo jogos com o MongoDB.
Antes de fazer isso, Go em frente e baixe a seguinte imagem:
MongoDB Leaf
Não importa qual imagem você usa, mas estou economizando o trabalho de ter que encontrar uma para este jogo.
Com a imagem dentro do seu projeto, precisamos alterar a funçãopreloadScene do arquivoindex.html:
1function preloadScene() {
2 this.load.html("form", "form.html");
3 this.load.image("leaf", "leaf.png");
4}
Observe que agora estamos carregando a imagem que acabamos de baixar. O objetivo aqui é ter muitas dessas imagens circulando pela tela.
Na funçãocreateScene, vamos fazer a seguinte modificação:
1function createScene() {
2
3 this.leafGroup = this.physics.add.group({
4 defaultKey: "leaf",
5 maxSize: 15
6 });
7
8 for(let i = 0; i < 15; i++) {
9 let randomX = Math.floor(Math.random() * 1000);
10 let randomY = Math.floor(Math.random() * 600);
11 let randomVelocityX = Math.floor(Math.random() * 2);
12 this.leafGroup.get(randomX, randomY)
13 .setScale(0.2)
14 .setVelocity([-100, 100][randomVelocityX], 200)
15 .setBounce(1, 1)
16 .setCollideWorldBounds(true)
17 }
18
19 // Chat and socket logic ...
20}
No código acima, estamos criando um pool de objetos com os sprites possíveis 15. Em seguida, estamos fazendo um loop e pegando cada um de nossos 15 sprites do pool de objetos e colocando-os aleatoriamente na tela. Estamos adicionando algumas informações físicas a cada sprite e dizendo que eles colidem com as bordas do nosso jogo.
Isso deve causar 15 sprites de folhas circulando pela tela.

Conclusão

Você acabou de ver como adicionar um chat em tempo real ao seu jogoFaser, em que cada mensagem de chat é armazenada no MongoDB para que possa ser acessada posteriormente por novos jogadores no jogo. As tecnologias que usamos foram Faser para o mecanismo de jogo, Socket.io para a interação em tempo real entre cliente e servidor e MongoDB para persistir nossos dados.
Mencionei anteriormente neste tutorial que havia criado um jogo diferente que usava entradas do usuário. Este artigo, intitulado Maintaining a Geolocation Specific Game Leaderboard with Phaser and MongoDB, concentrou-se na criação de tabelas de classificação para jogos. Também escrevi um tutorial intitulado Creating a Multiplayer Drawing Game with Phaser and MongoDB (Criando um jogo de desenho para vários jogadores com Phaser e MongoDB), que demonstrava a jogabilidade interativa, não apenas o bate-papo.

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Como usar expressões de agregação personalizadas no MongoDB 4.4


Sep 23, 2022 | 11 min read
Artigo

Mapeando Termos e Conceitos do SQL para o MongoDB


Oct 01, 2024 | 15 min read
Tutorial

Introdução ao MongoDB e ao AWS Codewhisperer


Sep 26, 2024 | 3 min read
Artigo

Queries que não diferenciam maiúsculas de minúsculas sem índices que não diferenciam maiúsculas de minúsculas


Oct 01, 2024 | 8 min read
Sumário