2D-Grafik 2D-GUI mit vielen .png

D

Des

Gast
Hallo!

Ich möchte gerne ein 2D-Strategiespiel programmieren und scheitere gerade an der Performance der GUI. Zur Einordnung: Im Moment funktioniert alles, es ist nur zu langsam.

Problemstellung: Ich habe viele Bilder 64x64 mit transparenten Texturen. Diese liegen zur Zeit alle im .png Format vor. Im groben werden diese 64x64 Bilder nun aneinandergepflastert und ergeben damit eine Landkarte. Je nach Spielfortschritt ändern sich aber Kleinigkeiten an den Texturen (Rohstoffe wurden erschlossen), sodass aktuell ein 64x64 Tile mit einem anderen überlagert wird, wenn der Spielfortschritt es erfordert. Man sieht dann also ein zum größten Teil transparentes 64x64 Bild über dem 64x64 Grundfeld liegen. Je nach Spielverlauf sind das viele Bilder übereinander (~10 Stück). Der Benutzer sieht nun einen Ausschnitt der Landkarte und die Karte soll scollbar sein.

Aktuelle Lösung (bitte nicht steinigen): Ich benutze ein JScollPane auf dem die gesamte Karte liegt. Dieses JScrollPane wird verschoben, wenn sich die Maus in einem definierten Bereich befindet. Meine Landkarte ist datenstrukturtechnisch in einem 2D-Array von 64x64 Feldern gespeichert und jeder Eintrag der Matrix steht für ein 64x64 Feld.

Immer, wenn sich aufgrund der Spielsituation etwas an der Darstellung der Karte ändert, rufe ich updateImage() auf den sich verändernden Feldern auf. Diese Methode macht eine deepCopy vom eigentlichen Grundfeld und fügt dieses mit den ganzen überlagernden Bildern mittels mergeImage() zusammen und speichert das fertige Image als Datenfeld in der Matrix. Je nachdem, was da noch so auf der Karte ist, muss ich manchmal noch eine schwarze Linie einzeichnen:
Java:
Graphics2D g2d = image.createGraphics();
	g2d.setColor(Color.BLACK);
	if ("blabla")	g2d.drawLine(33, 0, 63, 0);

Jedes dieser 64x64 Felder kennt also sein aktuelles Bild, was die GUI anzeigen soll. Wenn nun das JScrollPane verschoben wird, male ich den aktuell sichtbaren Bereich neu.

Wie genau? Meine Landkarte liegt auf einem JPanel. Dieses JPanel ist aud dem JScrollPane. Ich arbeite mit einem OffscreenImage (buffering). ich habe PaintComponent meiner Landkarte wie folgt überschrieben:

Java:
public void paintComponent (Graphics g) {
            //this clears the offscreen image, not the onscreen one
            offscreenGraphics.clearRect(0, 0, width, height);
            //calls the paintbuffer method with
            //the offscreen graphics as a param
            paintBuffer(offscreenGraphics, g);
            //we finaly paint the offscreen image onto the onscreen image
            g.drawImage(offscreenImage, 0, 0, null);
}

Was macht nun paintBuffer? Es geht alle Felder, die neu sichtbar sein sollen durch und malt alle Bilder: (Für alle x,y, die sichtbar sind: (das sind aktuell ca. 100 Stück, sollen aber mal deutlich mehr werden))

Java:
public void paintBuffer(final Graphics g, final Graphics onscreenGraphics) {
     BufferedImage image = tileMatrix[x][y].getImage();
     g.drawImage(image, y * 64 + x % 2 * 32, x * 64, null);
}

Problem: Je nach Leistungsfähigkeit des Computers dauert das Scrollen sehr lange. Wieso? Keine Ahnung! Ist der Ansatz zu langsam oder habe ich irgendwo einen Fehler drin?

Problem 2: Die schwarzen Linien, die ich male sind nicht immer schwarz, manchmal werden sie aus irgendeinem Grund in einer Farbe eines speziellen Pixels der fertigen 64x64 Bilder gezeichnet.


Soweit die Beschreibung, ich höre mal auf, sonst liest es sich niemand mehr durch. :p
Danke!
 

schlingel

Gesperrter Benutzer
Du zeichnest immer und immer wieder viele, viele Bilder obwohl es nicht nötig ist.

So auf die Schnelle ein Vorschlag:

Du zeichnest das aktuelle Bild in ein Bitmap dass deinen Screen darstellt und überzeichnest die entsprechenden Stellen nur wenn sich etwas ändert.

Beim Scrollen musst du die Logik dann so anpassen, dass der vorhandene Bildausschnitt weiter verwendet wird und du in das neue Bitmap das alte - Offset zeichnest und im Offset Bereich die neuen sichtbaren Elemente dazu zeichnest.

