Mar, piratas - jogo online 3D no navegador

Saudações aos usuários Habr e leitores casuais. Esta é a história do desenvolvimento de um jogo online para vários jogadores, baseado em navegador, com gráficos 3D de baixo nível de polis e física 2D simples.



Existem muitos mini-jogos 2D baseados em navegador, mas esse projeto é novo para mim. No gamedev, resolver problemas que você ainda não encontrou pode ser bastante emocionante e interessante. O principal é não ficar preso às peças de retificação e iniciar um jogo de trabalho enquanto houver desejo e motivação, por isso não vamos perder tempo e começar a desenvolver!





O jogo em poucas palavras



Survival Fight é o único modo de jogo no momento. Batalhas de 2 a 6 navios sem renascimento, onde o último jogador sobrevivente é considerado o vencedor e recebe x3 pontos e ouro.



Controles da arcada : botões W, A, D ou setas para mover, barra de espaço para disparar contra navios inimigos. Você não precisa mirar, não pode errar, o dano depende da aleatoriedade e do ângulo do tiro. Maior dano é acompanhado por uma medalha "no alvo".



Ganhamos ouro conquistando o primeiro lugar na classificação de jogadores em 24 horas e em 7 dias (redefinir às 00:00, horário de Moscou) e concluindo tarefas diárias (uma de três é emitida por um dia, por sua vez). Também há ouro para batalhas, mas menos.



Gastar ourodefinir velas negras em seu navio por 24 horas. Os planos para adicionar a capacidade de acordar o Kraken, que levará o fundo de qualquer navio inimigo com seus tentáculos gigantes :)



PVP ou covarde zassal ? Um recurso que eu queria implementar antes mesmo de escolher um tema pirata é a capacidade de lutar com os amigos em alguns cliques. Sem registro e gestos desnecessários, você pode enviar um link de convite para seus amigos e esperar até que eles entrem no jogo usando o link: uma sala privada que pode ser aberta para todos é criada automaticamente quando alguém segue o link, desde que o "autor" do link não tenha iniciado outro batalha.



Pilha de tecnologia



O Three.js é uma das bibliotecas mais populares para trabalhar com 3D no navegador, com boa documentação e muitos exemplos diferentes. Além disso, eu já usei o Three.js - a escolha é óbvia.



A falta de um mecanismo de jogo se deve à falta de experiência relevante e ao desejo de aprender algo sem o qual tudo funciona bem de qualquer maneira :)



Node.js porque é simples, rápido e conveniente, embora eu não tivesse experiência direta no Node.js. Eu considerei o Java uma alternativa, realizei algumas experiências locais, inclusive com soquetes da Web, mas não ousei descobrir se era difícil executar o Java em um VPS. Outra opção - Vá, sua sintaxe me deixa desanimado - não avançou em seu estudo nem um pouco.



Para soquetes da Web, use o módulo ws no Node.js.



PHP e MySQLescolha menos óbvia, mas o critério ainda é o mesmo - rápida e facilmente, já que existe experiência nessas tecnologias.



Acontece assim: o







PHP é necessário antes de tudo para retornar as páginas da Web ao cliente e para solicitações raras de AJAX, mas na maioria das vezes o cliente ainda se comunica com o servidor do jogo no Node.js por meio de soquetes da Web.



Eu não queria vincular o servidor do jogo ao banco de dados, então tudo passa pelo PHP. Na minha opinião, há vantagens aqui, embora não tenha certeza se são significativas. Por exemplo, como os dados prontos chegam ao Node.js. na forma exigida, o Node.js. não perde tempo processando e consultas adicionais no banco de dados, mas lida com coisas mais importantes - ele "digere" as ações dos jogadores e altera o estado do mundo do jogo nas salas.



Modele primeiro



O desenvolvimento começou com uma coisa simples e mais importante - um determinado modelo do mundo do jogo, descrevendo as batalhas marítimas do ponto de vista do servidor. A tela plana 2D é ideal para a exibição esquemática do modelo na tela.







Inicialmente, estabeleci a física "verlet" normal e levei em conta a resistência diferente ao movimento do navio em diferentes direções em relação à direção do casco. Mas, preocupando-me com o desempenho do servidor, substituí a física normal pela mais simples, onde os contornos do navio permaneciam apenas no visual, mas fisicamente os navios são objetos redondos que nem sequer têm inércia. Em vez de inércia, há aceleração direta limitada.



Tiros e acertos são reduzidos a operações simples com os vetores da direção do navio e a direção do tiro. Não há conchas aqui. Se o produto escalar de vetores normalizados se encaixar nos valores aceitáveis, levando em consideração a distância do alvo, haverá um tiro e um golpe se o jogador pressionar o botão.



O JavaScript do lado do cliente para renderizar o modelo do mundo do jogo, manipular o movimento de navios e disparos, eu movi para o servidor Node.js quase inalterado.



Servidor de jogo



