Zarządzanie mapami tekstowymi w układzie 2D, które zostaną namalowane na płótnie HTML5


46

Więc tworzę RPG HTML5 dla zabawy. Mapa jest <canvas>(szerokość 512 pikseli, wysokość 352 pikseli | 16 płytek w poprzek, 11 płytek od góry do dołu). Chcę wiedzieć, czy istnieje bardziej skuteczny sposób na pomalowanie <canvas>.

Oto jak mam to teraz.

Jak płytki są ładowane i malowane na mapie

Mapa jest malowana kafelkami (32x32) za pomocą tego Image()kawałka. Pliki obrazów są ładowane przez prostą forpętlę i umieszczane w tablicy wywoływanej tiles[]jako MALOWANE przy użyciu drawImage().

Najpierw ładujemy kafelki ...

wprowadź opis zdjęcia tutaj

a oto jak to się robi:

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

Oczywiście, gdy gracz rozpoczyna grę, ładuje mapę, którą ostatnio przerwał. Ale tutaj jest to mapa z trawą.

W tej chwili mapy używają tablic 2D. Oto przykładowa 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]];

Dostaję różne mapy za pomocą prostej ifstruktury. Gdy powyższa tablica 2d będzie return, odpowiednia liczba w każdej tablicy zostanie pomalowana zgodnie z Image()zapisem w środku tile[]. Potem drawImage()nastąpi i pomaluj zgodnie z xi yi 32pomaluj na właściwej x-ywspółrzędnej.

Jak występuje przełączanie wielu map

Z mojej gry, mapy mają pięć rzeczy do śledzenia: currentID, leftID, rightID, upID, i bottomID.

  • currentID: bieżący identyfikator mapy, na której jesteś.
  • leftID: Jaki identyfikator currentIDzaładować po wyjściu z lewej strony bieżącej mapy.
  • rightID: Jaki identyfikator currentIDzaładować po wyjściu z prawej strony bieżącej mapy.
  • downID: Jaki identyfikator currentIDzaładować po wyjściu z dolnej części bieżącej mapy.
  • upID: Jaki identyfikator currentIDzaładować po wyjściu na górę bieżącej mapy.

Coś do uwaga: Jeśli któraś leftID, rightID, upID, lub bottomIDnie są specyficzne, co oznacza, że są one 0. Oznacza to, że nie mogą opuścić tej strony mapy. To tylko niewidzialna blokada.

Tak więc, gdy dana osoba wyjdzie z boku mapy, w zależności od tego, gdzie wyszła ... na przykład jeśli wyszła na dole, bottomIDliczba mapładowanych osób zostanie w ten sposób namalowana na mapie.

Oto reprezentatywny plik .GIF, który pomoże ci lepiej wizualizować:

wprowadź opis zdjęcia tutaj

Jak widać, wcześniej czy później na wielu mapach będę miał do czynienia z wieloma identyfikatorami. A to może być trochę mylące i nerwowe.

Oczywiste zalety to to, że ładuje 176 kafelków jednocześnie, odświeża małe płótno 512x352 i obsługuje jedną mapę na raz. Wadą jest to, że identyfikatory MAP, gdy mamy do czynienia z wieloma mapami, mogą czasami być mylące.

Moje pytanie

  • Czy to skuteczny sposób przechowywania map (biorąc pod uwagę użycie kafelków), czy jest lepszy sposób obsługi map?

Myślałem o wielkiej mapie. Rozmiar mapy jest duży i składa się z jednej tablicy 2D. Jednak rzutnia ma wciąż 512 x 352 pikseli.

Oto kolejny plik .gif, który zrobiłem (na to pytanie), aby pomóc w wizualizacji:

wprowadź opis zdjęcia tutaj

Przepraszam, jeśli nie rozumiesz mojego angielskiego. Proszę pytać o wszystko, co masz problemy ze zrozumieniem. Mam nadzieję, że wyjaśniłem to. Dzięki.


16
Wysiłek włożony w to pytanie z grafiką i wszystko zasługuje na aprobatę. :)
Tapio,

Odpowiedzi:


5

Edycja: Właśnie zobaczyłem, że moja odpowiedź była oparta na twoim kodzie, ale tak naprawdę nie odpowiedziała na twoje pytanie. Zachowałem starą odpowiedź na wypadek, gdybyś mógł skorzystać z tych informacji.

Edycja 2: Naprawiłem 2 problemy z oryginalnym kodem: - Dodanie +1 w obliczeniach końca x i y było przez pomyłkę w nawiasach, ale należy je dodać po podziale. - Zapomniałem zweryfikować wartości xiy.