So sollte der Aufwand zu beim Zeichnen drastisch schrumpfen.
 
D

Des

Gast
Hmm ich weiß nicht, ob ich es verstehe. Meinst du, ich speichere mir das alte Offscreen-Image ab und mache dann beim Scrollen:

public void paintBuffer(final Graphics g, final Graphics onscreenGraphics) {
FÜR ALLE NEU SICHTBAREN:
BufferedImage image = tileMatrix[x][y].getImage();
g.drawImage(image, y * 64 + x % 2 * 32, x * 64, null);

d.drawImage(altesOffscreenImage.getSubimage(...), ...); //alte Felder
}
 

Marco13

Top Contributor
Ja, viel Beschreibung, aber trotzdem (für mich) nicht im Detail nachvollziehbar.

JScrollPane verwendet schon einige Tricks, um das Scrollen schnell zu machen, ggf. kannst du dich mal bei JViewport (Java Platform SE 7 ) umsehen.

Das Zeichnen der "Mehrlagigen" Bilder klingt kompliziert, vielleicht könnte man das nochmal .... "fokussierter" zusammenfassen.

Allgemein könnte es aber nicht schaden,
1. Die eingelesenen Bilder mit den Snippet aus http://www.java-forum.org/spiele-mu...18-performance-bufferedimages.html#post878295 in ein passendes Format zu bringen
2. Auf "getSubImage" zu verzichten, aus den in diesem Thread weiter oben genannten Gründen (man erkennt dort ja jetzt z.B. nicht, was mit diesem getSubImage genau ausgeschnitten wird...)
 
D

Des

Gast
Naja. Ich versuche die mehrlagigen Bilder zu umgehen indem ich die nur übereinanderlege, wenn sich wirklich etwas am Spiel ändert (Also nicht beim Zeichnen, sondern wirklich nur, wenn der Benutzer eine Aktion startet.). Das sieht dann so aus:

Java:
static private BufferedImage mergeImage(BufferedImage first, BufferedImage second) {
  // create the new image, canvas size is the max. of both image sizes
  int w = Math.max(first.getWidth(), second.getWidth());
  int h = Math.max(first.getHeight(), second.getHeight());
  BufferedImage combined = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
	
  // paint both images, preserving the alpha channels
  Graphics g = combined.getGraphics();
	
  g.drawImage(first, 0, 0, null);
  g.drawImage(second, 0, 0, null);
	
  // return new image
  return combined;
}

Die Idee ist also beim Scrollen jetzt nicht für jedes Feld alle überlagerten Bilder zu malen, sondern quasi schon für jedes Feld ein fertiges Image im Arbeitsspeicher zu haben, was man direkt malen kann. Ich hoffe, das passiert auch hier. (Eigentlich mache ich damit auch das, was das Snippet will, oder?)^^

Was passiert jetzt, wenn das JScrollPane scrollt? Es ruft repaint() auf meiner Landkarte auf, die als JPanel da drin liegt. (Wenn ich das JScrollPane richtig verstanden habe.) Dieses repaint() muss aber nicht die ganze Landkarte neu zeichnen, sondern eben nur den aktuell sichtbaren Teil. Wenn ich jetzt Schlingels Idee richtig verstanden habe, speichere ich das alte OffsceenImage als Image-Datenfeld ab und male den jetzt nach dem Scrollvorgang noch sichtbaren Teil des alten Bildes alles zusammen neu (den bekomme ich mit getSubimage(Koordinaten des noch sichtbaren Bereiches)) und zeichne am Rand meine neu sichtbaren Felder dran.

Ich hoffe das hilft fürs Verständnis. Habe ich Schlingels Idee jetzt richtig verstanden?
 

Marco13

Top Contributor
Hmja... Auch wenn ich es IMMERnoch nicht ganz einordnen kann: Dieses "combined"-Image jedes mal neu zu erstellen könnte man sich ggf. sparen: Dessen Größe ändert sich doch vermutlich nicht? Da könnte man eins erstellen und immer wieder übermalen.
Das hat aber wohl nicht so viel Einfluß auf die Zeichengeschwindigkeit an sich. Diese könnte eher durch getSubimage negativ beeinflusst werden (siehe verlinkter Thread)
 
D

Des

Gast
Ich habe mal versucht das Problem genauer zu lokalisieren und die Zeit beim Malen zu messen. Ich denke ich habe ein Problem beim gepufferten Malen. Macht man das so? Hier ist nochmal ein Codeausschnitt aus den Methoden meines JPanels (mit Zeitmessung):