O servidor Node.js WebSocket consiste em apenas 3 scripts:



  • main.js - o script principal que recebe mensagens dos jogadores do WS, cria salas e faz as engrenagens desta máquina girarem
  • room.js - um script responsável pela jogabilidade dentro da sala: atualizando o mundo do jogo, enviando atualizações para os jogadores na sala
  • funcs.js - inclui uma classe para trabalhar com vetores, algumas funções auxiliares e uma classe que implementa uma lista duplamente vinculada


À medida que o desenvolvimento avançava, novas classes foram adicionadas - quase todas elas estão diretamente relacionadas à jogabilidade e acabaram no arquivo room.js. Às vezes, é conveniente trabalhar com classes separadamente (em arquivos separados), mas a opção tudo em um também não é ruim, desde que não haja muitas classes (é conveniente rolar para cima e lembrar quais parâmetros um método de outra classe aceita).



A lista atual de classes de servidor de jogos:



  • WaitRoom - a sala em que os jogadores aguardam o início da batalha, possui seu próprio método de marcação que envia suas atualizações e inicia a criação da sala de jogos quando mais da metade dos jogadores estão prontos para a batalha
  • Room — , : /, ,
  • Player — «» :
  • Ship — : , , ,
  • PhysicsEngine — ,
  • PhysicsBody


Room
let upd = {p: [], t: this.gamet};
let t = Date.now();
let dt = t - this.lt;
let nalive = 0;

for (let i in this.players) {
	this.players[i].tick(t, dt);
}

this.physics.run(dt);

for (let i in this.players) {
	upd.p.push(this.players[i].getUpd());
}

this.chronology.addLast(clone(upd));
if (this.chronology.n > 30) this.chronology.remFirst();

let updjson = JSON.stringify(upd);

for (let i in this.players) {
	let pl = this.players[i];
	if (pl.ship.health > 0) nalive++;
	if (pl.deadLeave) continue;
	pl.cl.ws.send(updjson);
}

this.lt = t;
this.gamet += dt;

if (nalive <= 1) return false;
return true;




Além das aulas, existem funções como obter dados do usuário, atualizar uma tarefa diária, obter uma recompensa, comprar uma capa. Essas funções basicamente enviam solicitações https ao PHP, que executa uma ou mais consultas MySQL e retorna o resultado.



Atrasos na rede



A compensação de latência da rede é uma parte importante do desenvolvimento de jogos online. Sobre esse tópico, reli várias vezes aqui uma série de artigos sobre Habré . No caso de uma batalha de veleiros, a compensação de atraso pode ser simples, mas você ainda precisa fazer compromissos.



A interpolação é constantemente realizada no cliente - o cálculo do estado do mundo do jogo entre dois momentos no tempo, cujos dados já foram obtidos. Há uma pequena margem de tempo, que reduz a probabilidade de saltos repentinos e, com atrasos significativos na rede e a ausência de novos dados, a interpolação é substituída pela extrapolação. A extrapolação não fornece resultados muito corretos, mas é barato para o processador e não depende de como o movimento de navios é implementado no servidor e, é claro, às vezes pode salvar a situação.



Ao resolver o problema de defasagens, depende muito do jogo e do seu ritmo. Eu sacrifico uma resposta rápida às ações do jogador em favor de animação suave e correspondência exata da imagem com o estado do mundo do jogo em um determinado ponto no tempo. A única exceção é que uma salva de canhão é jogada imediatamente com o apertar de um botão. O resto pode ser atribuído às leis do universo e ao excesso de rum da tripulação do navio :)



A parte dianteira



Infelizmente, não há estrutura ou hierarquia clara de classes e métodos. Todo JS é dividido em objetos com suas próprias funções, que em certo sentido são iguais. Quase todos os meus projetos anteriores eram mais lógicos que este. Isso ocorre em parte porque o primeiro objetivo era depurar o modelo do mundo do jogo na interação do servidor e da rede sem prestar atenção à interface e ao componente visual do jogo. Quando chegou a hora de adicionar 3D, eu literalmente a adicionei à versão de teste existente, substituindo a função drawShip 2D por exatamente a mesma, mas 3D, embora de uma maneira amigável valesse a pena revisar toda a estrutura e preparar a base para futuras mudanças.



Navio 3D



O Three.js suporta o uso de modelos 3D prontos em vários formatos. Escolhi o formato GLTF / GLB para mim, onde texturas e animações podem ser incorporadas, ou seja, o desenvolvedor não deve estar se perguntando "todas as texturas foram carregadas?"



Eu nunca lidei com editores de 3D antes. O passo lógico era entrar em contato com um especialista em uma troca freelance com a tarefa de criar um modelo 3D de um barco à vela com uma animação incorporada de uma salva de canhão. Mas não pude resistir a pequenas mudanças no modelo de especialista finalizado sozinho e acabei criando o meu modelo do zero no Blender. Criar um modelo de baixo poli com quase nenhuma textura é simples, difícil, sem um modelo pronto de um especialista para estudar em um editor 3D o que é necessário para uma tarefa específica (pelo menos moralmente :).







Shaders para o deus dos shaders



A principal razão pela qual eu preciso dos meus shaders é a capacidade de manipular a geometria de um objeto na placa de vídeo durante a renderização, que apresenta um bom desempenho. O Three.js não apenas permite que você crie seus próprios shaders, mas também pode assumir parte do trabalho.



