Gestire mappe di testo in un array 2D da dipingere su HTML5 Canvas


46

Quindi, sto realizzando un gioco di ruolo HTML5 solo per divertimento. La mappa è una <canvas>(512 pixel di larghezza, 352 pixel di altezza | 16 tessere attraverso, 11 tessere dall'alto verso il basso). Voglio sapere se esiste un modo più efficiente di dipingere <canvas>.

Ecco come lo ho adesso.

Come le tessere vengono caricate e dipinte sulla mappa

La mappa viene dipinta da tessere (32x32) usando il Image()pezzo. I file di immagine vengono caricati attraverso un semplice forciclo e inseriti in un array chiamato tiles[]per essere DIPINTO durante l'utilizzo drawImage().

Innanzitutto, cariciamo le tessere ...

inserisci qui la descrizione dell'immagine

ed ecco come viene fatto:

// 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 un giocatore inizia una partita, carica la mappa che ha interrotto l'ultima volta. Ma per qui, è una mappa tutta erba.

Al momento, le mappe usano array 2D. Ecco una mappa di esempio.

[[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]];

Ricevo diverse mappe usando una ifstruttura semplice . Una volta che l'array 2d sopra è return, il numero corrispondente in ciascun array verrà dipinto secondo l' Image()archiviazione interna tile[]. Quindi drawImage()si verificherà e dipingerà in base al xe ye volte 32per dipingere sulla x-ycoordinata corretta .

Come si verifica la commutazione di più mappe

Con il mio gioco, mappe hanno cinque cose da tenere traccia di: currentID, leftID, rightID, upID, e bottomID.

  • currentID: l'ID corrente della mappa in cui ti trovi.
  • leftID: quale ID currentIDcaricare quando esci a sinistra della mappa corrente.
  • rightID: quale ID currentIDcaricare quando esci sulla destra della mappa corrente.
  • downID: quale ID currentIDcaricare quando esci nella parte inferiore della mappa corrente.
  • upID: quale ID currentIDcaricare quando esci nella parte superiore della mappa corrente.

Qualcosa da Nota: Se uno dei due leftID, rightID, upID, o bottomIDnon sono specifici, che significa che sono una 0. Ciò significa che non possono lasciare quel lato della mappa. È semplicemente un blocco invisibile.

Quindi, una volta che una persona esce da un lato della mappa, a seconda di dove è uscita ... per esempio se è uscita in fondo, bottomIDil numero del mapda caricare sarà quindi dipinto sulla mappa.

Ecco un .GIF rappresentativo per aiutarti a visualizzare meglio:

inserisci qui la descrizione dell'immagine

Come puoi vedere, prima o poi, con molte mappe mi occuperò di molti ID. E questo può forse diventare un po 'confuso e frenetico.

I vantaggi ovvi è che carica 176 tessere alla volta, aggiorna una piccola tela 512x352 e gestisce una mappa alla volta. L'aspetto negativo è che gli ID MAP, quando hanno a che fare con molte mappe, a volte possono creare confusione.

La mia domanda

  • È un modo efficiente per memorizzare le mappe (dato l'uso delle tessere) o esiste un modo migliore per gestire le mappe?

Stavo pensando sulla falsariga di una gigantesca mappa. La dimensione della mappa è grande ed è tutta una matrice 2D. Il viewport, tuttavia, è ancora 512x352 pixel.

Ecco un altro .gif che ho creato (per questa domanda) per aiutare a visualizzare:

inserisci qui la descrizione dell'immagine

Scusa se non riesci a capire il mio inglese. Per favore, chiedi qualsiasi cosa tu abbia difficoltà a capire. Spero di averlo chiarito. Grazie.


16
Lo sforzo messo in questa domanda con la grafica e tutto merita un voto. :)
Tapio,

Risposte:


5

Modifica: ho appena visto che la mia risposta era basata sul tuo codice ma in realtà non ha risposto alla tua domanda. Ho conservato la vecchia risposta nel caso in cui sia possibile utilizzare tali informazioni.

Modifica 2: ho risolto 2 problemi con il codice originale: - L'aggiunta +1 nel calcolo di end xey era erroneamente tra parentesi, ma deve essere aggiunta dopo la divisione. - Ho dimenticato di convalidare i valori xey.

Potresti semplicemente avere la mappa corrente in memoria e caricare le mappe circostanti. Immagina un mondo che consiste in una matrice 2d di singoli livelli della dimensione 5x5. Il giocatore inizia nel campo 1. Poiché i limiti superiore e sinistro del livello sono ai margini del mondo, non è necessario caricarli. Quindi in quel caso è attivo il livello 1/1 e vengono caricati i livelli 1/2 e 2/1 ... Se il giocatore ora si sposta a destra, tutti i livelli (oltre a quello in cui ci si sposta) vengono scaricati e i nuovi dintorni sono caricato. Il livello 2/1 è ora attivo, 1/1, 2/2 e 3/1 sono ora caricati.

Spero che questo ti dia un'idea di come si possa fare. Ma questo approccio non funzionerà molto bene, quando ogni livello deve essere simulato durante il gioco. Ma se riesci a congelare i livelli inutilizzati, questo dovrebbe funzionare bene.

Vecchia risposta:

Quello che faccio quando eseguo il rendering del livello (anche basato su riquadri) è calcolare quali elementi dell'array di riquadri si intersecano con il riquadro di visualizzazione e quindi visualizzo solo quei riquadri. In questo modo puoi avere grandi mappe ma devi solo renderizzare la porzione sullo schermo.

//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: il codice sopra non è testato ma dovrebbe dare un'idea di cosa fare.

Di seguito un esempio di base per una rappresentazione della finestra:

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

Una rappresentazione javascript sarebbe simile

<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 prendi il mio esempio di codice, devi semplicemente sostituire le dichiarazioni int e float con var. Assicurarsi inoltre di utilizzare Math.floor su quei calcoli, assegnati ai valori basati su int, per ottenere lo stesso comportamento.

Una cosa che prenderei in considerazione (anche se non sono sicuro se questo è importante in JavaScript) è quella di mettere tutte le tessere (o il maggior numero possibile) in una grande trama invece di usare molti singoli file.

Ecco un link che spiega come farlo: http://thiscouldbebetter.wordpress.com/2012/02/25/slicing-an-image-into-tiles-in-javascript/


Giusto per chiarire ... viewport.x sarebbe predeterminato come "531" e viewport.y sarebbe "352" e tilesize = "32". E simili..?
prova il

Se intendi che la posizione della videocamera è 531, allora sì. Ho aggiunto alcuni commenti al codice sopra.
Tom van Green,

Sto usando JavaScript ... I am Java è quasi identico a questo.
prova l'

Sì, devi solo creare una classe viewport basata su js (o potresti semplicemente creare 4 variabili x, y, w e h). Inoltre, è necessario assicurarsi di eseguire il floor (Math.floor) dei calcoli assegnati ai valori int. Ho aggiunto un esempio di codice, come apparirebbe la classe viewport. Assicurati solo di mettere la dichiarazione prima dell'uso.
Tom van Green,

Sto solo dicendo che hai messo 640, 480.. ma può funzionare con 512, 352giusto?
prova l'

3

La memorizzazione di mappe come riquadri è utile per consentire il riutilizzo delle risorse e la riprogettazione della mappa. Realisticamente, però, non offre davvero molti vantaggi al giocatore. Inoltre, ridisegnerai ogni singola tessera ogni volta. Ecco cosa farei:

Memorizza l'intera mappa come un grande array. Non ha senso avere mappe e sottomappe, nonché potenzialmente sub-mappe secondarie (se stai entrando in edifici, per esempio). Lo stai caricando comunque e questo mantiene tutto bello e semplice.

Rendering della mappa tutto in una volta. Avere una tela fuori schermo su cui eseguire il rendering della mappa e quindi utilizzarla nel gioco. Questa pagina ti mostra come farlo con una semplice funzione javascript:

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

Questo presuppone che la tua mappa non cambierà molto. Se si desidera rendere sul posto le sotto-mappe (ad esempio l'interno di un edificio), è sufficiente renderizzarle anche su una tela e quindi disegnarle sopra. Ecco un esempio di come puoi usarlo per rendere la tua mappa:

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);
}

