HTML5 Canvas camera / viewport – como fazer isso de maneira atraente?

Tenho certeza que isso foi resolvido mil vezes antes: eu tenho uma canvas no tamanho de 960 * 560 e uma sala no tamanho de 5000 * 3000, da qual apenas 960 * 560 devem ser desenhados, dependendo de onde o jogador está. O jogador deve estar sempre no meio, mas quando perto de fronteiras – então a melhor vista deve ser calculada). O jogador pode se mover totalmente livre com WASD ou as teclas de seta. E todos os objects devem se mover – ao invés disso eu movo todo o resto, menos o jogador, para criar a ilusão de que o jogador se move.

Eu encontrei agora essas duas perguntas:

HTML5 – Criando uma viewport para canvas funciona, mas apenas para este tipo de jogo, não consigo reproduzir o código para o meu.

Mudar a visão “center” de uma canvas html5 parece ser mais promissora e também perfomante, mas eu só entendo isso para desenhar todos os outros objects corretamente em relação ao player e não como rolar a viewport da canvas em relação ao player, o que eu quero para alcançar primeiro, claro.

Meu código (simplificado – a lógica do jogo é separada):

var canvas = document.getElementById("game"); canvas.tabIndex = 0; canvas.focus(); var cc = canvas.getContext("2d"); // Define viewports for scrolling inside the canvas /* Viewport x position */ view_xview = 0; /* Viewport y position */ view_yview = 0; /* Viewport width */ view_wview = 960; /* Viewport height */ view_hview = 560; /* Sector width */ room_width = 5000; /* Sector height */ room_height = 3000; canvas.width = view_wview; canvas.height = view_hview; function draw() { clear(); requestAnimFrame(draw); // World's end and viewport if (player.x < 20) player.x = 20; if (player.y  room_width-20) player.x = room_width-20; if (player.y > room_height-20) player.y = room_height-20; if (player.x > view_wview/2) ... ? if (player.y > view_hview/2) ... ? } 

O jeito que eu estou tentando fazer funcionar parece totalmente errado e eu nem sei como estou tentando … Alguma idéia? O que você acha da coisa context.transform?

Espero que você entenda minha descrição e que alguém tenha uma ideia. Atenciosamente

DEMO AO VIVO em jsfiddle.net

Esta demonstração ilustra o uso da janela de visualização em um cenário de jogo real. Use as setas para mover o jogador pela sala. A grande sala é gerada em tempo real usando retângulos e o resultado é salvo em uma imagem.

Observe que o jogador está sempre no meio, exceto quando perto de bordas (como você deseja).


Agora vou tentar explicar as partes principais do código, pelo menos as partes que são mais difíceis de entender apenas olhando para ele.


Usando a drawImage para desenhar imagens grandes de acordo com a posição da viewport

Uma variante do método drawImage possui oito novos parâmetros. Podemos usar esse método para cortar partes de uma imagem de origem e desenhá-las na canvas.

drawImage (imagem, sx, sy, sWidth, altura, dx, dy, dWidth, dHeight)

A primeira imagem de parâmetro, assim como as outras variantes, é uma referência a um object de imagem ou uma referência a um elemento de canvas diferente. Para os outros oito parâmetros, é melhor olhar para a imagem abaixo. Os quatro primeiros parâmetros definem a localização e o tamanho da fatia na imagem de origem. Os últimos quatro parâmetros definem a posição e o tamanho na canvas de destino.

Canvas drawImage

Fonte: https://developer.mozilla.org/pt-BR/docs/Web/Guide/HTML/Canvas_tutorial/Using_images

Como funciona na demonstração:

Temos uma imagem grande que representa a sala e queremos mostrar na canvas apenas a parte da viewport. A posição de recorte (sx, sy) é a mesma posição da câmera (xView, yView) e as dimensões de recorte são as mesmas da viewport (canvas), de modo que sWidth=canvas.width e sHeight=canvas.height .