O mecanismo ou método que eu usei ao criar um sistema de partículas para animar danos a um navio, uma superfície de água dinâmica ou um fundo do mar estático é o mesmo: o ShaderMaterial especial fornece uma interface simplificada para usar seu shader (seu código GLSL), o BufferGeometry permite criar geometria a partir de dados arbitrários ...



Um espaço em branco vazio, uma estrutura de código conveniente para eu copiar, suplementar e modificar para criar meu objeto 3D da mesma maneira:



Mostrar Código
let vs = `
	attribute vec4 color;
	varying vec4 vColor;

	void main(){
		vColor = color;
		gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		// gl_PointSize = 5.0; // for particles
	}
`;
let fs = `
	uniform float opacity;
	varying vec4 vColor;

	void main() {
		gl_FragColor = vec4(vColor.xyz, vColor.w * opacity);
	}
`;

let material = new THREE.ShaderMaterial( {
	uniforms: {
		opacity: {value: 0.5}
	},
	vertexShader: vs,
	fragmentShader: fs,
	transparent: true
});

let geometry = new THREE.BufferGeometry();

//let indices = [];
let vertices = [];
let colors = [];

/* ... */

//geometry.setIndex( indices );
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 4 ) );

let mesh = new THREE.Mesh(geometry, material);




Danos no navio



As animações de dano de navio estão movendo partículas que mudam de tamanho e cor, cujo comportamento é determinado por seus atributos e pelo código de shader GLSL. A geração de partículas (geometria e material) ocorre antecipadamente e, para cada navio, é criada sua própria instância (malha) de partículas danificadas (a geometria é comum a todos, o material é clonado). Existem muitos atributos de partículas, mas o sombreador criado implementa simultaneamente grandes nuvens de poeira em movimento lento, detritos que voam rapidamente e partículas de fogo, cuja atividade depende do grau de dano à nave.







Mar



O mar também é implementado usando ShaderMaterial. Cada vértice se move nas três direções ao longo de um sinusóide, formando ondas aleatórias. Os atributos definem as amplitudes para cada direção do movimento e a fase do sinusóide.



Para diversificar as cores na água e tornar o jogo mais interessante e agradável aos olhos, decidiu-se adicionar o fundo e as ilhas. A cor do fundo depende da altura / profundidade e brilha através da superfície da água, criando áreas escuras e claras.



O fundo do mar é criado a partir de um mapa de altura, criado em 2 estágios: primeiro, o fundo sem ilhas foi criado em um editor gráfico (no meu caso, as ferramentas foram renderizadas -> nuvens e desfocagem gaussiana), depois as ilhas foram adicionadas em ordem aleatória usando o Canvas JS online no jsFiddle desenhando um círculo e desfocando. Algumas ilhas são baixas, através delas você pode atirar em oponentes, outras têm uma certa altura, tiros não passam por elas. Além do próprio mapa de altura, na saída eu recebo dados em formato json sobre as ilhas (sua posição e tamanho) para física no servidor.







Qual é o próximo?



Existem muitos planos para o desenvolvimento do jogo. Os principais são novos modos de jogo. Os menores - crie sombras / reflexos na água, levando em consideração as limitações de desempenho do WebGL e JS. Eu já mencionei a oportunidade de acordar o Kraken :) A unificação dos jogadores em salas com base na experiência acumulada ainda não foi implementada. Uma melhoria de prioridade óbvia, mas não muito alta, é criar vários mapas do fundo do mar e das ilhas e escolher um deles aleatoriamente para uma nova batalha.



Você pode criar muitos efeitos visuais desenhando repetidamente a cena "na memória" e combinando todos os dados em uma imagem (na verdade, pode ser chamada de pós-processamento), mas minha mão não se levanta para aumentar a carga no cliente dessa maneira, porque o cliente ainda é um navegador em vez de um aplicativo nativo. Talvez um dia eu decida esse passo.



Há também perguntas que agora acho difícil responder: quantos jogadores on-line suportam um servidor virtual barato, se será possível coletar pelo menos um certo número de jogadores interessados ​​e como fazê-lo.



Ovos de pascoa



Quem não gosta de se lembrar de velhos jogos de computador que deram tantas emoções? Eu amo repetir o jogo Corsairs 2 (também conhecido como Sea Dogs 2) várias vezes até agora. Não pude deixar de acrescentar um segredo ao meu jogo e uma reminiscência explícita e indireta de "Corsairs 2". Não revelarei todas as cartas, mas darei uma dica: meu ovo de Páscoa é um determinado objeto que você pode encontrar ao explorar o mar (você não precisa navegar muito pelo mar sem fim, o objeto está dentro da razão, mas ainda a probabilidade de encontrá-lo não é alta). O ovo da páscoa repara completamente o navio danificado.



O que aconteceu



Vídeo por minuto (teste de 2 dispositivos):





Link para o jogo: https://sailfire.pw



Também há um formulário de contato, mensagens são enviadas para mim em telegramas: https://sailfire.pw/feedback/

Links para quem deseja acompanhar as notícias e atualizações: VK Public , canal Telegram



All Articles