Gerenciando mapas de texto em uma matriz 2D para serem pintados no HTML5 Canvas


46

Então, eu estou fazendo um RPG HTML5 apenas por diversão. O mapa é um <canvas>(largura de 512 px, altura de 352 px | 16 blocos de largura, 11 blocos de cima para baixo). Quero saber se existe uma maneira mais eficiente de pintar o <canvas>.

Aqui está como eu o tenho agora.

Como os blocos são carregados e pintados no mapa

O mapa está sendo pintado por azulejos (32x32) usando a Image()peça. Os arquivos de imagem são carregados através de um forloop simples e colocados em uma matriz chamada tiles[]PAINTED ao usar drawImage().

Primeiro, carregamos os ladrilhos ...

insira a descrição da imagem aqui

e aqui está como está sendo feito:

// SET UP THE & DRAW THE MAP TILES
tiles = [];
var loadedImagesCount = 0;
for (x = 0; x <= NUM_OF_TILES; x++) {
  var imageObj = new Image(); // new instance for each image
  imageObj.src = "js/tiles/t" + x + ".png";
  imageObj.onload = function () {
    console.log("Added tile ... " + loadedImagesCount);
    loadedImagesCount++;
    if (loadedImagesCount == NUM_OF_TILES) {
      // Onces all tiles are loaded ...
      // We paint the map
      for (y = 0; y <= 15; y++) {
        for (x = 0; x <= 10; x++) {
          theX = x * 32;
          theY = y * 32;
          context.drawImage(tiles[5], theY, theX, 32, 32);
        }
      }
    }
  };
  tiles.push(imageObj);
}

Naturalmente, quando um jogador inicia um jogo, ele carrega o mapa que ele deixou por último. Mas por aqui, é um mapa de grama.

No momento, os mapas usam matrizes 2D. Aqui está um exemplo de mapa.

[[4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 1, 1, 1, 1], 
[1, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 1, 1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 13, 13, 13, 1, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 13, 13, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 1], 
[13, 13, 13, 1, 1, 1, 1, 1, 1, 1, 13, 13, 13, 13, 13, 1], 
[1, 1, 1, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1, 1, 1]];

Recebo mapas diferentes usando uma ifestrutura simples . Uma vez que o 2d array acima seja return, o número correspondente em cada array será pintado de acordo com o Image()armazenado no interior tile[]. Em seguida, drawImage()irá ocorrer e pintar de acordo com o xe ye vezes por 32pintar na correta x-ycoordenadas.

Como ocorre a troca de mapas múltiplos

Com o meu jogo, mapas têm cinco coisas para manter o controle de: currentID, leftID, rightID, upID, e bottomID.

  • currentID: O ID atual do mapa em que você está.
  • leftID: qual ID de currentIDcarregar quando você sai à esquerda do mapa atual.
  • rightID: qual ID de currentIDcarregar quando você sai à direita do mapa atual.
  • downID: qual ID de currentIDcarregar quando você sai na parte inferior do mapa atual.
  • upID: qual ID de currentIDcarregar quando você sai na parte superior do mapa atual.

Algo a nota: Se qualquer um leftID, rightID, upID, ou bottomIDnão são específicos, isso significa que eles são um 0. Isso significa que eles não podem sair desse lado do mapa. É apenas um bloqueio invisível.

Assim, uma vez que uma pessoa sai de um lado do mapa, dependendo de onde saiu ... por exemplo, se saiu na parte inferior, bottomIDserá o número do mapcarregamento a ser carregado e, assim, será pintado no mapa.

Aqui está um .GIF representacional para ajudá-lo a visualizar melhor:

insira a descrição da imagem aqui

Como você pode ver, mais cedo ou mais tarde, com muitos mapas, lidarei com muitos IDs. E isso pode ficar um pouco confuso e agitado.

Os profissionais óbvios são que ele carrega 176 blocos de cada vez, atualiza uma pequena tela de 512x352 e manipula um mapa de cada vez. A desvantagem é que os IDs do MAP, ao lidar com muitos mapas, podem ficar confusos às vezes.