Możesz po prostu mieć aktualną mapę w pamięci i załadować otaczające ją mapy. Wyobraź sobie świat, który składa się z 2d tablicy pojedynczych poziomów o rozmiarze 5x5. Gracz zaczyna w polu 1. Ponieważ górna i lewa granica poziomu znajdują się na skraju świata, nie trzeba ich ładować. W takim przypadku poziom 1/1 jest aktywny, a poziomy 1/2 i 2/1 są ładowane ... Jeśli gracz przesunie się teraz w prawo, wszystkie poziomy (oprócz tego, do którego się przeprowadzasz) są rozładowywane, a nowe otoczenie jest załadowany. Oznacza, że ​​poziom 2/1 jest teraz aktywny, 1/1, 2/2 i 3/1 są teraz załadowane.

Mam nadzieję, że daje to wyobrażenie o tym, jak można to zrobić. Ale to podejście nie zadziała bardzo dobrze, gdy każdy poziom musi być symulowany podczas gry. Ale jeśli możesz zamrozić nieużywane poziomy, powinno to działać dobrze.

Stara odpowiedź:

Podczas renderowania poziomu (również na podstawie kafelków) robię to, aby obliczyć, które elementy z tablicy kafelków przecinają się z rzutnią, a następnie renderuję tylko te kafelki. W ten sposób możesz mieć duże mapy, ale wystarczy wyrenderować fragment na ekranie.

//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++)
          [...]

Uwaga: powyższy kod nie jest testowany, ale powinien dać wyobrażenie o tym, co robić.

Po podstawowym przykładzie reprezentacji rzutni:

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

Tak wyglądałaby reprezentacja javascript

<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>

Jeśli weźmiesz mój przykład kodu, wystarczy zastąpić deklaracje int i float var. Upewnij się także, że używasz Math.floor do tych obliczeń, które są przypisane do wartości opartych na int, aby uzyskać takie samo zachowanie.

Jedną rzeczą, którą powinienem rozważyć (choć nie jestem pewien, czy to ma znaczenie w javascript), jest umieszczenie wszystkich kafelków (lub jak największej ich liczby) w jednej dużej teksturze zamiast używania wielu pojedynczych plików.

Oto link wyjaśniający, jak to zrobić: http://thiscouldbebetter.wordpress.com/2012/02/25/slicing-an-image-into-tiles-in-javascript/


Żeby wyjaśnić ... viewport.x byłby predefiniowany jako „531”, a viewport.y miałby wartość „352”, a tileize = = 32. Oraz taki..?
przetestuj

Jeśli masz na myśli, że pozycja kamery to 531, to tak. Dodałem kilka komentarzy do powyższego kodu.
tom van green

Używam JavaScript ... Jestem Java jest prawie identyczny z tym.
test

Tak, po prostu musisz stworzyć klasę rzutni opartą na js (lub możesz po prostu utworzyć 4 zmienne x, y, wih). Musisz także upewnić się, że podłoga (Math.floor) obliczeń, które są przypisane do wartości int. Dodałem przykład kodu, jak wyglądałaby klasa rzutni. Upewnij się tylko, że umieścisz deklarację przed użyciem.
tom van green

Mówiąc tylko, że umieściłeś 640, 480... ale to może działać z 512, 352prawdą?
przetestuj

3

Przechowywanie map jako kafelków jest przydatne, aby umożliwić ponowne wykorzystanie zasobów i ponowne zaprojektowanie mapy. Realistycznie jednak nie zapewnia tak wiele korzyści graczowi. Ponadto za każdym razem rysujesz ponownie każdą płytkę. Oto co bym zrobił:

Przechowuj całą mapę jako jedną dużą tablicę. Nie ma sensu mieć map i submaps, a także potencjalnie pod-map (np. Jeśli wchodzisz do budynków). W każdym razie ładujesz to wszystko, dzięki czemu wszystko jest ładne i proste.

Renderuj mapę naraz. Przygotuj płótno poza ekranem, na którym renderujesz mapę, a następnie wykorzystaj je w swojej grze. Ta strona pokazuje, jak to zrobić za pomocą prostej funkcji javascript:

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

Zakłada się, że twoja mapa niewiele się zmieni. Jeśli chcesz renderować mapy podrzędne w miejscu (na przykład wnętrze budynku), po prostu renderuj to również na płótnie, a następnie narysuj na wierzchu. Oto przykład, w jaki sposób możesz użyć tego do renderowania mapy:

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

