Sprite Library (in Anlehnung an Quaxli)

MLNW

Mitglied
Hallo erstmal,

Ich habe vor einigen Tagen dieses Tutorial von Quaxli gefunden und es einmal komplett durchgelesen, da ich gerade dabei bin eines meiner Projekte von vor einem Jahr komplett neu zu erstellen. Damals hatte ich gerade mal ein Jahr mit Java gearbeitet und dementsprechend schlecht war der Code (und wenn ich schlecht sage dann meine ich auch wirklich schlecht :oops:).

Da ich mein Projekt dabei gleich erweitern will und mit eigenen Animationen/Musik etc aufpeppen will hat mir das Tutorial von Quaxli in der Hinsicht sehr weitergeholfen. Deshalb habe ich mich auch für das Grundgerüst (Sprites etc) daran orientiert. Aber da ich wie gesagt das Projekt direkt auch erweiterbar aufbauen will habe ich mir anstatt, wie zuerst die ganzen Sprites im JPanel des Spiels abzulegen, eine eigene SpriteLib programmiert. Damit komme ich auch zu dem warum ich hier poste. Undzwar wollte ich das mal jemand drüber schaut und mir seine Meinung dazu sagt. Es ist für mich eben wichtig das die Datenstruktur stimmt, damit ich nicht dann in paar Wochen irgendwelche Probleme damit hab und wieder bei 0 Anfangen muss ... Danke schon mal im Vorraus!


Java:
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.Hashtable;
import java.util.Vector;
import javax.imageio.ImageIO;
import data.Sprite;

/**
 * Eine Sprite Library in der verschiedenste Sprites, Bilder und Animationen
 * gespeichert werden können. Dient als Datenbank für das Spielgeschehen.
 * 
 * @author Lucas
 * @version 0.1
 */
public class SpriteLib {

    /**
     * Speichert alle geladenen Sprites mit einem dazugehörigen Key.
     */
    private Hashtable<String, Sprite> sprites;
    
    /**
     * Speichert alle geladenen Bilder. Hierraus können entweder Animationen
     * erstellt werden oder diese direkt verwendet werden.
     */
    private Hashtable<String, BufferedImage> pics;
    
    /**
     * Speichert alle erstellten Animationen mitsamt einem Key über den diese
     * Abgerufen werden können. Somit können für ein Sprite auch verschiedene
     * Animationen benutzt werden, aber auch Animationen für andere Objekte
     * benutzt werden (e.g. Explosionen)
     */
    private Hashtable<String, BufferedImage[]> animations;

    /**
     * Instanziiert die drei Hashtables der SpriteLib.
     */
    public SpriteLib() {
	sprites = new Hashtable<String, Sprite>();
	pics = new Hashtable<String, BufferedImage>();
	animations = new Hashtable<String, BufferedImage[]>();
    }

    /**
     * Lädt ein Bild aus dem Angebenen Pfad in die Datenbank ein und gibt dieses
     * auch zurück.
     * 
     * @param key
     *            Name des Pics unter dem es in die DB abgelegt werden soll
     * @param path
     *            Pfad des Bildes ("pics/xxxxx.xxx")
     * @return Gibt das geladene Bild zurück
     */
    public BufferedImage loadPic(String key, String path) {
	BufferedImage source = null;
	URL pic_url = getClass().getClassLoader().getResource(path);

	try {
	    source = ImageIO.read(pic_url);
	} catch (IOException e) {

	}
	this.pics.put(key, source);
	return source;
    }

    /**
     * Lädt ein Image aus dem angegebenen Pfad aus. Erstellt daraus dann eine
     * Animation und gibt diese zurück.
     * 
     * @param key
     *            Key für die Hashtabelle
     * @param path
     *            Pfad der Animation
     * @param pics
     *            Anzahl der Einzelbilder
     * @return Die erstellte Animation
     */
    public BufferedImage[] loadAnimation(String key, String path, int pics) {
	BufferedImage source = null;
	URL pic_url = getClass().getClassLoader().getResource(path);
	try {
	    source = ImageIO.read(pic_url);
	} catch (IOException e) {

	}
	return createAnimatedImage(key, source, pics);
    }