Minha pergunta

  • É uma maneira eficiente de armazenar mapas (dado o uso de blocos) ou existe uma maneira melhor de lidar com mapas?

Eu estava pensando nas linhas de um mapa gigante. O tamanho do mapa é grande e é uma matriz 2D. A janela de exibição, no entanto, ainda tem 512x352 pixels.

Aqui está outro .gif que eu fiz (para esta pergunta) para ajudar a visualizar:

insira a descrição da imagem aqui

Desculpe se você não consegue entender meu inglês. Por favor, pergunte qualquer coisa que você tenha problemas para entender. Felizmente, deixei claro. Obrigado.


16
O esforço colocado nesta questão com os gráficos e tudo merece um voto positivo. :)
Tapio

Respostas:


5

Editar: Acabei de ver que minha resposta foi baseada no seu código, mas na verdade não respondeu à sua pergunta. Eu mantive a resposta antiga, caso você possa usar essas informações.

Editar 2: Corrigi dois problemas com o código original: - A adição de +1 no cálculo das extremidades x e y foi por engano dentro dos colchetes, mas precisa ser adicionada após a divisão. - Esqueci de validar os valores x e y.

Você pode simplesmente ter o mapa atual na memória e carregar os mapas ao redor. Imagine um mundo que consiste em uma matriz 2D de níveis únicos do tamanho 5x5. O jogador começa no campo 1. Como os limites superior e esquerdo do nível estão na extremidade do mundo, eles não precisam ser carregados. Portanto, nesse caso, o nível 1/1 está ativo e os níveis 1/2 e 2/1 são carregados ... Se o jogador agora se mover para a direita, todos os níveis (além daquele para o qual você está se mudando) serão descarregados e o novo ambiente será carregado. carregado. O nível 2/1 agora está ativo, 1/1, 2/2 e 3/1 agora estão carregados.

Espero que isso lhe dê uma idéia de como isso poderia ser feito. Mas essa abordagem não funcionará muito bem, quando todos os níveis tiverem que ser simulados durante o jogo. Mas se você pode congelar níveis não utilizados, isso deve funcionar bem.

Resposta antiga:

O que faço ao renderizar o nível (também baseado em blocos) é calcular quais itens da matriz de blocos interceptam a viewport e, então, eu apenas processo esses blocos. Dessa forma, você pode ter mapas grandes, mas precisa renderizar a parte na tela.

//Mock values
//camera position x
//viewport.x = 233f;
//camera position y
//viewport.y = 100f;
//viewport.w = 640
//viewport.h = 480
//levelWidth = 10
//levelHeight = 15
//tilesize = 32;

//startX defines the starting index for the x axis.
// 7 = 233 / 32
int startX = viewport.x / tilesize;

//startY defines the starting index
// 3 = 100 / 32
int startY = viewport.y / tilesize;

//End index y
// 28 = (233 + 640) / 32 + 1
int endX = ((viewport.x + viewport.w) / tilesize) + 1;

//End index y
// 19 = (100 + 480) / 32 + 1
int endX = ((viewport.x + viewport.w) / tilesize) + 1;

//Validation
if(startX < 0) startX = 0;
if(startY < 0) startY = 0;
//endX is set to 9 here
if(endX >= levelWidth) endX = levelWidth - 1;
//endX is set to 14 here
if(endY >= levelHeight) endY = levelHeight - 1;

for(int y = startY; y < yEnd; y++)
    for(int x = startX; x < xEnd; x++)
          [...]

Nota: O código acima não foi testado, mas deve ter uma ideia do que fazer.

Seguindo um exemplo básico para uma representação de viewport:

public class Viewport{
    public float x;
    public float y;
    public float w;
    public float h;
}

Uma representação em javascript se pareceria

<script type="text/javascript">
//Declaration
//Constructor
var Viewport = function(xVal, yVal, wVal, hVal){
    this.x = xVal;
    this.y = yVal;
    this.w = wVal;
    this.h = hVal;
}
//Prototypes
Viewport.prototype.x = null;
Viewport.prototype.y = null;
Viewport.prototype.w = null;
Viewport.prototype.h = null;
Viewport.prototype.toString = function(){
    return ["Position: (", this.x, "/" + this.y + "), Size: (", this.w, "/", this.h, ")"].join("");
}