Precisamos tomar cuidado com as dimensões da cultura porque a drawImage não desenha nada na canvas se a posição da cultura ou as dimensões da cultura com base na posição forem inválidas. É por isso que precisamos das seções if abaixo.

 var sx, sy, dx, dy; var sWidth, sHeight, dWidth, dHeight; // offset point to crop the image sx = xView; sy = yView; // dimensions of cropped image sWidth = context.canvas.width; sHeight = context.canvas.height; // if cropped image is smaller than canvas we need to change the source dimensions if(image.width - sx < sWidth){ sWidth = image.width - sx; } if(image.height - sy < sHeight){ sHeight = image.height - sy; } // location on canvas to draw the croped image dx = 0; dy = 0; // match destination with source to not scale the image dWidth = sWidth; dHeight = sHeight; // draw the cropped image context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); 

Objetos de jogos de desenho relacionados à viewport

Ao escrever um jogo, é uma boa prática separar a lógica e a renderização de cada object no jogo. Então, em demonstração, temos funções de update e draw . O método de update altera o status do object como posição no "mundo do jogo", aplica física, estado de animação, etc. O método draw realmente renderiza o object e o processa apropriadamente considerando a viewport, o object precisa conhecer o contexto render e a viewport propriedades.

Observe que os objects do jogo são atualizados considerando a posição do mundo do jogo. Isso significa que a posição (x, y) do object é a posição no mundo. Apesar disso, como a janela de exibição está mudando, os objects precisam ser renderizados corretamente e a posição de renderização será diferente da posição do mundo.

A conversão é simples:

posição do object no mundo (sala): (x, y)
posição da janela de visualização: (xView, yView)

posição de renderização : (x-xView, y-yView)

Isso funciona para todos os tipos de coordenadas, até mesmo as negativas.


Câmera do jogo

Nossos objects de jogo possuem um método de atualização separado. Na implementação do Demo, a câmera é tratada como um object de jogo e também tem um método de atualização separado.

O object da câmera mantém a posição superior esquerda da viewport (xView, yView) , um object a ser seguido, um retângulo representando a viewport, um retângulo que representa o limite do mundo do jogo e a distância mínima de cada borda que o player poderia estar antes da câmera começar move (xDeadZone, yDeadZone). Também definimos os graus de liberdade da câmera (eixo). Para um jogo de estilo de vista superior como um RPG, a câmera pode se mover nos eixos x (horizontal) e y (vertical).

Para manter o player no meio da viewport, definimos o deadZone de cada eixo para convergir com o centro da canvas. Veja a function follow no código:

camera.follow (player, canvas.width / 2, canvas.height / 2)


Limites do mundo

Como cada object, incluindo a câmera, possui sua própria function de atualização, é fácil verificar os limites do mundo do jogo. Lembre-se apenas de colocar o código que bloqueia o movimento no final da function de atualização.


Demonstração

Veja o código completo e tente você mesmo. Muito melhor que explicar com palavras. Talvez depois de ler o código, essa quantidade de informações seja esclarecida.

DEMONSTRAÇÃO AO VIVO

Código completo:

        

Sinta-se à vontade para relatar erros ou adicionar sugestões.