    /**
     * Erstellt aus einem einfachen Bild mit mehreren Einzelbildern eine
     * Animation die zurückgegeben wird und in der DB abgespeichert wird.
     * 
     * @param key
     *            Name für die Datenbank
     * @param source
     *            Die original Bilddatei
     * @param pics
     *            Die Anzahl der Einzelbilder
     * @return Die erstellte Animation
     */
    public BufferedImage[] createAnimatedImage(String key,
	    BufferedImage source, int pics) {
	BufferedImage[] anim = new BufferedImage[pics];
	for (int x = 0; x < pics; x++) {
	    anim[x] = source.getSubimage(x * source.getWidth() / pics, 0,
		    source.getWidth() / pics, source.getHeight());
	}
	animations.put(key, anim);
	return anim;
    }

    /**
     * Wandelt die vorhandene Hashtable der Sprites in einen Vector um der alle
     * gespeicherten Sprites dieser SpriteLib beinhaltet und gibt diesen zurück.
     * 
     * @return einen Vector mit allen Sprites dieser SpriteLib
     */
    public Vector<Sprite> getSprites() {
	Vector<Sprite> temp = new Vector<Sprite>();
	Sprite[] tmp = new Sprite[sprites.size()];
	sprites.values().toArray(tmp);
	for (int i = 0; i < sprites.size(); i++) {
	    temp.add(tmp[i]);
	}
	return temp;
    }

    public BufferedImage getPic(String key) {
	return pics.get(key);
    }

    public void removePic(String key) {
	pics.remove(key);
    }

    public BufferedImage[] getAnimation(String key) {
	return animations.get(key);
    }

    public void removeAnimation(String key) {
	animations.remove(key);
    }

    public Sprite getSprite(String key) {
	return sprites.get(key);
    }

    public void addSprite(String key, Sprite sprite) {
	sprites.put(key, sprite);
    }

}
 

Fu3L

Top Contributor
Ein Punkt wäre, dass man möglichst gegen das Interface programmiert, also Map<String, Sprite> bei der Deklaration, ebenso beim Vector. Lieber eine List zurückgeben.
Desweiteren könntest du eventuell HashMap und LinkedList/ArrayList nutzen, diese sind nicht synchronisiert und daher eventuell etwas schneller, wenn synchronisation nicht nötig ist (also vor allem, wenn nachträglich kein Bild geändert werden kann). Dies zeigt auch schon: Wenn du direkt die Interfaces verwendest, müsstest dus kaum ändern.

Ein anderer sehr wichtiger Punkt: Direkt nach dem Laden eines Bildes solltest du es in ein neues BufferedImage vom Typ TYPE_INT_ARGB malen, da der Typ nach dem Laden sehr langsam ist. (Solltest du keine Transparenz wollen, einfach das A weglassen -> Vermutlich noch schneller)

Außerdem kannst du dir die zusätzliche Implementierung von loadPic sparen, ruf dort einfach loadAnim mit 1 auf und lass dir das eine Bild mit [0] hintendran geben ;)

Kleine Anregung, wenn du es noch erweitern wolltest: Meine SpriteLib hat noch eine Funktion, um mir Bilder skaliert zu geben, kann ganz praktisch sein ;)

Sollte etwas zu unklar formuliert sein, einfach schreiben was genau unklar ist ;)
 

MLNW

Mitglied
Ein Punkt wäre, dass man möglichst gegen das Interface programmiert, also Map<String, Sprite> bei der Deklaration, ebenso beim Vector. Lieber eine List zurückgeben.
Desweiteren könntest du eventuell HashMap und LinkedList/ArrayList nutzen, diese sind nicht synchronisiert und daher eventuell etwas schneller, wenn synchronisation nicht nötig ist (also vor allem, wenn nachträglich kein Bild geändert werden kann). Dies zeigt auch schon: Wenn du direkt die Interfaces verwendest, müsstest dus kaum ändern.


Danke erstmal für die schnelle Antwort. Leider sind ein paar Sachen doch etwas unklar für mich jetzt. Erst mal was genau meinst du mit "gegen das Interface programmieren"? Um ehrlich zu sein benutzte ich diese Hashtabellen zum ersten mal hier, und weiß daher auch nicht ganz genau inwiefern die Perfomance sich zu anderen Datensätzen unterscheiden kann.