Questo disegnerà una piccola porzione della mappa centrata attorno mapCentreX, mapCentreY. Questo ti offrirà uno scorrimento uniforme su tutta la mappa, oltre al movimento di una sottotegola: puoi memorizzare la posizione del giocatore come 1/32 di una piastrella, in modo da poter ottenere un movimento regolare sulla mappa (chiaramente se vuoi sposta tessere per tessera come scelta stilistica, puoi semplicemente incrementare in blocchi di 32).

Puoi ancora dividere la tua mappa se vuoi, o se il tuo mondo è enorme e non si adatta alla memoria dei tuoi sistemi di destinazione (tieni presente che anche per 1 byte per pixel, una mappa 512x352 è 176 KB) ma per pre -rendendo la tua mappa in questo modo vedrai un grande miglioramento delle prestazioni nella maggior parte dei browser.

Ciò che ti dà è la flessibilità di riutilizzare le tessere in tutto il mondo senza il mal di testa di modificare una grande immagine, ma anche permetterti di eseguirla rapidamente e facilmente e considerare solo una 'mappa' (o quante 'mappe' vuoi) .



1

Un modo semplice per tenere traccia degli ID sarebbe semplicemente usare un altro array 2d con loro in: mantenendo solo x, y su quel "meta array", puoi facilmente cercare gli altri ID.

