Zum Inhalt springen

SDL: Sprites

Aus Wikibooks

Themen dieses Kapitels:

[Bearbeiten]

Anders als ich es im letzten Kapitel versprochen habe werde ich in diesem nicht auf die Soundprogrammierung eingehen sondern mich mit dem wohl wichtigsten der 2D-Spieleprogrammierung befassen: Den Sprites. Der Stoff dieses Kapitels wird also das implementieren einer Sprite-Klasse auf deren Basis wir auch später die Spiele, welche in den nachfolgenden Kapiteln erstellt werden, aufbauen werden. Deshalb empfehle ich jedem der dieses Kapitel liest bei irgendwelchen nicht verstandenen Sachen Kontakt zu mir aufzunehmen und das Problem mir zu erklären, da für die weiteren Kapitel die hier erworbenen Kenntnisse von elementarer Bedeutung sein werden. Also Konzentration!

Sprites? Ist das ein neuer Knuspersnack? ;)

[Bearbeiten]

Um genau verstehen zu können, was Sprites eigentlich sind sollten wir uns nun einmal näher betrachten, woraus denn die Szene eines üblichen 2D Spiels aufgebaut ist. Schauen wir uns aus diesem Grund doch einmal dieses Bild an: Ein Meisterwerk, keine Frage, doch woraus besteht es eigentlich? Mal abgesehen von der hässlichen Titelleiste ist da ein Hintergrundbild, welches eine Grasfläche mit roter Graffiti und verkohlten Boden in der Mitte hat, und 2 Sprites. Jetzt höre ich schon die ersten sagen: "Na toll, natürlich sind da zwei Figuren aber wieso sind das Sprites?". Nun ja, also ein Sprite ist meist all das in einem Spiel, was sich bewegen kann und sichtbar ist. Also fast alles, nur gibt es aktive und passive Sprites. Die Passiven haben dabei keinen Einfluss auf das Spielgeschehen und unterliegen so z.B. keiner Kollisionsabfrage. Wenn unser Held Commander Keen z.B. gegen ein passives Sprite läuft passiert beiden garnichts. Bei den Aktiven ist dies jedoch genau anders. Sie haben also direkten Einfluss auf das Spielgeschehen und unterliegen auch meistens diesen Kollisionsabfragen. Um bei unserem Beispiel mit Keen zu bleiben wäre Billy Blaze selbst und die gegnerischen Figuren aktive Sprites die bei gegenseitiger Berührung den Tod einer von ihnen verursachen würden. Natürlich werden auch bei den Landschaftteilen im Level Kollisionsabfragen durchgeführt deshalb sollte man sie auch als aktive Sprites ansehen, obgleich sie wahrscheinlich aber anders gehandhabt werden als die normalen Sprites in Form von z.B. einem Gegner. Nun aber ein Beispiel zu einem Passiven. Schauen wir uns doch mal den Himmel in Commander Keen an: Er ist mit Wolken besiedelt, und diese Wolken sind passive Sprites, da sie zwar angezeigt werden aber bei der Kollision mit einem aktiven keine Veränderung im Spielgeschehen verursachen. Doch muss ich auch hier zugeben, dass es in Keen witzigerweise auch aktive Wolken gibt die mir schon so manches mal beim ergattern eines der vielen Powerups geholfen haben. Aber genug über Commander Keen. Wir können jetzt also letztendlich zusammenfassen, dass Sprites Grafiken in einem Spiel sind die manchmal Einfluss auf das Spielgeschehen haben und sich bewegen.

Die Definition der Klasse:

[Bearbeiten]

Jetzt, wo wir uns der Bedeutung eines Sprites bewusst sind, können wir endlich mit dem Entwerfen einer Klasse für diese beginnen. Hier erstmal die Deklaration der Klasse und der restliche Inhalt für den Header unserer Sprite-Klasse: "Sprite.h"

#ifndef __SPRITE_H__
#define __SPRITE_H__
#include <SDL.h>
class Sprite
{
    public:
        Sprite(SDL_Surface *new_image, int x, int y);
        void SetImage(SDL_Surface *new_image);
        void SetPos(int x, int y);
        void Move(int move_x, int move_y);
        SDL_Rect *GetRect(void);
        void Draw(SDL_Surface *target);
    protected:
        SDL_Surface *image;
        SDL_Rect rect;
};
#endif

