Lançado em 2015, o
Agar.io se tornou o progenitor do novo gênero de
games .io , cuja popularidade desde então cresceu significativamente. O crescimento da popularidade dos jogos .io que experimentei: nos últimos três anos,
criei e vendi dois jogos desse gênero. .
Caso você nunca tenha ouvido falar desses jogos antes: estes são jogos on-line gratuitos para vários jogadores, fáceis de participar (sem necessidade de conta). Geralmente eles empurram muitos jogadores adversários na mesma arena. Outros jogos famosos do gênero .io são:
Slither.io e
Diep.io.Neste post, descobriremos como
criar um jogo .io a partir do zero . Para isso, apenas o conhecimento de Javascript será suficiente: você precisa entender coisas como a sintaxe
ES6 , a
this
e
Promises . Mesmo se você não souber o Javascript perfeitamente, ainda poderá descobrir a maior parte do post.
Exemplo de jogo .io
Para ajudar você a aprender, vamos nos referir a
um exemplo de jogo .io . Tente jogar!
O jogo é bastante simples: você controla um navio em uma arena onde existem outros jogadores. Seu navio dispara conchas automaticamente e você tenta acertar outros jogadores, evitando as conchas deles.
1. Visão geral / estrutura do projeto
Eu recomendo baixar o código fonte de um jogo de exemplo para que você possa me seguir.
O exemplo usa o seguinte:
- Express é a estrutura da web mais popular do Node.js que gerencia o servidor da web do jogo.
- socket.io - uma biblioteca de websocket para troca de dados entre um navegador e um servidor.
- Webpack é um gerenciador de módulos. Você pode ler sobre por que usar o Webpack aqui .
Aqui está a aparência da estrutura de diretórios do projeto:
public/ assets/ ... src/ client/ css/ ... html/ index.html index.js ... server/ server.js ... shared/ constants.js
público /
Tudo no
public/
pasta será transmitido estaticamente pelo servidor.
public/assets/
contém as imagens usadas pelo nosso projeto.
src /
Todo o código fonte está na pasta
src/
. Os nomes
client/
e
server/
falam por si e
shared/
contém um arquivo constante importado pelo cliente e pelo servidor.
2. Montagens / parâmetros do projeto
Como afirmado acima, usamos o
gerenciador de módulo
Webpack para construir o projeto. Vamos dar uma olhada na nossa configuração do Webpack:
webpack.common.js:
const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { entry: { game: './src/client/index.js', }, output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ['@babel/preset-env'], }, }, }, { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, }, 'css-loader', ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', }), new HtmlWebpackPlugin({ filename: 'index.html', template: 'src/client/html/index.html', }), ], };
O mais importante aqui são as seguintes linhas:
src/client/index.js
é o ponto de entrada para o cliente Javascript (JS). O Webpack começará a partir daqui e procurará recursivamente outros arquivos importados.- O JS de saída do nosso assembly Webpack estará localizado no diretório
dist/
. Vou chamar esse arquivo de pacote JS . - Usamos o Babel e, em particular, a configuração @ babel / preset-env para transpilar nosso código JS para navegadores mais antigos.
- Usamos o plug-in para extrair todo o CSS referenciado pelos arquivos JS e combiná-los em um só lugar. Vou chamá-lo de nosso pacote CSS .
Você deve ter notado nomes estranhos de arquivos de pacotes
'[name].[contenthash].ext'
. Eles contêm a
substituição dos nomes dos arquivos Webpack:
[name]
será substituído pelo nome do ponto de entrada (no nosso caso, este é um
game
) e
[contenthash]
será substituído por um hash do conteúdo do arquivo. Fazemos isso para
otimizar o projeto para o hash - você pode dizer aos navegadores para armazenar em cache nossos pacotes JS indefinidamente, porque
se o pacote for alterado, o nome do arquivo será alterado (o
contenthash
alterado). O resultado final será um nome de arquivo no formato
game.dbeee76e91a97d0c7207.js
.
O arquivo
webpack.common.js
é o arquivo de configuração base que importamos nas configurações de desenvolvimento e projeto finalizado. Por exemplo, uma configuração de desenvolvimento:
webpack.dev.js
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', });
Para eficiência, usamos o
webpack.dev.js
no
webpack.dev.js
desenvolvimento e
webpack.prod.js
para o
webpack.prod.js
para otimizar o tamanho dos pacotes quando implementados na produção.
Configuração local
Eu recomendo instalar o projeto em uma máquina local para que você possa seguir as etapas listadas nesta postagem. A configuração é simples: primeiro, o
Node e o
NPM devem estar instalados no sistema. Em seguida, você precisa executar
$ git clone https:
e você está pronto para ir! Para iniciar o servidor de desenvolvimento, basta executar
$ npm run develop
e acesse o
localhost: 3000 em um navegador da web. O servidor de desenvolvimento reconstruirá automaticamente os pacotes JS e CSS conforme o código for alterado - basta atualizar a página para ver todas as alterações!
3. Pontos de entrada do cliente
Vamos ao código do jogo em si. Primeiro, precisamos da página
index.html
. Quando você visita o site, o navegador o carrega primeiro. Nossa página será bem simples:
index.html
<! DOCTYPE html>
<html>
<head>
<title> Um exemplo de jogo .io </title>
<tipo de link = "text / css" rel = "folha de estilo" href = "/ game.bundle.css">
</head>
<body>
<canvas id = "game-canvas"> </canvas>
<script async src = "/ game.bundle.js"> </script>
<div id = "play-menu" class = "oculto">
<input type = "text" id = "nome do usuário-entrada" espaço reservado = "Nome do Usuário" />
<button id = "play-button"> JOGAR </button>
</div>
</body>
</html>
Este exemplo de código é um pouco simplificado para maior clareza, farei o mesmo com muitos outros exemplos da postagem. O código completo sempre pode ser visualizado no Github .Nós temos:
<canvas>
Canvas HTML5 ( <canvas>
) que usaremos para renderizar o jogo.<link>
para adicionar nosso pacote CSS.<script>
para adicionar nosso pacote Javascript.- O menu principal com o nome de usuário
<input>
e o <button>
"PLAY" ( <button>
).
Depois de carregar a página inicial, o código Javascript começará a ser executado no navegador, começando com o arquivo JS do ponto de entrada:
src/client/index.js
.
index.js
import { connect, play } from './networking'; import { startRendering, stopRendering } from './render'; import { startCapturingInput, stopCapturingInput } from './input'; import { downloadAssets } from './assets'; import { initState } from './state'; import { setLeaderboardHidden } from './leaderboard'; import './css/main.css'; const playMenu = document.getElementById('play-menu'); const playButton = document.getElementById('play-button'); const usernameInput = document.getElementById('username-input'); Promise.all([ connect(), downloadAssets(), ]).then(() => { playMenu.classList.remove('hidden'); usernameInput.focus(); playButton.onclick = () => {
Isso pode parecer complicado, mas na realidade não há muitas ações acontecendo aqui:
- Importe vários outros arquivos JS.
- Importe CSS (para que o Webpack saiba incluí-los em nosso pacote CSS).
- Execute
connect()
para estabelecer uma conexão com o servidor e execute downloadAssets()
para baixar as imagens necessárias para renderizar o jogo. - Após concluir a etapa 3 , o menu principal (
playMenu
) é playMenu
. - Configurando o manipulador para pressionar o botão PLAY. Quando o botão é pressionado, o código inicializa o jogo e informa ao servidor que estamos prontos para jogar.
A principal "carne" da nossa lógica cliente-servidor está nos arquivos que foram importados pelo arquivo
index.js
. Agora vamos considerá-los todos em ordem.
4. Troca de dados do cliente
Neste jogo, usamos a conhecida biblioteca
socket.io para se comunicar com o servidor. O Socket.io possui suporte interno para
WebSockets , que são adequados para comunicação bidirecional: podemos enviar mensagens para o servidor
e o servidor pode nos enviar mensagens pela mesma conexão.
Teremos um arquivo
src/client/networking.js
que tratará de
todas as comunicações com o servidor:
networking.js
import io from 'socket.io-client'; import { processGameUpdate } from './state'; const Constants = require('../shared/constants'); const socket = io(`ws://${window.location.host}`); const connectedPromise = new Promise(resolve => { socket.on('connect', () => { console.log('Connected to server!'); resolve(); }); }); export const connect = onGameOver => ( connectedPromise.then(() => {
Este código também é um pouco reduzido para maior clareza.Existem três ações principais neste arquivo:
- Estamos tentando conectar ao servidor.
connectedPromise
é permitido apenas quando estabelecemos uma conexão. - Se a conexão for estabelecida com sucesso, registramos funções de retorno de chamada (
processGameUpdate()
e onGameOver()
) para mensagens que podemos receber do servidor. - Exportamos
play()
e updateDirection()
para que outros arquivos possam usá-los.
5. Renderização do cliente
É hora de exibir uma imagem na tela!
... mas antes que possamos fazer isso, precisamos fazer o download de todas as imagens (recursos) necessárias para isso. Vamos escrever um gerenciador de recursos:
assets.js
const ASSET_NAMES = ['ship.svg', 'bullet.svg']; const assets = {}; const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset)); function downloadAsset(assetName) { return new Promise(resolve => { const asset = new Image(); asset.onload = () => { console.log(`Downloaded ${assetName}`); assets[assetName] = asset; resolve(); }; asset.src = `/assets/${assetName}`; }); } export const downloadAssets = () => downloadPromise; export const getAsset = assetName => assets[assetName];
O gerenciamento de recursos não é tão difícil de implementar! O ponto principal é armazenar o objeto de
assets
, que vinculará a chave do nome do arquivo ao valor do objeto
Image
. Quando o recurso é carregado, nós o salvamos no objeto de
assets
para recuperação rápida no futuro. Quando o download for permitido para cada recurso individual (ou seja,
todos os recursos serão baixados), ativamos o
downloadPromise
.
Depois de baixar os recursos, você pode começar a renderizar. Como afirmado anteriormente, usamos o
HTML5 Canvas (
<canvas>
) para desenhar na página da web. Nosso jogo é bastante simples, então precisamos desenhar o seguinte:
- Antecedentes
- Navio do jogador
- Outros jogadores no jogo
- Conchas
Aqui estão os trechos importantes de
src/client/render.js
que renderizam exatamente os quatro pontos listados acima:
render.js
import { getAsset } from './assets'; import { getCurrentState } from './state'; const Constants = require('../shared/constants'); const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;
Este código também é reduzido para maior clareza.render()
é a principal função deste arquivo.
startRendering()
e
stopRendering()
controlam a ativação do ciclo de renderização a 60 FPS.
As implementações específicas das funções individuais de renderização auxiliar (por exemplo,
renderBullet()
) não são tão importantes, mas aqui está um exemplo simples:
render.js
function renderBullet(me, bullet) { const { x, y } = bullet; context.drawImage( getAsset('bullet.svg'), canvas.width / 2 + x - me.x - BULLET_RADIUS, canvas.height / 2 + y - me.y - BULLET_RADIUS, BULLET_RADIUS * 2, BULLET_RADIUS * 2, ); }
Observe que usamos o método
getAsset()
que foi visto anteriormente em
asset.js
!
Se você estiver interessado em explorar outras funções auxiliares de renderização, leia o restante do src / client / render.js .
6. Entrada do cliente
É hora de tornar o jogo
jogável ! O esquema de controle será muito simples: você pode usar o mouse (no computador) ou tocar na tela (no dispositivo móvel) para alterar a direção do movimento. Para implementar isso, registraremos
Listeners de Eventos para eventos Mouse e Touch.
src/client/input.js
fará tudo isso:
input.js
import { updateDirection } from './networking'; function onMouseInput(e) { handleInput(e.clientX, e.clientY); } function onTouchInput(e) { const touch = e.touches[0]; handleInput(touch.clientX, touch.clientY); } function handleInput(x, y) { const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y); updateDirection(dir); } export function startCapturingInput() { window.addEventListener('mousemove', onMouseInput); window.addEventListener('touchmove', onTouchInput); } export function stopCapturingInput() { window.removeEventListener('mousemove', onMouseInput); window.removeEventListener('touchmove', onTouchInput); }
onMouseInput()
e
onTouchInput()
são ouvintes de eventos que chamam
updateDirection()
(de
networking.js
) quando ocorre um evento de entrada (por exemplo, ao mover o mouse).
updateDirection()
está envolvido em mensagens com um servidor que processa o evento de entrada e atualiza o estado do jogo de acordo.
7. Condição do cliente
Esta seção é a mais difícil na primeira parte da postagem. Não desanime se você não o entender desde a primeira leitura! Você pode até pular e voltar mais tarde.
A última peça do quebra-cabeça necessária para concluir o código cliente-servidor é
state . Lembra do snippet de código da seção Renderização de cliente?
render.js
import { getCurrentState } from './state'; function render() { const { me, others, bullets } = getCurrentState();
getCurrentState()
deve poder nos fornecer o estado atual do jogo no cliente
a qualquer momento, com base nas atualizações recebidas do servidor. Aqui está um exemplo de uma atualização de jogo que um servidor pode enviar:
{ "t": 1555960373725, "me": { "x": 2213.8050880413657, "y": 1469.370893425012, "direction": 1.3082443894581433, "id": "AhzgAtklgo2FJvwWAADO", "hp": 100 }, "others": [], "bullets": [ { "id": "RUJfJ8Y18n", "x": 2354.029197099604, "y": 1431.6848318262666 }, { "id": "ctg5rht5s", "x": 2260.546457727445, "y": 1456.8088728920968 } ], "leaderboard": [ { "username": "Player", "score": 3 } ] }
Cada atualização do jogo contém cinco campos idênticos:
- t : carimbo de data e hora do servidor para o momento em que esta atualização foi criada.
- eu : informações sobre o jogador que está recebendo esta atualização.
- outros : um conjunto de informações sobre outros jogadores que participam do mesmo jogo.
- balas : um conjunto de informações sobre as conchas do jogo.
- tabela de classificação : dados atuais da tabela de classificação. Neste post, não os levaremos em consideração.
7.1 O estado ingênuo do cliente
A implementação ingênua de
getCurrentState()
só pode retornar dados diretamente da atualização de jogo mais recente recebida.
naive-state.js
let lastGameUpdate = null;
Bonito e claro! Mas se tudo fosse tão simples. Um dos motivos pelos quais essa implementação é problemática:
limita a taxa de quadros da renderização à frequência do relógio do servidor .
Taxa de quadros: o número de quadros (ou seja, chamadas de render()
) por segundo ou FPS. Os jogos geralmente tendem a atingir pelo menos 60 FPS.
Tick Rate : A frequência com que o servidor envia atualizações de jogos para os clientes. Muitas vezes, é menor que a taxa de quadros . Em nosso jogo, o servidor roda a uma frequência de 30 ciclos por segundo.
Se apenas renderizarmos a atualização mais recente do jogo, o FPS nunca poderá exceder 30, porque
nunca receberemos mais de 30 atualizações por segundo do servidor . Mesmo se chamarmos
render()
60 vezes por segundo, metade dessas chamadas simplesmente redesenhará a mesma coisa, essencialmente sem fazer nada. Outro problema da implementação ingênua é que é
propenso a atrasos . Com uma velocidade ideal da Internet, o cliente receberá uma atualização do jogo exatamente a cada 33 ms (30 por segundo):
Infelizmente, nada é perfeito. Uma imagem mais realista seria:
Uma implementação ingênua é praticamente o pior caso de atraso. Se a atualização do jogo for recebida com um atraso de 50 ms, o
cliente ficará mais lento por mais 50 ms, porque ainda renderiza o estado do jogo da atualização anterior. Você pode imaginar o quão inconveniente é para um jogador: devido à frenagem arbitrária, o jogo parecerá instável e instável.
7.2 Status aprimorado do cliente
Faremos algumas melhorias na implementação ingênua. Primeiro, usamos
um atraso de renderização de 100 ms. Isso significa que o estado "atual" do cliente sempre ficará atrás do estado do jogo no servidor em 100 ms. Por exemplo, se o horário no servidor for
150 , o estado em que o servidor estava no horário
50 será renderizado no cliente:
Isso nos fornece um buffer de 100 ms, permitindo sobreviver ao tempo imprevisível para receber atualizações de jogos:
Pagar por isso será um
atraso de entrada constante
(atraso de entrada) de 100 ms. Este é um pequeno sacrifício pela jogabilidade suave - a maioria dos jogadores (especialmente os casuais) nem notam esse atraso. É muito mais fácil para as pessoas se adaptarem a um atraso constante de 100 ms do que jogar com um atraso imprevisível.
Podemos usar outra técnica chamada “previsão do lado do cliente”, que faz um bom trabalho na redução de atrasos percebidos, mas isso não será considerado nesta postagem.
Outra melhoria que usamos é
a interpolação linear . Devido a atrasos na renderização, geralmente ultrapassamos o tempo atual no cliente por pelo menos uma atualização. Quando
getCurrentState()
é chamado, podemos executar uma
interpolação linear entre as atualizações do jogo imediatamente antes e depois da hora atual no cliente:
Isso resolve o problema com a taxa de quadros: agora podemos renderizar quadros únicos com a frequência necessária!
7.3 Implementando o status aprimorado do cliente
Um exemplo de implementação em
src/client/state.js
usa atraso de renderização e interpolação linear, mas isso não é por muito tempo. Vamos dividir o código em duas partes. Aqui está o primeiro:
state.js parte 1
const RENDER_DELAY = 100; const gameUpdates = []; let gameStart = 0; let firstServerTimestamp = 0; export function initState() { gameStart = 0; firstServerTimestamp = 0; } export function processGameUpdate(update) { if (!firstServerTimestamp) { firstServerTimestamp = update.t; gameStart = Date.now(); } gameUpdates.push(update);
O primeiro passo é descobrir o que o
currentServerTime()
faz. Como vimos anteriormente, um registro de data e hora do servidor é incluído em todas as atualizações do jogo. Queremos usar o atraso de renderização para renderizar a imagem 100 ms atrás do servidor, mas
nunca saberemos a hora atual no servidor , porque não podemos saber quanto tempo uma das atualizações chegou até nós. A Internet é imprevisível e sua velocidade pode variar bastante!
Para contornar esse problema, você pode usar uma aproximação razoável:
fingiremos que a primeira atualização chegou instantaneamente . Se isso fosse verdade, saberíamos a hora do servidor nesse momento específico!
firstServerTimestamp
o registro de data e hora do servidor no
firstServerTimestamp
e salvamos o registro de data e hora
local (cliente) ao mesmo tempo no
gameStart
.
Oh espera.
Não deveria haver tempo no servidor = tempo no cliente? Por que distinguimos entre "registro de data e hora do servidor" e "registro de data e hora do cliente"? Esta é uma ótima pergunta! Acontece que isso não é a mesma coisa.
Date.now()
retornará diferentes registros de data e hora no cliente e no servidor, e isso depende de fatores locais para essas máquinas.
Nunca assuma que os carimbos de data e hora são iguais em todas as máquinas.Agora entendemos o que
currentServerTime()
faz: retorna o
carimbo de data e hora do servidor para o tempo de renderização atual . , (
firstServerTimestamp <+ (Date.now() - gameStart)
) (
RENDER_DELAY
).
, .
processGameUpdate()
,
gameUpdates
. ,
, .
« »?
, , . ?
«Client Render Time» .
? ? ,
- getCurrentState()
:
state.js, 2
export function getCurrentState() { if (!firstServerTimestamp) { return {}; } const base = getBaseUpdate(); const serverTime = currentServerTime();
:
base < 0
, (. getBaseUpdate()
). - . .base
— , . - . , .- , , !
,
state.js
— , ( ) . ,
state.js
Github .
2. -
Node.js,
.io .
1.
- - Node.js
Express .
src/server/server.js
:
server.js, 1
const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackConfig = require('../../webpack.dev.js');
, Webpack? Webpack. :
server.js
socket.io , Express:
server.js, 2
const socketio = require('socket.io'); const Constants = require('../shared/constants');
socket.io . -
game
:
server.js, 3
const Game = require('./game');
Criamos um jogo do gênero .io, por isso precisamos de apenas uma instância Game
("Jogo") - todos os jogadores jogam na mesma arena! Na próxima seção, veremos como essa classe funciona Game
.2. servidor de jogo
A classe Game
contém a lógica mais importante do lado do servidor. Ele tem duas tarefas principais: gerenciamento de jogadores e simulação de jogos .Vamos começar com a primeira tarefa - com o gerenciamento de jogadores.game.js, parte 1
const Constants = require('../shared/constants'); const Player = require('./player'); class Game { constructor() { this.sockets = {}; this.players = {}; this.bullets = []; this.lastUpdateTime = Date.now(); this.shouldSendUpdate = false; setInterval(this.update.bind(this), 1000 / 60); } addPlayer(socket, username) { this.sockets[socket.id] = socket;
id
socket.io ( ,
server.js
). Socket.io
id
, .
ID .
,
Game
:
sockets
— , ID , . ID .players
— , ID code>Player
bullets
É uma matriz de objetos Bullet
que não possui uma ordem específica.lastUpdateTime
- Este é o registro de data e hora da última atualização do jogo. Em breve veremos como é usado.shouldSendUpdate
É uma variável auxiliar. Também veremos seu uso em breve.Métodos addPlayer()
, removePlayer()
e handleInput()
não há necessidade de explicar, eles são usados no server.js
. Se você precisar atualizar sua memória, volte um pouco mais.A última linha constructor()
inicia o ciclo de atualização do jogo (com uma frequência de 60 atualizações / s):game.js, parte 2
const Constants = require('../shared/constants'); const applyCollisions = require('./collisions'); class Game {
O método update()
provavelmente contém a parte mais importante da lógica do lado do servidor. Em ordem, listamos tudo o que ele faz:- Calcula quanto tempo
dt
se passou desde o último update()
. - . . ,
bullet.update()
true
, ( ). - . —
player.update()
Bullet
. applyCollisions()
, , . , ( player.onDealtDamage()
), bullets
.- .
update()
. shouldSendUpdate
. update()
60 /, 30 /. , 30 / ( ).
? . 30 – !
Por que simplesmente não ligar update()
30 vezes por segundo? Para melhorar a simulação do jogo. Quanto mais frequentemente chamado update()
, mais precisa será a simulação do jogo. Mas não se empolgue com o número de chamadas update()
, porque é uma tarefa computacionalmente cara - 60 por segundo é suficiente.
O restante da classe Game
consiste em métodos auxiliares usados em update()
:game.js, parte 3
class Game {
getLeaderboard()
– , .
createUpdate()
update()
, .
serializeForUpdate()
,
Player
Bullet
. ,
– , !
3.
: . ,
Object
:
object.js
class Object { constructor(id, x, y, dir, speed) { this.id = id; this.x = x; this.y = y; this.direction = dir; this.speed = speed; } update(dt) { this.x += dt * this.speed * Math.sin(this.direction); this.y -= dt * this.speed * Math.cos(this.direction); } distanceTo(object) { const dx = this.x - object.x; const dy = this.y - object.y; return Math.sqrt(dx * dx + dy * dy); } setDirection(dir) { this.direction = dir; } serializeForUpdate() { return { id: this.id, x: this.x, y: this.y, }; } }
. . ,
Bullet
Object
:
bullet.js
const shortid = require('shortid'); const ObjectClass = require('./object'); const Constants = require('../shared/constants'); class Bullet extends ObjectClass { constructor(parentID, x, y, dir) { super(shortid(), x, y, dir, Constants.BULLET_SPEED); this.parentID = parentID; }
Bullet
!
Object
:
- shortid
id
. parentID
, , .update()
, true
, (, ?).
Player
:
player.js
const ObjectClass = require('./object'); const Bullet = require('./bullet'); const Constants = require('../shared/constants'); class Player extends ObjectClass { constructor(id, username, x, y) { super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED); this.username = username; this.hp = Constants.PLAYER_MAX_HP; this.fireCooldown = 0; this.score = 0; }
, , .
update()
, , ,
fireCooldown
(, ?).
serializeForUpdate()
, .
Object
— , . ,
Object
distanceTo()
, .
,
Object
.
4.
, – , !
update()
Game
:
game.js
const applyCollisions = require('./collisions'); class Game {
applyCollisions()
, , . , ,
- , .
distanceTo()
, Object
.
:
collisions.js
const Constants = require('../shared/constants');
,
, . , :
:
- . ,
bullet.parentID
player.id
. - .
break
: , , .
! , - .io. O que vem a seguir?
.io!Github .