Spowoduje to narysowanie niewielkiej części mapy wokół mapCentreX, mapCentreY. Umożliwi to płynne przewijanie całej mapy, a także ruch pod kafelkami - możesz zapisać pozycję gracza jako 1/32 części kafelka, dzięki czemu możesz uzyskać płynny ruch po mapie (oczywiście, jeśli chcesz przesuń płytkę po płytce jako stylistyczny wybór, możesz po prostu zwiększyć o 32 części.

Jeśli chcesz, możesz jeszcze podzielić mapę lub jeśli twój świat jest ogromny i nie zmieściłby się w pamięci w systemach docelowych (pamiętaj, że nawet dla 1 bajtu na piksel mapa 512 x 352 ma 176 KB), ale wcześniej -renderując mapę w ten sposób, zauważysz duży wzrost wydajności w większości przeglądarek.

Zapewnia to elastyczność ponownego wykorzystywania kafelków na całym świecie bez konieczności edytowania jednego dużego obrazu, ale umożliwia także szybkie i łatwe uruchamianie tego kafelka i rozważenie tylko jednej „mapy” (lub tylu „map”, ile chcesz) .



1

Łatwym sposobem na śledzenie identyfikatorów byłoby po prostu użycie innej tablicy 2d z nimi: utrzymując x, y na tej „meta-tablicy”, możesz łatwo wyszukać inne identyfikatory.

Biorąc to pod uwagę, oto podejście Google do gry 2D na kafelkach na płótnie (no cóż, właściwie multiplayer HTML5, ale silnik kafelkowy jest również objęty) z ich ostatniego we / wy 2012: http://www.youtube.com/watch?v=Prkyd5n0P7k It dostępny jest również kod źródłowy.


1

Twój pomysł, aby najpierw załadować całą mapę do tablicy, jest skutecznym sposobem, ale łatwo będzie uzyskać JavaScript Injection'd, każdy będzie mógł poznać mapę i tam jest przygoda.

Pracuję również nad grą RPG w Internecie, sposób w jaki ładuję moją mapę to Ajax ze strony PHP, która ładuje mapę z bazy danych, gdzie jest napisane X, Y, pozycja Z i vlaue płytki, narysuj ją, a na koniec gracz ruszy się.

Nadal pracuję nad tym, aby uczynić go bardziej wydajnym, ale mapa ładuje się wystarczająco szybko, aby utrzymać go w ten sposób.


1
Czy mogę zobaczyć wersję demo?
testuj

1

Nie , tablica jest najbardziej efektywnym sposobem przechowywania mapy kafelków .

Może istnieć kilka struktur, które wymagają nieco mniej pamięci, ale tylko kosztem znacznie wyższych kosztów ładowania i dostępu do danych.


Należy jednak wziąć pod uwagę, kiedy zamierzasz załadować następną mapę. Biorąc pod uwagę, że mapy nie są szczególnie duże, rzeczą, która faktycznie zajmie najdłużej, jest czekanie, aż serwer dostarczy mapę. Rzeczywiste ładowanie zajmie tylko kilka milisekund. Ale to oczekiwanie może odbywać się asynchronicznie, nie musi blokować przepływu gry. Najlepszą rzeczą jest nie ładowanie danych w razie potrzeby, ale wcześniej. Gracz jest na jednej mapie, teraz załaduj wszystkie sąsiednie mapy. Gracz porusza się na następnej mapie, ponownie ładuje wszystkie sąsiednie mapy itp. Itp. Gracz nawet nie zauważy, że gra się ładuje w tle.

W ten sposób możesz również stworzyć iluzję świata bez granic, rysując także sąsiednie mapy, w takim przypadku musisz załadować nie tylko sąsiednie mapy, ale także te sąsiadujące z nimi.


0

Nie jestem pewien, dlaczego tak myślisz: jak widzisz, wcześniej czy później na wielu mapach będę miał do czynienia z wieloma identyfikatorami. A to może być trochę mylące i nerwowe.

LeftID zawsze będzie wynosił -1 od currentID, rightID zawsze będzie +1 od currentID.

UpID zawsze będzie wynosił - (całkowita szerokość mapy) bieżącego ID, a downID zawsze będzie + (całkowita szerokość mapy) bieżącego ID, z wyjątkiem zera, co oznacza, że ​​trafiłeś na krawędź.

Pojedyncza mapa może zostać podzielona na wiele map i są zapisywane sekwencyjnie z mapIDXXXX, gdzie XXXX jest identyfikatorem. W ten sposób nie ma co się mylić. Byłem na twojej stronie i wydaje mi się, że mogę się mylić, że problem leży po stronie edytora map, który narzuca ograniczenie rozmiaru, które utrudnia twoją automatyzację dzielenia dużej mapy na wiele małych przy zapisywaniu, a to ograniczenie jest prawdopodobnie ograniczające rozwiązanie techniczne.

Napisałem edytor map Javascript, który przewija we wszystkich kierunkach, 1000 x 1000 (nie polecam tego rozmiaru) kafelków i nadal porusza się tak dobrze, jak mapa 50 x 50, czyli z 3 warstwami, w tym przewijaniem paralaksy. Zapisuje i odczytuje w formacie json. Wyślę ci kopię, jeśli powiesz, że nie przeszkadza ci wiadomość e-mail o wielkości około 2 meg. To ma być na githubie, ale nie mam przyzwoitego (legalnego) zestawu kafelków, dlatego nie zadałem sobie trudu, żeby go tam umieścić.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.