Joa das sieht für ein geübtes OOP-Progger Auge eigentlich ziemlich primitiv aus, doch möchte ich auch den Anfängern unter meinen Lesern (Gott segne sie!) die Möglichkeit geben, das ganze nachzuvollziehen. Also erstmal alles Stück für Stück auseinanderblättern. Beginnen wir mit den ersten beiden Zeilen und der Letzten. Sie enthalten Präprozessorbefehle die dazu dienen, den Header im Falle eines mehrfachen einbindens in einer anderen Datei nicht mehrfach einzubinden, sondern nur beim ersten der Dateien im Projekt, welche die "Sprite.h" einbinden will, dieses zu vollziehen. Klingt alles ziemlich kompliziert, soll aber nur verdeutlichen, dass es dadurch nicht zu einer Mehrfachdefinition der Sprite-Klasse kommt, da diese Fehler mit sich ziehen würde. Funktionieren tut dies, weil in der ersten Zeile durch den Präprozessor-Befehl #ifndef ... überprüft wird, ob die Konstante __SPRITE_H__ noch nicht gesetzt wurde. Ist dies nicht der Fall wird sie dann gesetzt und die Sprite-Klasse definiert und danach die #ifndef" Konstruktion mit dem #endif beendet. So wird also beim ersten Inkludieren dieser Datei von einer anderen die Konstante gesetzt, da sie ja davor noch nicht gesetzt war und die #ifndef-Abfrage deshalb erfolgreich war. Wenn diese nun gesetzt ist wird bei allen danach erfolgenden Versuchen des Inkludierens die Abfrage erfolglos sein und somit nicht in den nach der Abfrage folgenden Code springen. Doch schluss mit den ganzen redundanten Erklärungen des Sinnes der Präprozessoranweisungen. In den nachfolgenden Zeilen wird jetzt die Sprite-Klasse definiert, welche einige Memberfunktionen und Variablen enthält. Dabei sind alle Funktionen public, also mit voller Zugriffserlaubnis von der Außenwelt, und alle Variablen protected, also keine Zugriffserlaubnis von nicht Memberfunktionen. Die Variablen sind dabei nicht private, weil wir in Zukunft auch vorhaben werden, neue Klassen von dieser Sprite-Klasse abzuleiten und eine ordentliche Vererbung wäre eben mit private nicht möglich, aber das wissen die OOP Leute sowieso schon längst. Also zu den Memberfunktionen: Der Konstruktor sorgt dafür, dass das Sprite mit einem Bild und einer Position initialisiert wird. Dabei habe ich jedoch keinen Destruktor eingebaut, weil ich einfach keine sinnvolle Aufgabe für ihn finden konnte und seine Existenz nur wegen des Glaubens, dass man bei einem Konstruktor auch einen Destruktor bräuchte, nicht begründbar für mich war. Die SetImage Funktion übernimmt dann die Rolle der Oberfläche des Sprites eine aus dem Speicher zuzuweisen. Ich habe hier aber extra keine Funktion hineingenommen, die uns ein Bild von der Platte in den Videospeicher lädt, weil es in Spielen öfters dazu kommt, dass viele Sprites ein und dasselbe Bild haben und somit das Laden der Bilder jedes Sprite selbst übernehmen zu lassen totale Speicherverschwendung gewesen wäre, mal ganz davon abgesehen, wieviel länger es gedauert hätte, all diese erst zu laden. Die nächsten beiden Funktionen sind schnell erklärt: Die Erste setzt die Position des Sprites, welche in dem Rechteck rect gespeichert ist, auf einen übergebenen Wert und die Zweite ist dafür zuständig die Position des Sprites um einen bestimmten X und Y Wert zu verschieben. Die GetRect Funktion liefert dann noch das Rechteck des Sprites zurück, anhand dessen man die Position des Sprites erfahren kann, was vor allem bei der Kollisionsabfrage sich als äußerst hilfreich erweisen kann. Die letzte Funktion ist nun schließlich die Draw Funktion welche einfach die Oberfläche des Sprites auf eine übergebene andere Oberfläche blittet, also unsere Zeichenfunktion. Das war auch schon alles, was unsere Sprite-Klasse können muss und an Eigenschaften besitzt. Jetzt sind wir nun endlich in der Lage an die Implementierung der Funktionen zu schreiten.

Die Implementierung der Klasse:

[Bearbeiten]

Die Defintion ist abgeschlossen, die Implementierung in der Datei "Sprite.cpp" folgt:

#include "Sprite.h"
Sprite::Sprite(SDL_Surface *new_image, int x, int y) {
    SetImage(new_image);
    SetPos(x, y);
}
void Sprite::SetImage(SDL_Surface *new_image) {
    image = new_image;
}
void Sprite::SetPos(int x, int y) {
    rect.x = x;
    rect.y = y;
}
void Sprite::Move(int move_x, int move_y) {
    rect.x += move_x;
    rect.y += move_y;
}
SDL_Rect *Sprite::GetRect(void) {
    return ▭
}
void Sprite::Draw(SDL_Surface *target) {
    SDL_BlitSurface(image, NULL, target, &rect);
}

