SFML Einsteiger-Tipps

Aus Spieleprogrammierer-Wiki
Wechseln zu: Navigation, Suche

Beim Lernen von C++ wird unter anderem gern auf SFML zurückgegriffen, weil man damit spielerisch die Welt von C++ erkunden kann und nicht auf die Konsole angewiesen ist. SFML bietet diverse Klassen zur einfachen Erstellung von Fenstern, Anzeige von Grafiken, Abspielen von Sounds, Verwendung von Netzwerk und mehr. Doch oft sind diese Dinge mit höherem Verständnis von C++ verknüpft als es zunächst den Anschein hat. SFML setzt voraus, dass der C++ Entwickler die grundlegenden Konzepte von Speicherverwaltung, Konstruktoren und Destruktoren, sowie Pointern und Referenzen verstanden hat. So lauern bei der Verwendung von SFML einige Fallstricke, die man beachten muss.

Bitte beachte, dass viele Beispiele hier zeigen, wie man es nicht machen sollte!

Inhaltsverzeichnis

Fehler #1: Falsche Verwendung von sf::Texture

Dieses Problem ist eines der häufigsten, welches sich bei Einsteigern findet. Da das Wissen um Pointer und Referenzen noch nicht gefestigt wurde, wird zu simplen Values gegriffen, was dann zu Fehlern führt.

Textur als einfacher Member

Der einfachste Fall, der bei falscher Verwendung von Texturen auftritt, führt einfach nur zu massiver Speicher- und CPU-Belastung. Dabei wird die Textur oft als Member einer Klasse deklariert, wie z.B. wie folgt:

class Player
{
    sf::Texture m_Texture;
    sf::Sprite m_Image;
 
public:
    Player()
    {
        m_Texture.loadFromFile("player.png");
        m_Image = sf::Sprite(m_Texture);
    }
 
    void draw(sf::RenderTarget& target)
    {
        target.draw(m_Image);
    }
    ...
};

Auf den ersten Blick wirkt diese Klasse in sich abgeschlossen. Doch was passiert, wenn diese Klasse nun über Funktionen hin und her gereicht wird? Zum Beispiel so:

void drawGame(Player player1, Player player2)
{
   player1.draw();
   player2.draw();
}
...
Player player1, player2;
...
drawGame(player1, player2);

Wird dieser Code ausgeführt, wird man schnell merken, dass die Aufrufe recht lange dauern. Doch warum? Nun, player1 und player2 werden als Wert (pass by value) übergeben und dabei kopiert. Um Kopien von einem Player zu erzeugen, legt der Compiler einen Kopierkonstruktor an. Dabei kopiert wird für jede Player-Instanz das Sprite und die Textur kopiert. Im Endeffekt heißt das, dass das Bild von der Grafikkarte heruntergeladen, eine Kopie des Bilds erzeugt und diese Kopie wieder an die Grafikkarte hochgeladen werden muss. Man kann sich leicht vorstellen, dass das eine sehr aufwendige Operation ist. Zudem ist sie auch noch überflüssig, denn nun gibt es jede Textur zweimal auf der Grafikkarte. Das verbraucht natürlich auch doppelten Video-Speicher. Dazu kommt noch, dass die Kopien der Sprites noch immer eine Referenz auf die originale Textur benutzen. Die Kopien werden also zwar angelegt, aber von gar keinem Sprite verwendet. Am Ende der update-Funktion werden diese Kopien wieder von der Grafikkarte gelöscht. Also haufenweise Arbeit für nichts. Mit Übergabe als Referenz wäre das Problem nicht aufgetreten.

Crash durch Referenzen und Kopierkonstruktor

Richtig schlimm wird es, wenn die referenzierte Textur gelöscht wird. Man stelle sich dazu z.B. eine Funktion vor, die einen neuen Spieler erstellt und ihn auf eine gewisse Startposition setzt oder gewisse andere Dinge an ihm ändert:

Player createPlayer(int health)
{
    Player playerTemp;
    playerTemp.setPosition(0, 0);
    playerTemp.setHealth(health);
    return playerTemp;
}
...
Player player1 = createPlayer(100);
...
player.draw(window);