//Usage
var viewport = new Viewport(23, 111, 640, 480);
//Alerts "Position: (23/111), Size: (640/480)
alert(viewport.toString());
</script>

Se você pegar meu exemplo de código, basta substituir as declarações int e float por var. Além disso, certifique-se de usar Math.floor nesses cálculos, atribuídos aos valores baseados em int, para obter o mesmo comportamento.

Uma coisa que eu consideraria (embora não tenha certeza se isso importa em javascript) é colocar todos os blocos (ou o máximo possível) em uma grande textura em vez de usar muitos arquivos únicos.

Aqui está um link que explica como fazê-lo: http://thiscouldbebetter.wordpress.com/2012/02/25/slicing-an-image-into-tiles-in-javascript/


Apenas para esclarecer ... viewport.x seria predado como "531" e viewport.y seria "352" e tilesize = "32". E tal ..?
teste

Se você quer dizer que a posição da câmera é 531, então sim. Eu adicionei alguns comentários ao código acima.
Tom van verde

Estou usando JavaScript ... Eu sou Java é quase idêntico a isso.
test

Sim, você apenas precisa criar uma classe de viewport baseada em js (ou você pode simplesmente criar 4 variáveis ​​x, y, weh). Além disso, você deve certificar-se de incluir (Math.floor) esses cálculos atribuídos aos valores int. Adicionei um exemplo de código, como seria a classe da viewport. Apenas certifique-se de colocar a declaração antes do uso.
tom van green

Só estou dizendo que você coloca 640, 480.. mas pode funcionar com 512, 352certo?
Teste

3

Armazenar mapas como blocos é útil para permitir que você reutilize ativos e redesenha o mapa. Realisticamente, isso realmente não oferece muitos benefícios ao jogador. Além disso, você está redesenhando cada bloco de cada vez. Aqui está o que eu faria:

Armazene o mapa inteiro como uma grande matriz. Não há nenhum ponto em ter mapas e submapas, bem como potencialmente submapas (se você estiver entrando em edifícios, por exemplo). Você está carregando tudo de qualquer maneira e isso mantém tudo agradável e simples.

Renderize o mapa de uma só vez. Tenha uma tela fora da tela para a qual você renderiza o mapa e use-a no seu jogo. Esta página mostra como fazer isso com uma simples função javascript:

var renderToCanvas = function (width, height, renderFunction) {
  var buffer = document.createElement('canvas');
  buffer.width = width;
  buffer.height = height;
  renderFunction(buffer.getContext('2d'));
  return buffer;
};

Isso pressupõe que seu mapa não mude muito. Se você quiser renderizar submapas no local (por exemplo, o interior de um edifício), você também o renderiza em uma tela e depois desenha-a no topo. Aqui está um exemplo de como você pode usar isso para renderizar seu mapa:

var map = renderToCanvas( map_width*32, map_height*32, renderMap );

var MapSizeX = 512;
var MapSizeY = 352;

var draw = function(canvas, mapCentreX, mapCentreY) {
  var context = canvas.getContext('2d');

  var minX = playerX - MapSizeX/2;
  var minY = playerY - MapSizeY/2;

  context.drawImage(map,minX,minY,MapSizeX,MapSizeY,0,0,MapSizeX,MapSizeY);
}

Isso desenhará uma pequena parte do mapa centralizada mapCentreX, mapCentreY. Isso oferecerá rolagem suave em todo o mapa, além de movimento de sub-mosaico - você pode armazenar a posição do jogador como 1/32 de um mosaico, para que você possa obter um movimento suave no mapa (claramente, se desejar mova lado a lado como uma opção estilística, você pode aumentar em pedaços de 32).

Você ainda pode agrupar seu mapa, se quiser, ou se o seu mundo for enorme e não caber na memória dos sistemas de destino (lembre-se de que mesmo para 1 byte por pixel, um mapa de 512x352 tem 176 KB), mas por Ao renderizar seu mapa assim, você verá um grande ganho de desempenho na maioria dos navegadores.