Nachdem wir die von mir vorgenommene Programmierung der Memberfunktionen ordentlich überflogen haben, werde ich auf jede einzelne, dem Verständnis wegen, näher eingehen. Der Konstruktor sorgt sofort nach der Erzeugung einer Instanz, also eines Sprites, dafür, dass es durch Aufruf der weiter unten implementierten SetImage und SetPos Funktionen gleich ein Bild und eine Position zugeordnet kriegt. Die nächste Funktion, die wie erwähnt vom Konstruktor bei der Ausführung mit aufgerufen wird, weist der Oberfläche unseres Sprites eine andere zu. Die "SetPos" Funktion, welche auch noch im Konstruktor Benutzung findet, setzt die x- und y-Werte des Rechtecks unseres Sprites auf die übergebenden. Die Bewegungsfunktion macht nun etwas ganz ähnliches wie die zur Positionssetzung, nur wird hier nicht die neue Position übergeben, sondern um wieweit sich das Rechteck des Sprites in irgendwelche Richtungen bewegen soll. So würde man z.B. für eine Bewegung nach rechts oben den Wert 1 für move_x angeben und den Wert -1 für move_y, wenn das ganze nun aber etwas schneller sein sollte nehme man z.B. 4 und -4, und falls es nach links unten soll würde man -4 und 4 in Erwägung ziehen. Die nächste Funktion ist dann die einzige mit Rückgabewert und gibt in diesem die Adresse des Rechtecks unseres Sprites zurück. Die Letzte ist dann für das blitten der Oberfläche des Sprites auf eine andere, wie oben bereits beschrieben.

Spaß mit der Sprite-Klasse:

[Bearbeiten]

Nein, die Sprite-Klasse lässt sich nicht *zensieren*, aber man kann auch anders Spaß mit ihr haben. Aus diesem Grund werden wir uns jetzt einmal eine kleine Anwendung, die ein Sprite beinhaltet, schreiben. Hier der Code:

#include <stdlib.h>
#include <SDL.h>
#include "Sprite.h"
SDL_Surface *LoadBMP(const char *filename);
int main(int argc, char *argv[]) {
    Uint32 bgcolor, magenta;
    Sprite *tux;
    SDL_Surface *screen, *image;
    SDL_Event event;
    bool done = false;
    if (SDL_Init(SDL_INIT_VIDEO) == -1) {
        printf("Can't init SDL:  %s\n", SDL_GetError());
        exit(1);
    }
    atexit(SDL_Quit); 
    screen = SDL_SetVideoMode(640, 480, 16, SDL_HWSURFACE | SDL_DOUBLEBUF);
    if (screen == NULL) {
        printf("Can't set video mode: %s\n", SDL_GetError());
        exit(1);
    }
    bgcolor = SDL_MapRGB(screen->format, 255, 255, 0);
    magenta = SDL_MapRGB(screen->format, 255, 0, 255);
    image = LoadBMP("tux.bmp");
    SDL_SetColorKey(image, SDL_SRCCOLORKEY | SDL_RLEACCEL, magenta);
    tux = new Sprite(image, 0, 50);
    while (!done) {
        while (SDL_PollEvent(&event)) {
            switch(event.type) {
            case SDL_QUIT:
                done = true;
                break;
            }
        }
        if (tux->GetRect()->x + tux->GetRect()->w < screen->w) {
            tux->Move(1, 0);
        } else {
            tux->SetPos(0, 50);
        }
        SDL_FillRect(screen, NULL, bgcolor);
        tux->Draw(screen);
        SDL_Flip(screen);
    }
    delete tux;
    SDL_FreeSurface(image);
    return 0;
}
SDL_Surface *LoadBMP(const char *filename) {
    SDL_Surface *temp, *image;
    temp = SDL_LoadBMP(filename);
    image = SDL_DisplayFormat(temp);
    SDL_FreeSurface(temp);
    return image;
}

Hua das sieht ja schon ziemlich nach nem großen Beispiel aus. Bloß sollte es garnicht so schwer sein diesen Code zu verstehen, da er garnicht soviel neues enthält. Die Technik des ladens einer Oberfläche in den Grafikspeicher, wie sie in der Bildladefunktion angewandt wird, stammt aus Kapitel 3 und große Teile des Rests aus Kapitel 2. Also schön darüber nachdenken, warum das funktioniert und was jede Zeile des Codes als Aufgabe bzw. Zweck hat. Falls nach fleißigem durchschauen dennoch die ein oder andere unbeantwortete Frage auftaucht, würde ich mich darüber freuen, wenn du sie mir mailst. Denn ohne das wäre ich nie in der Lage das Tutorial so weit verständlich zu machen, dass es wirklich jeder kapiert. Also schön mailen bei Fragen und sonst, wenn man denn alles kapiert hat, kann man sich ordentlich freuen und auf das nächste Tutorial warten, dessen Thema ich hier aus so einigen Gründen lieber nicht nennen will. Wo wir gerade dabei sind: Nochmal riesig Sorry für das nichteinhalten meines Vorhabens mit dem Soundprogging tut, bloß hielt ich es momentan einfach für wichtiger das Schreiben einer Sprite-Klasse zu erklären, da ich in den nächsten Tuts schon große Dinge vorhabe. Aber bis dahin wird es leider noch etwas dauern und ich wünsche dir auf jedenfall noch eine Menge Spaß mit der SDL und der Spieleprogrammierung an sich. :)