O código na resposta aceita é um pouco demais. É tão simples assim:

 function draw() { ctx.setTransform(1,0,0,1,0,0);//reset the transform matrix as it is cumulative ctx.clearRect(0, 0, canvas.width, canvas.height);//clear the viewport AFTER the matrix is reset //Clamp the camera position to the world bounds while centering the camera around the player var camX = clamp(-player.x + canvas.width/2, yourWorld.minX, yourWorld.maxX - canvas.width); var camY = clamp(-player.y + canvas.height/2, yourWorld.minY, yourWorld.maxY - canvas.height); ctx.translate( camX, camY ); //Draw everything } 

E braçadeira parece com:

 function clamp(value, min, max){ if(value < min) return min; else if(value > max) return max; return value; } 

Veja como usar a canvas para ser uma viewport em outra imagem maior que a da canvas

Uma janela de visualização é, na verdade, apenas uma parte recortada de uma imagem maior exibida para o usuário.

Nesse caso, a porta de visualização será exibida para o usuário em uma canvas (a canvas é a viewport).

Primeiro, codifique uma function de movimento que passe a janela de visualização pela imagem maior.

Esta function move o canto superior / esquerdo da viewport por 5px na direção especificada:

 function move(direction){ switch (direction){ case "left": left-=5; break; case "up": top-=5; break; case "right": left+=5; break; case "down": top+=5 break; } draw(top,left); } 

A function move chama a function draw.

Em draw (), a function drawImage irá cortar uma porção especificada de uma imagem maior.

drawImage também exibirá esse “segundo plano recortado” para o usuário na canvas.

 context.clearRect(0,0,game.width,game.height); context.drawImage(background,cropLeft,cropTop,cropWidth,cropHeight, 0,0,viewWidth,viewHeight); 

Neste exemplo,

Plano de fundo é a imagem de fundo completa (geralmente não exibida, mas sim uma fonte de recorte)

cropLeft & cropTop definem onde, na imagem de fundo, o recorte começará.

cropWidth & cropHeight define o tamanho de um retângulo que será cortado da imagem de plano de fundo.

0,0 diz que a sub-imagem que foi cortada do fundo será desenhada em 0,0 na canvas do viewport.

viewWidth & viewHeight são a largura e a altura da canvas do visor

Então, aqui está um exemplo de drawImage usando números.

Digamos que nossa viewport (= nossa canvas de exibição) tenha 150 pixels de largura e 100 pixels de altura.

 context.drawImage(background,75,50,150,100,0,0,150,100); 

Os 75 e 50 dizem que o recorte começará na posição x = 75 / y = 50 na imagem de fundo.

Os 150.100 dizem que o retângulo a ser cortado terá 150 de largura e 100 de altura.

Os 0,0,150,100 dizem que a imagem do retângulo recortado será exibida usando o tamanho total da canvas do visor.

É isso para a mecânica de desenhar uma janela … basta adicionar controles-chave!

Aqui está o código e um violino: http://jsfiddle.net/m1erickson/vXqyc/

           

O jeito que você está fazendo agora parece correto para mim. Eu mudaria os limites “20” para uma variável, então você pode facilmente mudar os limites de um nível ou o jogo inteiro, se você precisar.

Você pode abstrair essa lógica em um método específico de “Viewport”, que simplesmente manipulará os cálculos necessários para determinar onde sua “Camera” precisa estar no mapa e, então, certificar-se de que as coordenadas X e Y de seu caractere coincidam com o centro de sua câmera.

Você também pode inverter esse método e determinar a localização de sua câmera com base na posição dos caracteres (por exemplo: (position.x - (desired_camera_size.width / 2)) ) e desenhe a câmera de lá em diante.

Quando você tiver a sua posição da câmera descoberta, você pode começar a se preocupar em desenhar a própria sala como a primeira camada da sua canvas.

Esta é uma simples questão de definir a janela de visualização para as coordenadas xey do destino, como afirma Colton , em cada quadro. As transformações não são necessárias, mas podem ser usadas conforme desejado. A fórmula que funcionou para mim foi:

 function update() { // Assign the viewport to follow a target for this frame viewport.x = -target.x + canvas.width / 2; viewport.y = -target.y + canvas.height / 2; // Draw each entity, including the target, relative to the viewport ctx.fillRect( entity.x + viewport.x, entity.y + viewport.y, entity.size, entity.size ); } 

A fixação ao mapa é um segundo passo opcional:

 function update() { // Assign the viewport to follow a target for this frame viewport.x = -target.x + canvas.width / 2; viewport.y = -target.y + canvas.height / 2; // Keep viewport in map bounds viewport.x = clamp(viewport.x, canvas.width - map.width, 0); viewport.y = clamp(viewport.y, canvas.height - map.height, 0); // Draw each entity, including the target, relative to the viewport ctx.fillRect( entity.x + viewport.x, entity.y + viewport.y, entity.size, entity.size ); } // Restrict n to a range between lo and hi const clamp = (n, lo, hi) => n < lo ? lo : n > hi ? hi : n; 

Aqui está um exemplo: https://jsfiddle.net/ggorlen/7yv7u572/