Ich gehe mal davon aus, dass du mit nachträglich ändern meinst das die Bilder nur einmal geladen werden und danach nicht weiter bearbeitet werden. Das ist soweit der Fall.

Ein anderer sehr wichtiger Punkt: Direkt nach dem Laden eines Bildes solltest du es in ein neues BufferedImage vom Typ TYPE_INT_ARGB malen, da der Typ nach dem Laden sehr langsam ist. (Solltest du keine Transparenz wollen, einfach das A weglassen -> Vermutlich noch schneller)


Ansonsten habe ich auch bisher noch keine BufferedImages benutzt und kann mit dem Typ nicht viel anfangen. Das ARGB steht wohl für den Alphawert? Doch was damit gemeint ist das der Typ nach dem Laden sehr langsam ist weiß ich nicht genau. Transparenz muss aufjedenfall gegeben sein, ansonsten hätten ja alle Sprites einen hässlichen weißen Hintergrund oder nicht?
 

Fu3L

Top Contributor
ansonsten hätten ja alle Sprites einen hässlichen weißen Hintergrund oder nicht?

jopp, also brauchst du ARGB ;)

Wenn du nach dem Laden das Bild in der Variablen pic hast, könntest du so vorgehen:

Java:
BufferedImage betterPic = new BufferedImage(pic.getWidth, pic.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = betterPic.getGraphics();
g.drawImage(pic, 0, 0, null);

Danach hast du ein beim Zeichnen wesentlich performanteres Image.

Zu den Interfaces:
Also es gibt ja verschiedene Sorten von Maps, wie HashMap, TreeMap, Hashtable und so weiter. Hashtable hab ich noch nie im Einsatz gesehen, HashMaps sind wohl gebräuchlicher? (Hilfe Marco oder sonst wer^^). So, wenn man jezz gerne nicht mehr HashMap, sondern TreeMap nutzen würde, weil man Sortierung braucht, müsste man gleich alles ändern, was relativ doof ist.
Deswegen nutzt man das Interface Map:

Java:
private Map<String, Sprite> sprites;

//Irgendwo anders oder direkt dahinter:
sprites = new HashMap<String, Sprite>();

Danach verwendet man sprites wie jede andere Map auch. Will man nun eine TreeMap nutzen, muss man nur die eine Zeile am Ende des Beispiels ändern.
Gleiches bei List<IrgendeinObject>: Vector, ArrayList, LinkedList können dadurch alle gleich verwendet und bei veränderten Anforderungen leichter ausgetauscht werden^^
Am besten auch mal in der JavaAPI gucken, wie das zusammenhängt und mal in "Java ist auch eine Insel" lesen zu den Collections-klassen (ist ein frei verfügbares Buch, einfach nach Googlen ;))
 

MLNW

Mitglied
jopp, also brauchst du ARGB ;)

Wenn du nach dem Laden das Bild in der Variablen pic hast, könntest du so vorgehen:

Java:
BufferedImage betterPic = new BufferedImage(pic.getWidth, pic.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = betterPic.getGraphics();
g.drawImage(pic, 0, 0, null);

Danach hast du ein beim Zeichnen wesentlich performanteres Image.

Also hier zu erst mal: Ohne das ich das ARGB habe sind die Bilder da durchsichtig wo ich sie haben will, da ich sie so in Photoshop erstellt habe. Also müsste es schon was an der Performance ausmachen um das zu ändern. Zweitens, soweit ich deinen Code interpretiere würde sich beim Zeichnen nix ändern, denn ..

Java:
Graphics g = betterPic.getGraphics();
g.drawImage(pic, 0, 0, null);

.. bei g.drawImage() benutzt du ja die variable pic als zu zeichnendes Bild. Wenn es jetzt nicht darum geht das der Grafikkontext von BetterPic benutzt wird, sollte sich doch nichts geändert haben oder?^^


Zu den Interfaces:
Also es gibt ja verschiedene Sorten von Maps, wie HashMap, TreeMap, Hashtable und so weiter. Hashtable hab ich noch nie im Einsatz gesehen, HashMaps sind wohl gebräuchlicher? (Hilfe Marco oder sonst wer^^). So, wenn man jezz gerne nicht mehr HashMap, sondern TreeMap nutzen würde, weil man Sortierung braucht, müsste man gleich alles ändern, was relativ doof ist.
Deswegen nutzt man das Interface Map:

Java:
private Map<String, Sprite> sprites;

//Irgendwo anders oder direkt dahinter:
sprites = new HashMap<String, Sprite>();

Danach verwendet man sprites wie jede andere Map auch. Will man nun eine TreeMap nutzen, muss man nur die eine Zeile am Ende des Beispiels ändern.
Gleiches bei List<IrgendeinObject>: Vector, ArrayList, LinkedList können dadurch alle gleich verwendet und bei veränderten Anforderungen leichter ausgetauscht werden^^
Am besten auch mal in der JavaAPI gucken, wie das zusammenhängt und mal in "Java ist auch eine Insel" lesen zu den Collections-klassen (ist ein frei verfügbares Buch, einfach nach Googlen ;))

Der Begriff einer Map ist mir bisher noch nicht untergekommen, und mein kurzes einlesen in die Java API hat mir jetzt auch kein Licht aufgetan. Ich denke mal es ist die grundlegende Datenstruktur für jegliche Listen o.ä.? ???:L

Die Hashtable selbst habe ich ja auch nur aus dem Tutorial mir beschafft. Dort war sie in der SoundLib in Verwendung.
 

Fu3L

Top Contributor

Marco13

Top Contributor
Hashtable hab ich noch nie im Einsatz gesehen, HashMaps sind wohl gebräuchlicher? (Hilfe Marco oder sonst wer^^).

Hashtable ist für Map das, was Vector für List ist: Eine Klasse mit der Funktionalität des Interfaces, die es aber schon gab, bevor die Collections-API (und damit das Interface) erstellt wurde - und die nur nachträglich angepasst wurde, um das Interface zu erfüllen. Wie Vector ist auch Hashtable synchronized, aber meistens braucht man das nicht. HashMap ist also tatsächlich in den meisten Fällen "besser". Wenn man eine synchronized Map braucht, könnte man Hashtable verwenden - speziell wenn man ansonsten nur das Interface verwendet
Map<K,V> map = new Hashtable<K,V>();
Oder dann eben eine
Map<K,V> map = Collections.synchronizedMap(new HashMap<K,V>());
 

MLNW

Mitglied
jopp, also brauchst du ARGB ;)

Wenn du nach dem Laden das Bild in der Variablen pic hast, könntest du so vorgehen:

Java:
BufferedImage betterPic = new BufferedImage(pic.getWidth, pic.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = betterPic.getGraphics();
g.drawImage(pic, 0, 0, null);

Danach hast du ein beim Zeichnen wesentlich performanteres Image.


Also um noch mal dadrauf zurück zu kommen: Wie realisiere ich das dann in meinem Code? Muss ich nur das Graphics-Object von diesem betterPics Image holen und damit das Bild was ich geladen habe zeichnen oder muss ich das geladene Bild in ein anderes Image reichzeichnen und dieses zeichnen lassen.

Vor allem habe ich bisher meine komplette Malarbeit im GamePanel abgelegt, so wies Quaxli in seinem Tutorial gemacht hat. Sollte ich diese Funktionen auch in die SpritesLib ablegen oder sind die dort wo sie sind besser platziert? Weiterhin hatte ich dran gedacht evtl. Doublebuffering anzuwenden, da das Spiel was ich programmiere wahrscheinlich ansonsten recht langsam laufen wird. So wies jetzt ist, wo außer zwei schwarzen Vierecken, einer geladenen Grafik und der FPS-Anzeige nichts abläuft, komme ich auf konstante 100 FPS, was ja wohl nicht all zu viel ist. Sobald ich erst mal den richtigen Spielalgorithmus implementiert habe, und da zu noch Kollisionsabfragen und evtl andere Animationen, Gegner etc simuliere, schätze ich mal das die FPS - Anzahl stark abnehmen wird, und bevor das ganze unspielbar wird, würde ich doch gerne von Anfang an die meiste Performance aus dem Code rausholen.
 

Marco13

Top Contributor
Also um noch mal dadrauf zurück zu kommen: Wie realisiere ich das dann in meinem Code?

Hier ist ein Snippet dazu drin: http://www.java-forum.org/spiele-mu...18-performance-bufferedimages.html#post803128 - eigentlich wird tatsächlich nur die eine ImageIO.read-Zeile zu einem Fünfzeiler, für den Rest des Programms sollte sich nichts ändern.

EDIT: BTW, was meinst du mit "Malarbeit"? Und DoubleBuffering hat nicht direkt was mit Performance zu tun (DoubleBuffering macht Swing schon automatisch). Beschreib' ggf. mal genauer, was du meinst...
 

Fu3L

Top Contributor
So wies jetzt ist, wo außer zwei schwarzen Vierecken, einer geladenen Grafik und der FPS-Anzeige nichts abläuft, komme ich auf konstante 100 FPS

Eventuell liegt das genau daran, dass dein BufferedImage vom falschen Typ ist. Ich erwähne es nicht ohne Grund ;) Du brauchst wirklich nur die 3 Zeilen Code von oben, beim Laden anhängen und dann anstatt pic das betterPic speichern und nutzen. Das wars.
Zeichnen sollte deine SpriteLib übrigends nicht, das tut im besten Fall ein Renderer (oder die Objekte haben ihre eigene Methode, wie bei Quaxlie (was aber nicht "best practice" ist, dennoch für Anfänger sehr praktisch)).

Edit: Außerdem ist es auch im Ermessen des AWT-Threads wie oft gezeichnet wird und FPS != Durchläufe des Game Loops, da repaint() nur einen Hinweis darstellt und kein Konkretes Zeichnen.
 

MLNW

Mitglied
Hier ist ein Snippet dazu drin: http://www.java-forum.org/spiele-mu...18-performance-bufferedimages.html#post803128 - eigentlich wird tatsächlich nur die eine ImageIO.read-Zeile zu einem Fünfzeiler, für den Rest des Programms sollte sich nichts ändern.

Java:
public BufferedImage[] loadAnimation(String key, File file, int pics) {
	BufferedImage source = null;
	try {
	    source = ImageIO.read(file);
	} catch (IOException e) {
	    System.out.println("[SpriteLib] Konnte kein Bild auslesen!");
	}
	return createAnimatedImage(key, source, pics);
    }

So sieht meine derzeitige meist genutzte Funktion aus, mit der ich meine Bilder in die Lib einfüge. Wie müsste denn jetzt der Code aussehen, damit ich dieses ARGB Bild abspeichern kann, vor allem da ich ja am Ende das Bild noch mal durch einen Algorithmus jage um eine Animation draus zu machen (sprich ein BufferedImage-Array mit den einzelnen Bildern.
Irgendwie sehe ich selbst nicht was ich anders machen müsste um das gewünschte Ergebniss zu bekommen :eek: vor allem da ich nicht sehr vertraut mit der ganzen BufferedImage Geschichte bin.

EDIT: BTW, was meinst du mit "Malarbeit"? Und DoubleBuffering hat nicht direkt was mit Performance zu tun (DoubleBuffering macht Swing schon automatisch). Beschreib' ggf. mal genauer, was du meinst...


Mit der Malarbeit meinte ich eigentlich die Paintmethoden und sonstige Paintsachen, zb drawObject etc. Und mit DoubleBuffering meinte ich eher die Bufferung von diesen Methoden und nicht den Swing-Komponenten. Ich habe auch schon vor einer Weile mitbekommen, dass man auch ein volatileImage erstellen kann und damit direkt die Grafikkarte ansprechen kann für etwaige Berechnungen und Anzeigen. Sowas wäre für mich schon sehr interessant, da wie ich schon mal gescrhieben habe denke das ich sonst irgendwann an eine Grenze stoße wo das ganze unspielbar wird und ich nicht erst dann rumfuhrwerken will


Fu3L hat gesagt.:
Edit: Außerdem ist es auch im Ermessen des AWT-Threads wie oft gezeichnet wird und FPS != Durchläufe des Game Loops, da repaint() nur einen Hinweis darstellt und kein Konkretes Zeichnen.


Das wusste ich, auch wenn ich ehrlich gesagt habe nicht dran gedacht habe, doch ich meinte nicht diese FPS sondern die richtigen. Und da ich eh das Zeichnen so umstellen wollte, dass der wirklich selbst zeichnet und nicht nur eine Meldung abschickt von wegen repaint(), sollte dies auch nicht gelten für das was ich geplant habe.
 

Marco13

Top Contributor
So sieht meine derzeitige meist genutzte Funktion aus

Achso... wenn du die geladenen BufferedImages sowieso nicht direkt zeichnest, erübrigt sich das vielleicht - das kommt darauf an, wie die createAnimatedImage-Methode aussieht, poste die am besten mal (bzw. die relevanten Teile, wo die einzenlnen Images erstellt werden).


Zur "Malarbeit": Mit VolatileImage habe ich bisher auch noch nicht viel gemacht, aber mir ist wiederum nicht klar, was du mit "Bufferung von diesen Methoden" meinst. Vielleicht hat da jemand anderes eine klarere Kristallkugel als ich? ;) Es geht erstmal nur darum, so schnell wie möglich "viele" Sprites auf den Bildschirm zu bringen, oder...?
 