Java:
	@Override
	public void paintComponent (Graphics g) {
        // checks the buffersize with the current panelsize
        // or initialises the image with the first paint
        if (width != getSize().width
                || height != getSize().height
                || offscreenImage == null || offscreenGraphics == null) {
            resetBuffer();
        }
        if (offscreenGraphics != null) {
        	long vorher = System.currentTimeMillis();
            //this clears the offscreen image, not the onscreen one
            offscreenGraphics.clearRect(0, 0, width, height);
            //calls the paintbuffer method with
            //the offscreen graphics as a param
            paintBuffer(offscreenGraphics, g);
            long mitte = System.currentTimeMillis();
            //we finaly paint the offscreen image onto the onscreen image
            g.drawImage(offscreenImage, 0, 0, null);
            long nachher = System.currentTimeMillis();
            
            System.out.println("BufferMalen:  "+ (mitte - vorher));
            System.out.println("OnScreenImage:  "+ (nachher - mitte));
      }  
    }
Java:
    private void resetBuffer() {
        // always keep track of the image size
        width = getSize().width;
        height = getSize().height;
        // clean up the previous image
        if (offscreenGraphics != null) {
            offscreenGraphics.dispose();
        }
        if (offscreenImage != null) {
            offscreenImage.flush();
        }
        // create the new image with the size of the panel
        offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        offscreenGraphics = offscreenImage.createGraphics();
    }
Java:
    public void paintBuffer(final Graphics g, final Graphics onscreenGraphics) {
          //hier passieren die eigentlichen g.drawImage-Aufrufe für meine ganzen Bilder
    }

Ausgabe auf der Konsole:
Java:
BufferMalen:  65
OnScreenImage:  124
BufferMalen:  64
OnScreenImage:  141
BufferMalen:  79
OnScreenImage:  125
BufferMalen:  65
OnScreenImage:  124
BufferMalen:  64
OnScreenImage:  125
BufferMalen:  79
OnScreenImage:  125
BufferMalen:  65
OnScreenImage:  124
BufferMalen:  64
OnScreenImage:  125
BufferMalen:  64
OnScreenImage:  125
BufferMalen:  80
OnScreenImage:  124
BufferMalen:  64
OnScreenImage:  125
BufferMalen:  64
OnScreenImage:  125
BufferMalen:  64
OnScreenImage:  125
BufferMalen:  79
OnScreenImage:  125
BufferMalen:  79
OnScreenImage:  125
BufferMalen:  64
OnScreenImage:  125

Ergebnis: Das Übertragen des OffscreenImages auf den Bildschirm dauert extrem lange.
--->Fehler gefunden! Mein Offscreen Image hat nicht Größe meines Bildschirms, sondern Größe meines Panels. Manchmal kanns so einfach sein.

Laufzeittest:
Java:
BufferMalen:  16
OnScreenImage:  1
BufferMalen:  16
OnScreenImage:  2
BufferMalen:  17
OnScreenImage:  1
BufferMalen:  0
OnScreenImage:  16
BufferMalen:  15
OnScreenImage:  1
BufferMalen:  16
OnScreenImage:  1
BufferMalen:  17
OnScreenImage:  1
BufferMalen:  16
OnScreenImage:  1
BufferMalen:  17
OnScreenImage:  2
BufferMalen:  16
OnScreenImage:  1
BufferMalen:  17
OnScreenImage:  1
BufferMalen:  18
OnScreenImage:  0
BufferMalen:  17
OnScreenImage:  0

hihi, danke euch! (Nehme aber trotzdem noch weiterhin Anmerkungen entgegen.^^)
 

L-ectron-X

Gesperrter Benutzer
Sieht für mich so aus, als würdest du versuchen, hier was zu doublebuffern?
In Swing sind doch die Komponenten bereits gepuffert. Wozu arbeitest du dann mit einem offscreen-Image?
 
D

Des

Gast
Ich dachte, das wäre sinnvoll, weil ich eben etwas aufwendigere Zeichenoperationen in meinem paintComponent(Graphics g) mache, die länger dauern als die Bildwiederholrate des Bildschirms. Daher die Idee erst alles im Hintergrund zu zeichnen um es dann schneller anzuzeigen.
 

Marco13

Top Contributor
@L-ectron-X: Ja, das klang etwas dubios und undurchsichtig, aber ... ich hatte das jetzt so verstanden, dass er VIELE Bilder übereinander zeichnet. Wenn man sich irgendeinen Extremfall vorstellt, z.B. 10x10 Tiles wo jedes Tile einen Stapel von 50 Bildern enthält (wo man ggf. nur das oberste sieht, aber evtl. durch Alpha in den einzelnen Bildern auch eine Mischung aus diesen 50 Bildern) könnte es sinnvoll sein, für schnelle Bewegungen nur das eine, große, fertige Bild zu malen, statt der 10x10x50 Bilder jedes mal komplett. Aber wie oben schon angedeutet: So ganz habe ich vielleicht noch nicht durchschaut, was dort eigentlich gemacht werden soll... :bahnhof:
 

Ähnliche Java Themen


Oben