O que isso oferece é a flexibilidade de reutilizar blocos em todo o mundo, sem a dor de cabeça de editar uma imagem grande, mas também permitindo que você execute isso de maneira rápida e fácil e considere apenas um 'mapa' (ou tantos 'mapas' quanto desejar) .



1

Uma maneira fácil de acompanhar os IDs seria simplesmente usar outro array 2D com eles: mantendo apenas x, y nesse "meta array", você pode facilmente pesquisar os outros IDs.

Dito isto, aqui está a opinião do Google sobre os jogos de tela 2D em mosaico em mosaico (bem, na verdade, o multiplayer HTML5, mas o mecanismo de mosaico também é coberto) de sua recente I / O 2012: http://www.youtube.com/watch?v=Prkyd5n0P7k It também possui código fonte disponível.


1

Sua idéia de carregar o mapa inteiro primeiro em uma matriz é uma maneira eficiente, mas será fácil obter o JavaScript Injection'd, qualquer pessoa poderá conhecer o mapa e a aventura se inicia.

Também estou trabalhando em um jogo de RPG baseado na Web, do jeito que carrego meu mapa via Ajax a partir de uma página PHP, que carrega o mapa do banco de dados, onde diz a posição X, Y, Z e vlaue do bloco, desenhe-o e, finalmente, o jogador se move.

Ainda estou trabalhando para torná-lo mais eficiente, mas o mapa carrega rápido o suficiente para mantê-lo dessa maneira agora.


1
Posso ver uma demonstração?
teste

1

Não , uma matriz é a maneira mais eficiente de armazenar um mapa de blocos .

Pode haver algumas estruturas que requerem um pouco menos de memória, mas apenas à custa de custos muito mais altos para carregar e acessar os dados.


No entanto, o que precisa ser considerado é quando você carregará o próximo mapa. Dado que os mapas não são especialmente grandes, o que realmente levará mais tempo é esperar o servidor entregar o mapa. O carregamento real levará apenas alguns milissegundos. Mas essa espera pode ser feita de forma assíncrona, não precisa bloquear o fluxo do jogo. Portanto, o melhor é não carregar os dados quando necessário, mas antes. O jogador está em um mapa, agora carregue todos os mapas adjacentes. O jogador se move no próximo mapa, carrega todos os mapas adjacentes novamente, etc., etc. O jogador nem notará que seu jogo está carregando em segundo plano.

Dessa forma, você também pode criar a ilusão de um mundo sem fronteiras, também desenhando os mapas adjacentes; nesse caso, você precisará carregar não apenas os mapas adjacentes, mas também os adjacentes.


0

Não sei por que você pensa: como você pode ver, mais cedo ou mais tarde, com muitos mapas, lidarei com muitos IDs. E isso pode ficar um pouco confuso e agitado.

LeftID sempre será -1 do currentID, o rightID sempre será +1 do currentID.

O UpID sempre será - (largura total do mapa) do ID atual e o downID será sempre + (largura total do mapa) do ID atual, com exceção de zero, o que significa que você atingiu o limite.

Um único mapa pode ser dividido em muitos mapas e é salvo seqüencialmente com mapIDXXXX, onde XXXX é o ID. Dessa forma, não há nada para se confundir. Estive no seu site e parece, posso estar errado, que o problema está no editor de mapas que impõe uma limitação de tamanho, o que está impedindo sua automação de dividir um mapa grande em vários pequenos ao salvar e essa limitação é provavelmente restringindo uma solução técnica.

Eu escrevi um editor de mapa Javascript que rola em todas as direções, blocos de 1000 x 1000 (eu não recomendaria esse tamanho) e ele ainda se move tão bem quanto um mapa de 50 x 50, com 3 camadas, incluindo rolagem de paralaxe. Ele salva e lê no formato json. Enviar-lhe-ei uma cópia se você disser que não se importa com um e-mail de aproximadamente 2meg. Ele deve estar no github, mas eu não tenho nenhum conjunto de peças decente (legal), e é por isso que ainda não me preocupei em colocá-lo lá.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.