MLNW

Mitglied
Java:
public BufferedImage[] createAnimatedImage(String key,
	    BufferedImage source, int pics) {
	BufferedImage[] anim = new BufferedImage[pics];
	for (int x = 0; x < pics; x++) {
	    anim[x] = source.getSubimage(x * source.getWidth() / pics, 0,
		    source.getWidth() / pics, source.getHeight());
	}
	if (!animations.containsKey(key)) {
	    animations.put(key, anim);
	}
	return anim;
    }

Hier nochmal diese Methode, wobei auch die komplette Klasse um die es hier geht ganz oben im Post steht ;)

Gezeichnet wird bisher noch immer in der paintComponent() Methode des JPanels, zumindest geht von hier ein Aufruf an jedes einzelne Sprite um dieses zu zeichnen.

Mit "Bufferung von diesen Methoden" soll "Bufferung" erstmal bedeuten so viel Performance wie möglich rauszubekommen und "von diesen Methoden" generell alle Methoden bedeuten in denen ein Sprite gezeichnet wird, oder aber Linien etc gezeichnet werden. ISt das noch immer unverständlich für dich? :bahnhof:
 

Marco13

Top Contributor
Ahja OK, hatten den Code oben nicht gelesen. Wie in dem "Performance von BufferedImages"-Thread auch steht, kann man sich mit getSubImage die Performance kaputtmachen - sowohl in bezug auf das Bild, auf dem das aufgerufen wird, als auch in bezug auf die SubImages. Da ersteres hier nicht gezeichnet werden muss, könnte man die Methode etwa so schreiben
Java:
public BufferedImage[] createAnimatedImage(String key,
        BufferedImage source, int pics) {
    BufferedImage[] anim = new BufferedImage[pics];
    for (int x = 0; x < pics; x++) {
        int x0 = x * source.getWidth() / pics;
        int y0 = 0;
        int w = source.getWidth() / pics;
        int h = source.getHeight();
        anim[x] = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics g = anim[x].createGraphics();
        g.drawImage(source.getSubimage(x0, y0, w, h), 0, 0, null);
        g.dispose();
    }
    if (!animations.containsKey(key)) {
        animations.put(key, anim);
    }
    return anim;
}
(ungetestet). Das wichtige ist, dass die einzelnen Bilder neu erstellte Bilder mit dem TYPE_INT_ARGB sind.


Das mit dem Buffern ... nicht so ganz. Theoretisch kann man zwar ein BufferedImage erstellen, wo man den aktuellen Zustand reinmalt, und das kann theoretisch einen Performancevorteil bringen, aber das ist ein bißchen komplizierter (und wie man das mit VolatileImages verheiraten könnte, müßte ich mir auch erst ansehen...)
 

Ähnliche Java Themen

Neue Themen


Oben