Detto questo, ecco introito di Google su tela 2D piastrelle di gioco (beh, in realtà HTML5 multiplayer, ma il motore di piastrelle è anche coperto) dal loro recente I / O 2012: http://www.youtube.com/watch?v=Prkyd5n0P7k E ' ha anche il codice sorgente disponibile.


1

La tua idea di caricare prima l'intera mappa in un array, è un modo efficiente, ma sarà facile ottenere JavaScript Injection, chiunque sarà in grado di conoscere la mappa e andrà all'avventura.

Sto lavorando anche su un gioco di ruolo basato sul Web, il modo in cui carico la mia mappa è tramite Ajax da una pagina PHP, che carica la mappa dal database, dove dice la posizione X, Y, Z e il vlaue della tessera, disegnala e infine il giocatore si sposta.

Ci sto ancora lavorando per renderlo più efficiente, ma la mappa si carica abbastanza velocemente da mantenerla così ormai.


1
Posso vedere una demo?
prova il

1

No , un array è il modo più efficiente di archiviare una mappa di riquadri .

Potrebbero esserci alcune strutture che richiedono un po 'meno memoria ma solo a spese di costi molto più elevati per il caricamento e l'accesso ai dati.


Ciò che necessita di qualche considerazione, tuttavia, è quando caricherai la mappa successiva. Dato che le mappe non sono particolarmente grandi, la cosa che effettivamente impiegherà più tempo è in attesa che il server fornisca la mappa. Il caricamento effettivo richiederà solo pochi millisecondi. Ma questa attesa può essere fatta in modo asincrono, non deve bloccare il flusso del gioco. Quindi la cosa migliore non è caricare i dati quando necessario ma prima. Il giocatore è in una mappa, ora carica tutte le mappe adiacenti. Il giocatore si sposta nella mappa successiva, carica nuovamente tutte le mappe adiacenti, ecc. Ecc. Il giocatore non si accorgerà nemmeno che il gioco si sta caricando in background.

In questo modo puoi anche creare un'illusione di un mondo senza frontiere disegnando anche le mappe adiacenti, in tal caso devi caricare non solo le mappe adiacenti ma anche quelle adiacenti di quelle.


0

Non sono sicuro del motivo per cui pensi: come puoi vedere, prima o poi, con molte mappe mi occuperò di molti ID. E questo può forse diventare un po 'confuso e frenetico.

LeftID sarà sempre -1 dell'ID corrente, l'ID destro sarà sempre +1 dell'ID corrente.

UpID sarà sempre - (larghezza totale della mappa) dell'ID corrente e downID sarà sempre + (larghezza totale della mappa) dell'ID corrente, ad eccezione di zero, il che significa che hai toccato il bordo.

Una singola mappa può essere suddivisa in più mappe e salvata in sequenza con mapIDXXXX dove XXXX è l'id. In questo modo, non c'è nulla di cui essere confusi. Sono stato sul tuo sito e sembra, potrei sbagliarmi, che il problema sia dovuto al tuo editor di mappe che impone una limitazione delle dimensioni che sta impedendo alla tua automazione di scomporre una grande mappa in più piccole su salvataggio e questa limitazione è probabilmente limitando una soluzione tecnica.

Ho scritto un editor di mappe Javascript che scorre in tutte le direzioni, tessere 1000 x 1000 (non consiglierei quella dimensione) e si muove ancora bene come una mappa 50 x 50 e cioè con 3 livelli incluso lo scorrimento della parallasse. Salva e legge in formato json. Ti invierò una copia se dici che non ti dispiace un'e-mail di circa 2meg. È pensato per essere su Github ma non ho un set di tessere decente (legale), motivo per cui non mi sono ancora preso la briga di metterlo lì.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.