Gerencie perfis de usuário do jogo com MongoDB, Changer e JavaScript
Avalie esse Tutorial
Quando se trata de desenvolvimento de jogos, quase sempre você precisará armazenar informações sobre seu jogador. Essas informações podem ser sobre quantos pontos de saúde você tem atualmente no jogo ou podem ir além da experiência de jogo e abranger detalhes como as informações de cobrança da pessoa que está jogando. Quando falamos desse tipo de dados, estamos falando de um armazenamento de perfis de usuário.
O perfil do usuário contém tudo sobre o usuário ou jogador e não termina em pontos de saúde ou informações de cobrança.
Neste tutorial, veremos como criar perfis de usuário em um jogo que utiliza a estrutura de desenvolvimento de jogosPhaser, JavaScript e MongoDB.
Para ter uma ideia melhor do que vamos realizar, dê uma olhada na seguinte imagem animada:
Tudo bem, então não estamos fazendo um jogo muito emocionante. No entanto, muitos jogos modernos permitem que você crie seu personagem antes do início do jogo ou em algum momento à medida que avança no jogo. Essas personalizações que você adiciona ao seu personagem são armazenadas no perfil do usuário. Em nosso exemplo, personalizaremos nosso player e enviaremos as informações para nosso back-end para armazenamento no MongoDB.
Existem alguns componentes neste jogo, mas usaremos uma pilha JavaScript por toda parte. Para que este tutorial cumpra sua finalidade, você precisará do seguinte:
- Node.js 12+
- Um cluster MongoDB configurado corretamente com um banco de dados jogos e uma coleçãode perfis
Usaremos o Node.js para criar uma API de back-end que se comunicará com nosso cluster MongoDB. O jogo, que será nosso frontend, se comunicará com nosso backend do Node.js. Não entraremos em detalhes sobre como configurar e implantar uma instância MongoDB, mas você precisará de uma com banco de dados e coleção disponíveis para uso.
Se você quiser entrar em operação rapidamente com o MongoDB, considere implantar um clusterdo MongoDB Atlas M0 , que é GRÁTIS para sempre!
Para criar um jogo em Phaser, você não precisa de nada além de HTML, CSS e JavaScript básicos, portanto, não há nenhum requisito prévio para o componente do jogo.
Antes de entrarmos no código, é provavelmente uma boa ideia explorar o que o nosso perfil de usuário deve conter. Essas não são regras gerais para todos os perfis de usuário porque, muitas vezes, os jogos não são iguais.
Com base na imagem animada do início deste tutorial, sabemos que temos o seguinte JSON:
1 { 2 "_id": "nraboy", 3 "cosmetics": { 4 "eyes": "player-eyes-1", 5 "mouth": "player-mouth-1" 6 }, 7 "hp": 100, 8 "mp": 30 9 }
A profundidade do perfil acima é quase nada, mas sabemos que esse jogador em particular decidiu dar a seu personagem uma certa aparência e que esse personagem tem um conjunto específico de atributos, como pontos de saúde.
Se você quiser ver um perfil de usuário mais agressivo, confira meu tutorial anterior, que é focado em um jogo Unity que estou desenvolvendo com Adriene Tacke.
Você pode considerar adicionar uma senha com hash, posição do jogador no jogo, itens armazenados ou qualquer outro campo relevante ao seu perfil de usuário.
O que torna o MongoDB particularmente poderoso quando se trata de jogos e perfis de usuário é a flexibilidade do modelo de dados pode ser. Se lançarmos uma atualização em nosso jogo que inclua personalizações na cor do diamante, poderemos facilmente adicionar esse campo aos nossos documentos NoSQL. Adicionar e remover campos não requer migrações ou codificação especiais, o que se traduz em desenvolvimento mais rápido e código melhor.
Temos uma ideia aproximada de como deve ser nosso perfil de usuário para este jogo em particular. Agora precisamos ser capazes de adicioná-lo ao MongoDB e consultá-lo. Para fazer isso, criaremos um back-end com dois endpoints de API simples: um endpoint para criar um documento com o perfil e um endpoint para recuperá-lo.
Para este tutorial em particular, não vamos nos preocupar com autenticação. Em outras palavras, estaremos apenas criando perfis de usuário e não nos importando a quem eles realmente pertencem.
Em um novo diretório em seu computador, execute os seguintes comandos:
1 npm init -y 2 npm install cors express body-parser mongodb --save
Os comandos acima criarão um novo arquivopackage.json e baixarão as dependências que planejamos usar. Estamos usando o pacotecors porque planejamos executar nosso jogo localmente em um navegador da web. Se não gerenciarmos o compartilhamento de recursos entre origens (CORS), receberemos erros. Os pacotesExpress e body-parser serão para nossa estrutura de desenvolvimento, e o pacoteMongoDB será para interação com o MongoDB.
Dentro do mesmo projeto, crie um arquivo main.js com o seguinte código:
1 const { MongoClient, ObjectID } = require("mongodb"); 2 const Express = require("express"); 3 const BodyParser = require("body-parser"); 4 const Cors = require("cors"); 5 6 const server = Express(); 7 8 server.use(BodyParser.json()); 9 server.use(BodyParser.urlencoded({ extended: true })); 10 server.use(Cors()); 11 12 const client = new MongoClient(process.env["ATLAS_URI"]); 13 14 var collection; 15 16 server.post("/profile", async (request, response, next) => {}); 17 server.get("/profile/:username", async (request, response, next) => {}); 18 19 server.listen("3000", async () => { 20 try { 21 await client.connect(); 22 collection = client.db("gamedev").collection("profiles"); 23 console.log("Listening at :3000..."); 24 } catch (e) { 25 console.error(e); 26 } 27 });
O núcleo do que está acontecer no código acima é que estamos lançando a base para nosso aplicativo Express Framework importando as dependências e inicializando a estrutura. Quando começam a escutar conexões, nós nos conectamos ao MongoDB e definimos qual banco de dados e collection queremos usar. Neste exemplo, estamos usando um banco de dados
gamedev
e uma collectionprofiles
. Você pode usar o que fizer sentido para você.As informações reais de conexão do MongoDB são definidas com uma variável de ambiente
ATLAS_URI
. Você poderia facilmente usar um arquivo de configuração ou codificar esse valor, mas as variáveis de ambiente tendem a funcionar melhor por motivos de segurança e implantação.A string de conexão a ser usada seria mais ou menos assim:
1 mongodb+srv://<username>:<password>@plummeting-us-east-1.hrrxc.mongodb.net/<dbname>
Você pode coletar as informações do painel do MongoDB Atlas depois de criar as credenciais de usuário apropriadas para seu banco de dados e coleção.
Quando adiciono uma variável de ambiente, geralmente faço algo como o seguinte:
1 export ATLAS_URI="mongodb+srv://<username>:<password>@plummeting-us-east-1.hrrxc.mongodb.net/<dbname>"
Existem várias maneiras de fazer isso e ela pode variar dependendo do seu sistema operacional.
Então, vamos nos concentrar nesses endpoints.
Quando se trata de criar um perfil de usuário no MongoDB, podemos ter um endpoint parecido com o seguinte:
1 server.post("/profile", async (request, response, next) => { 2 try { 3 let result = await collection.insertOne(request.body); 4 response.send(result); 5 } catch (e) { 6 response.status(500).send({ message: e.message }); 7 } 8 });
O endpoint acima é provavelmente o endpoint mais básico que você pode criar. Estamos aceitando uma carga útil do front-end e a inserindo no banco de dados. Em seguida, estamos retornando a resposta que o banco de dados nos dá.
Em um jogo de produção, você provavelmente vai querer validar seus dados para evitar cola ou outras atividades maliciosas. Você pode pesquisar a validação de esquema com o MongoDB ou usar um pacote voltado para o cliente como o joi.
Com os dados entrarem no MongoDB, agora podemos criar nosso endpoint para recuperá-los:
1 server.get("/profile/:username", async (request, response, next) => { 2 try { 3 let result = await collection.findOne({ "_id": request.params.username }); 4 response.send(result); 5 } catch (e) { 6 response.status(500).send({ message: e.message }); 7 } 8 });
Depois de fornecer a esse endpoint um nome de usuário, que em nosso exemplo é o campo
_id
, o usamos para encontrar um documento específico. Esse documento é devolvido ao jogo ou ao que quer que tenha tentado recuperá-lo.Novamente, não estamos usando nenhuma validação ou autenticação de dados para este tutorial, pois isso está fora do escopo do que queremos realizar.
Agora temos uma API para trabalhar.
Com a API implementada, podemos nos concentrar no jogo em si. Lembre-se de que a profundidade do nosso jogo é bem rasa, mas ele oferece um recurso que muitos jogos oferecem. Esse recurso é personalizar o personagem.
Não vou compartilhar meus recursos gráficos porque quero que você use um pouco de imaginação para criar os seus próprios. Você vai criar algo melhor? Talvez, mas é por sua conta!
Crie um arquivoindex.html no seu computador com a seguinte marcação HTML:
1 2 <html> 3 <body> 4 <div id="game"></div> 5 <script src="//cdn.jsdelivr.net/npm/phaser@3.51.0/dist/phaser.min.js"></script> 6 <script> 7 8 const phaserConfig = { 9 type: Phaser.AUTO, 10 parent: "game", 11 width: 1280, 12 height: 720, 13 scene: { 14 init: initScene, 15 preload: preloadScene, 16 create: createScene 17 } 18 }; 19 20 const game = new Phaser.Game(phaserConfig); 21 22 function initScene() {} 23 function preloadScene() {} 24 function createScene() {} 25 26 </script> 27 </body> 28 </html>
A marcação acima é padrão do Phaser. Definimos o container onde o jogo deve ser carregado, importamos a dependência JavaScript e inicializamos o jogo enquanto dissemos para ele usar o container.
A verdadeira mágica vai acontecer nas funções do ciclo de vida
scene
. A funçãoinitScene
é onde inicializaremos nossas variáveis, a funçãopreloadScene
é onde carregaremos nossos ativos de mídia e a funçãocreateScene
é onde faremos nossa primeira renderização.Começando com a
initScene
função, adicione o seguinte:1 function initScene() { 2 3 this.player = { 4 "_id": "nraboy", 5 "cosmetics": { 6 "eyes": "", 7 "mouth": "" 8 }, 9 "hp": 100, 10 "mp": 30 11 } 12 this.eyes = []; 13 this.mouth = []; 14 15 }
Para nosso exemplo, o objeto
player
representará nosso objeto de perfil de usuário armazenado localmente. Ele será exatamente igual ao que você encontraria armazenado no banco de dados. Esse objeto pode ser tão simples ou complexo quanto você quiser, dependendo das necessidades do jogo. O campo_id
deve ser exclusivo, portanto, se você preferir deixar o MongoDB gerá-lo, remova o campo e crie um username
ou um campo semelhante. Só depende de você.As arrays
eyes
e mouth
representarão todas as opções possíveis para customização de caracteres. A personalização escolhida será armazenada no perfil do usuário.O próximo passo é examinar a função
preloadScene
:1 function preloadScene() { 2 3 this.load.image("background", "game-background.png"); 4 this.load.image("player-base", "player-base.png"); 5 this.load.image("player-eyes-1", "player-eyes-1.png"); 6 this.load.image("player-eyes-2", "player-eyes-2.png"); 7 this.load.image("player-mouth-1", "player-mouth-1.png"); 8 this.load.image("player-mouth-2", "player-mouth-2.png"); 9 10 }
Na função acima, estamos carregando nossos arquivos de imagem e dando a eles um nome de referência para ser usado ao longo do jogo. No exemplo acima, o nome de referência é um nome bem próximo ao nome do arquivo em si, mas não precisa ser.
Não forneci os ativos da imagem, então fique à vontade para ser ousado.
A mágica acontece na função
createScene
:1 function createScene() { 2 3 this.add.image(640, 360, "background"); 4 this.add.image(800, 250, "player-base"); 5 this.eyesActive = this.add.image(800, 250, "player-eyes-1"); 6 this.mouthActive = this.add.image(800, 250, "player-mouth-1"); 7 8 for (let i = 1; i <= 2; i++) { 9 this.eyes.push( 10 this.add.image(75, 90 * i, "player-eyes-" + i) 11 .setCrop(150, 115, 100, 50) 12 .setScale(0.5) 13 .setInteractive() 14 .on("pointerdown", () => { 15 this.eyesActive.setTexture("player-eyes-" + i); 16 }) 17 ); 18 this.eyes[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 115, 100, 50); 19 } 20 21 for (let i = 1; i <= 2; i++) { 22 this.mouth.push( 23 this.add.image(215, 90 * i - 40, "player-mouth-" + i) 24 .setCrop(150, 195, 100, 50) 25 .setScale(0.5) 26 .setInteractive() 27 .on("pointerdown", () => { 28 this.mouthActive.setTexture("player-mouth-" + i); 29 }) 30 ); 31 this.mouth[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 195, 100, 50); 32 } 33 34 this.add.text(289, 525, "PROFILE:", { fontSize: 36, color: "#FFFFFF", fontStyle: "bold" }) 35 this.profileData = this.add.text(289, 575, "", { fontSize: 16, color: "#FFFFFF" }) 36 37 this.startButton = this.add.text(1125, 640, "START", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 38 this.startButton.setInteractive(); 39 this.startButton.on("pointerdown", () => { 40 this.player.cosmetics.mouth = this.mouthActive.texture.key; 41 this.player.cosmetics.eyes = this.eyesActive.texture.key; 42 fetch("http://localhost:3000/profile", { 43 "method": "POST", 44 "headers": { 45 "content-type": "application/json" 46 }, 47 "body": JSON.stringify(this.player) 48 }) 49 .then(response => response.json()) 50 .then(response => { 51 this.profileData.setText(JSON.stringify(this.player)); 52 }, error => { 53 console.error(error); 54 }); 55 }, this); 56 57 }
Permitam-me começar por dizer algumas coisas primeiro.
- O jogo no meu exemplo é 1280x720 e os ativos de imagem refletem isso.
- A imagem base do personagem e qualquer imagem de anexo cosmético têm a mesma resolução.
Em vez de criar imagens de anexos cosméticos de tamanho perfeito, no meu exemplo, apenas removi a imagem base, mas deixei as imagens de anexos do mesmo tamanho, mas com transparência. Tentar descobrir como posicionar imagens pequenas pode se tornar mais trabalhoso do que eu queria. A disposição das imagens funcionou melhor.
Com isso dito, temos o seguinte:
1 this.add.image(640, 360, "background"); 2 this.add.image(800, 250, "player-base"); 3 this.eyesActive = this.add.image(800, 250, "player-eyes-1"); 4 this.mouthActive = this.add.image(800, 250, "player-mouth-1");
As imagens de inicialização são colocadas na tela em posições variáveis. O
eyesActive
e mouthActive
representam os anexos iniciais do caractere. Estamos definindo isso apenas como uma variável porque planejamos alterá-las mais tarde.Com base na convenção de nomenclatura das imagens e nos tamanhos que escolhemos usar, podemos exibir as opções de anexo para
eyes
na tela:1 for (let i = 1; i <= 2; i++) { 2 this.eyes.push( 3 this.add.image(75, 90 * i, "player-eyes-" + i) 4 .setCrop(150, 115, 100, 50) 5 .setScale(0.5) 6 .setInteractive() 7 .on("pointerdown", () => { 8 this.eyesActive.setTexture("player-eyes-" + i); 9 }) 10 ); 11 this.eyes[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 115, 100, 50); 12 }
Neste exemplo, temos duas possibilidades diferentes para os olhos. Ao inseri-los em nossa matriz, estamos especificando a posição, mas também estamos mudando algumas coisas em relação à imagem. Por exemplo, sabemos que as imagens dos anexos são em sua maioria transparentes. Podemos cortá-las e escalá-las para mostrar apenas o que queremos. Isso é apenas para fins de seleção, não sendo exibido no personagem real.
Também queremos responder a eventos de clique. Quando um evento de clique acontece, queremos que a imagem apropriada seja usada como a imagem ativa. No entanto, como dimensionamos e recortamos nossa imagem, precisamos redefinir o
hitArea
, que é a área que deve ser clicável na imagem.A execução da maioria dessas etapas só é necessária se você estiver usando imagens do mesmo tamanho e planejando colocá-las em camadas, como eu.
Podemos reproduzir nossas etapas para as opções de boca:
1 for (let i = 1; i <= 2; i++) { 2 this.mouth.push( 3 this.add.image(215, 90 * i - 40, "player-mouth-" + i) 4 .setCrop(150, 195, 100, 50) 5 .setScale(0.5) 6 .setInteractive() 7 .on("pointerdown", () => { 8 this.mouthActive.setTexture("player-mouth-" + i); 9 }) 10 ); 11 this.mouth[i - 1].input.hitArea = new Phaser.Geom.Rectangle(150, 195, 100, 50); 12 }
As etapas são as mesmas, mas estamos usando posições diferentes.
Lembre-se de que essas duas etapas são para definir as miniaturas de nossas imagens e especificar uma região de clique para elas. Enquanto a imagem ativa é o que mostramos no personagem, o lead up está relacionado às miniaturas.
Com as miniaturas fora do caminho, podemos adicionar um texto de espaço reservado sobre o perfil que queremos enviar:
1 this.add.text(289, 525, "PROFILE:", { fontSize: 36, color: "#FFFFFF", fontStyle: "bold" }) 2 this.profileData = this.add.text(289, 575, "", { fontSize: 16, color: "#FFFFFF" })
O objetivo com este texto é apenas mostrar como é o nosso objeto
player
na tela. Isso nos dará uma boa ideia do que foi enviado para nosso MongoDB database.Por fim, temos o botão que enviará os dados para nossa API:
1 this.startButton = this.add.text(1125, 640, "START", { fontSize: 40, color: '#000000', fontStyle: "bold", backgroundColor: "#FFFFFF", padding: 10 }); 2 this.startButton.setInteractive(); 3 this.startButton.on("pointerdown", () => { 4 this.player.cosmetics.mouth = this.mouthActive.texture.key; 5 this.player.cosmetics.eyes = this.eyesActive.texture.key; 6 fetch("http://localhost:3000/profile", { 7 "method": "POST", 8 "headers": { 9 "content-type": "application/json" 10 }, 11 "body": JSON.stringify(this.player) 12 }) 13 .then(response => response.json()) 14 .then(response => { 15 this.profileData.setText(JSON.stringify(this.player)); 16 }, error => { 17 console.error(error); 18 }); 19 }, this);
Quando o botão é clicado, a referência da imagem é adicionada ao objeto
player
. Usando uma operaçãofetch
, podemos enviar o objetoplayer
para nosso backend, que o armazenará no MongoDB. Como não estamos fazendo nenhuma validação de dados, isso será bem-sucedido desde que o campo_id
seja exclusivo.Se você estiver usando o MongoDB Atlas e examinar o gerenciador de dados, os documentos devem ser semelhantes à imagem acima.
Você acabou de ver como armazenar perfis de usuário para um jogo Typer como documentos NoSQL no MongoDB. Dependendo do jogo que você está criando, os documentos de perfil do usuário podem ser significativamente mais complexos em comparação com o que foi visto neste exemplo. No final das contas, você está armazenando informações sobre a sua experiência de jogo e essas informações podem incluir informações sobre o usuário, como senha, informações de cobrança ou similares, e você também está armazenando informações sobre o jogo, que podem incluir personalizações de roupas, inventário de itens, localização do jogador ou qualquer outra coisa relacionada ao jogo.
Se você estiver interessado em ver mais exemplos de Phaser com MongoDB, confira alguns de meus tutoriais anteriores sobre o assunto. Se você é um desenvolvedor Unity, também tenho algum conteúdo sobre esse assunto.
Tem dúvidas sobre este tutorial ou sobre o MongoDB? Visite os Fóruns da MongoDB Community e conecte-se conosco!