Auf den ersten Blick sieht das gar nicht so schlecht aus. Man hat eine Funktion, die schön gewisse Logik kapselt. Das Ergebnis ist ein Absturz des Programms. Was ist passiert? Die Spieler wurde korrekt in createPlayer erstellt und gewisse Initialwerte zugewiesen. Dann wurde er zurückgegeben und zugewiesen. Dabei wird allerdings wieder der Kopierkonstruktor von playerTemp gerufen. Den kennen wir schon aus dem vorherigen Beispiel. Der Kopierkonstruktor macht jedoch eine wichtige Sache nicht, er sagt dem Sprite nicht, dass es die kopierte Textur verwenden soll. Das Sprite von player1 zeigt also ebenfalls auf die originale Textur von playerTemp. Wird die Funktion verlassen und player neu zugewiesen, wird allerdings die alte Player-Instanz (playerTemp), nicht mehr gebraucht. Sie wurde ja kopiert in player1. Also wird sie zerstört. Und mit ihr auch die original Textur aus playerTemp. Das beutet, dass das Sprite in player1 nun eine Textur referenziert, die gar nicht mehr existiert. Zugriffe auf nicht existente Objekte verursachen diverse Fehler. Der Crash ist hier noch der beste Fall. Richtig fies ist, dass der Crash nicht unbedingt beim Aufruf von player.draw() erfolgen muss. Er kann auch wesentlich später erst eintreten, wo die Ursache des Fehlers nicht mehr nachvollziehbar ist.

Kopierte Texturen als Fehler deklarieren

Damit solche Fehler nicht unterlaufen können und bei der versehentlichen Kopie einer Textur gleich eine Fehlermeldung kommt, bietet es sich an eine eigene Textur-Klasse zu verwenden:

class NCTexture : public sf::Texture, private sf::NonCopyable
{
};

Diese Klasse verhält sich in allen Belangen wie eine übliche SFML-Textur, mit dem Unterschied, dass sie sich nicht versehentlich kopieren lässt. Versucht man es dennoch, bekommt man vom Compiler eine entsprechende Fehlermeldung. Dann muss man dieses Problem durch Referenzen und Pointer entsprechend auflösen. Es muss nur überall dort, wo normalerweise sf::Texture verwendet werden würde stattdessen die NCTexture verwenden.

Mehrfaches Laden und Textur-Cache

Das initiale Beispiel der Player-Klasse hat ein weiteres Problem, was dem Kopieren von Texturen relativ ähnlich ist. Folgendes Beispiel:

Player player1, player2, player3;

Hier werden drei Spieler erzeugt. Damit auch auch die Textur dreimal geladen. Das dauert dreimal so lange und benötigt auch dreimal so viel Speicher. Eine Lösung dazu ergibt sich durch Verwendung von Referenzen und einer zentralen Stelle, die sich um das Laden von Texturen kümmert. Man kann also z.B. eine TexturCache Klasse bauen, die alle Texturen des Spiels kennt und sie beim Start alle einmal lädt. Sie könnte dann eine Methode anbieten, über die man eine Referenz auf die jeweils gewünschte Textur geliefert bekommen könnte. Diese Referenz kann man nun im Konstruktor von Player entsprechend übergeben:

class Player
{
    const sf::Texture& m_Texture;
    sf::Sprite m_Image;
 
public:
    Player(const NCTexture& texture) : m_Texture(texture)
    {
        m_Image = sf::Sprite(m_Texture);
    }
    ...
};
...
Player player1(texturCache.getRedPlayerTexture());
Player player2(texturCache.getRedPlayerTexture());
Player player3(texturCache.getGreenPlayerTexture());

Wird nun ein Player kopiert, ist das alles gar kein Problem mehr, denn alle Spieler verwenden eine Referenz auf ein und dieselbe Textur, die nur einmalig im System geladen und im Speicher abgelegt wurde. Die Erstellung einer entsprechenden TexturCache-Klasse wird dem geneigten Leser als Übung überlassen.

Meine Werkzeuge
Namensräume
Varianten
Aktionen
Navigation
Werkzeuge