Tilemap 1

Inhalt

Geschichtliches
Map 1
Map 2
Map 2 optimiert

In diesem Tutorial werde ich zeigen wie man eine einfache 2D Karte programmiert. Dabei benutze ich Tiles (eng: Kacheln) basiertes System, welches einfach zu verstehen und zu implementieren ist. Ich werde alles so einfach wie möglich halten, was natürlich nicht die beste Qualität verspricht. Mir ist es nur wichtig, dass Sie möglichst einfachen Einstieg in 2D Welt finden und schnell zu ihren ersten Erfolgen kommen. Später, wenn Sie eine gewisse Erfahrung haben, werden Sie selbst erkennen was zu verbessern ist.

Geschichtliches

ascii map

Was sehen Sie in diesen Zeichenhaufen? Nichts? Ich sehe eine Kammer in einem Keller, in der sich ein Schatz befindet und die von einem üblen Monster bewacht wird. Erkennen Sie es? Nun, ich gebe zu, ohne Fantasie-Einsatz kann man dort nicht viel erkennen. Hier ist die Lösung:

# – Kerkerwände
__ – Kerkertür
| – Zellentür
S – Gefangener
% – Wächter
@ – Held

So sahen die ersten 2D Adventures aus. Das erste Spiel in diesem Stil heißt Rogue, welches im Jahr 1980 veröffentlicht wurde. Unter diesem Link http://www.hexatron.com/rogue/ kann man erfahren wie sich solche Spiele spielen.

Map 1

So eine Map aus Zeichen ist nichts Weiteres als ein einfaches zweidimensionales Feld. In C++ könnte die Deklaration und Initialisierung folgendermaßen aussehen:

const unsigned int map_hoehe = 10;
const unsigned int map_breite = 20;

char tilemap[map_hoehe][map_breite] =
{
'#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#',  0,  0,'S',  0,  0,  0,'#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#',  0,  0,  0,  0,  0,  0,'#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#',  0,  0,  0,  0,  0,  0,'#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'%','|',  0,  0,  0,  0,  0,  0,'#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#','#','#','#','#','#','#','#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#',  0,  0,  0,  0,  0,  0,'#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#',  0,  0,  0,  0,  0,  0,'#',
'#',  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,'#',  0,  0,  0,  0,  0,  0,'#',
'#','#','_','_','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#','#'
};

Da eine sollte Initialisierung bzw. Positionierung ziemlich schwierig ist, kann man sie höchstens als Beispiel zeigen. Später werden wir die Map aus einer Datei laden. Aber zuerst machen wir so weiter.

Jetzt, wo die Map „geladen“ ist, kann sie gezeichnet werden. Dazu gehen wir alle Spalten und Reihen durch und zeichnen jedes einzelnes Zeichen. Wenn eine neue Zeile beginnt, dann gibt’s einen Zeilenumbruch.

// map zeichnen
// alle Spalten und Reihen durchlaufen und alle Zeichen zeichnen
for (unsigned int i = 0; i < map_hoehe; i++)
{
    for(unsigned int j = 0; j < map_breite; j++)
    {
        std::cout << tilemap[i][j];
    }

    // neue zeile => zeilenumbruch
    std::cout<<std::endl;
}

Das Ergebnis sehen sieht folgendermaßen aus:

grafik: ascii map
ASCII Map

Beispiel 1 herunterladen: Tilemap Beispiel 1

Was wir jetzt betrachtet haben, war keine Tilemap, sondern eine ASCII-Map. Zu einer Tilemap ist es nur noch ein kleiner Schritt.

Map 2

Die Idee von einer Tilemap ist, dass man beim Zeichnen einer ASCII-Map, die Zeichen durch entsprechende Grafiken ersetzt.
Dazu erstellen wir uns eine Zuordnungstabelle.

Zuordnungstabelle
Beschreibung ASCII Zeichen Grafik

Zellentür

|

Zellentür Grafik

Kerkertür

_

Kerkertür Grafik

Wand

#

Wand Grafik

Schatz

S

Schatz Grafik

Monster

%

Monster Grafik

Held

@

Held Grafik
birneUnter Sprite wird ein 2D Objekt verstanden. Ein Sprite kann ein statisches(eine Textur) oder animiertes Bild(mehrere Texturen) sein. Sprites können zum Beispiel ein Raumschiff, ein Monster, ein Haus, eine Blume, ein Baum…. sein.

Doch bevor wir mit Grafiken arbeiten können, brauchen wir eine Klasse die für uns das Zeichnen von Texturen übernimmt. Dazu verwende ich eine minimalistisch aufgebaute Klasse CSprite. Diese Klasse kann nur statische, also nicht animierte Sprites darstellen. Das reicht auch für den Anfang.

Zuerst schauen wir uns an, wie man ein Sprite erstellt, zum Beispiel eine Wand. Das ist relativ simpel:

// Zuerst brauchen wir die CDirect3D Klasse die uns DirectGraphics bereitstellt.
// Das ist eine „Standard“-Klasse, wie sie in jedem DirectGraphics Tutorial zu finden ist.
// Für uns ist nur die Methode GetDevice() wichtig,
// die uns einen Zeiger auf die D3D Device zurück gibt.
// Instanz der von Direct3D - Klasse erstellen.
CDirect3D d3d;
d3d.Init(hWnd, true);    // true = nicht Vollbild
d3d.SetClearColor(0x3366cc);

// Einen Sprite für unsere Kerkerwand erstellen.
CSprite wand;

// Initialisieren und Grafik laden.
wand.Create(d3d.GetDevice(), L"Texturen/wand.png");
birneWie Sie bereits an dem L vor dem Grafiknamen sehen können, benutze ich Unicode. Also wundern Sie sich nicht, wenn bei jeder Zeichenkette ein L davor steht.

Das war’s schon auch mit der Zauberei. Jetzt kann das Sprite gezeichnet werden. Und hier gibt es auch keine Magie. Das Zeichnen selbst geschieht in der Hauptschleife des Spiels zwischen den beiden Methoden Aufrufen BeginScene() und EndScene() der CDirect3D-Klasse:

// Struktur, in der Windows-Nachrichten gespeichert werden.
MSG msg = { 0 };

while(msg.message != WM_QUIT)
{
    if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
    {
        // Das Zeichnen anfangen.
        d3d.BeginScene();

        // an dieser Stelle werden alle Sprites gezeichnet.

        // die Wand an der Position x = 50, y = 100 zeichnen.
        wand.SetPosition(50, 100);
        wand.Draw();

        // Das Ende des Zeichnenvorgangs signalisieren
        d3d.EndScene();
    }
}

So, das war eine kleine Einführung in die Benutzung von der CSprite-Klasse. Der grafischen Darstellung der Map steht nichts mehr im Weg.

Wir haben sechs verschiedene Sprites und diese müssen erst erstellt werden, also machen wir das mal.

// Sprites erstellen
CSprite wand, kerkertuer, zellentuer, schatz, monster, held;

// Grafiken laden
wand.Create(d3d.GetDevice(), L"Texturen/wand.png");
kerkertuer.Create(d3d.GetDevice(), L"Texturen/kerkertuer.png");
zellentuer.Create(d3d.GetDevice(), L"Texturen/zellentuer.png");
schatz.Create(d3d.GetDevice(), L"Texturen/schatz.png");
monster.Create(d3d.GetDevice(), L"Texturen/monster.png");
held.Create(d3d.GetDevice(), L"Texturen/held.png");

Die Sprites sind jetzt bereit gezeichnet zu werden, doch zuerst müssen über die Positionierung reden.
Unsere Map ist praktisch ein „Schachfeld“. Jedes Kästchen ist ein Platz für ein Sprite.

grafik: Eine 3x4 Tiles große Map
Eine 3×4 Tiles große Map

Nehmen wir an, wir wollen das blau-schattierte Sprite positionieren. Da der Ursprung der Map links oben liegt und die Sprite-Zählung bei Null beginnt, sind die Map-Koordinaten dieses Sprites ( 2 | 1 ). Um dieses Sprite in Pixel zu erreichen brauchen wir für die x-Koordinate 2-mal Sprite-Breite und für y-Koordinate 1-mal Sprite-Höhe nehmen. Wie man sieht lassen sich die Pixelkoordinaten jedes Sprites von den Map-Koordinaten leicht berechnen, praktisch:

X (in Pixeln) = Sprite-Breite * X-Koordinate des Sprites in der Map
Y (in Pixeln) = Sprite-Breite * Y-Koordinate des Sprites in der Map

Das hört sich viel komplizierter an als es ist. Im Grunde genommen muss man die Sprite-Größe mit der Sprite-Position in der Map nehmen um seine Position auf dem Bildschirm zu bekommen.

Dazu ein kleines Beispiel zur Veranschaulichung.

// alle Zeilen durchlaufen
for (unsigned int i = 0; i < 3; i++)
{
    // alle Spalten durchlaufen
    for(unsigned int j = 0; j < 4; j++)
    {
        // Position festlegen
        sprite.SetPosition( sprite_breite * j, sprite_hoehe * i);

        sprite.Draw();
    }
}

Nein, mehr wollte ich nicht erklären :)

Jetzt wo die Sprites geladen sind und die Positionierung klar ist, können wir das Zeichnen angehen. Dabei müssen wir alle Spalten und Reihen der Map durchlaufen und schauen welches ASCII-Zeichen in dem jeweiligen „Kästchen“ steht. Je nach Zeichen, wird ein anderes Sprite dargestellt – das wird mit einer switch-Anweisung realisiert.

// Das Zeichnen anfangen
d3d.BeginScene();

// alle Zeilen durchlaufen
for(unsigned int i = 0; i < map_hoehe; i++)
{
    // alle Spalten durchlaufen
    for(unsigned int j = 0; j < map_breite; j++)
    {
        // je nach ASCII-Zeichen in der Map, ein anderes Sprite zeichnen
        switch( tilemap[i][j] )
        {
            case 0: // kein tile => weiter machen
            break;

            case '#': // wand
            wand.SetPosition(j*tile_breite, i*tile_hoehe);
            wand.Draw();
            break;

            case '_': // kerkertür
            kerkertuer.SetPosition(j*tile_breite, i*tile_hoehe);
            kerkertuer.Draw();
            break;

            case '|': // zellentür
            zellentuer.SetPosition(j*tile_breite, i*tile_hoehe);
            zellentuer.Draw();
            break;

            case 'S': // schatztruhe
            schatz.SetPosition(j*tile_breite, i*tile_hoehe);
            schatz.Draw();
            break;

            case '%': // monster
            monster.SetPosition(j*tile_breite, i*tile_hoehe);
            monster.Draw();
            break;

            case '@': // spieler
            held.SetPosition(j*tile_breite, i*tile_hoehe);
            held.Draw();
            break;

            default:
            break;
        }
    }
}

// Das Ende des Zeichnenvorgangs signalisieren
d3d.EndScene();

Das Resultat sieht folgendermaßen aus:

grafik: Tilemap 1
Tilemap 1

Schon mal nicht schlecht oder?! Nur die blauen Bereiche sollten besser durch Laminat…ehm…ich meine durch kalte, graue Steine ersetzt werden ;)

Man könnte jetzt ein Sprite für Boden erstellen und es überall dort zeichnen wo eine Null in der Map steht. Das wäre optisch aber eine schlechte Lösung. Sehen Sie selbst.

grafik: Tilemap 2
Tilemap 2

In den transparenten Bereichen sieht man das Blaue obwohl man dort eigentlich auch Steine sehen sollte. Dazu gibt’s eine einfache Lösung: man zeichnet zuerst über die ganze Map den Boden und erst dann wird der Rest gezeichnet. Der Boden wird praktisch vor der switch-Anweisung gezeichnet.

// [...]

// Boden zeichnen
boden.SetPosition(j*tile_breite, i*tile_hoehe);
boden.Draw();

// je nach ASCII-Zeichen in der Map, ein anderes Sprite zeichnen
switch( tilemap[i][j] )
{
// [...]

Das Resultat sieht dann so aus:

grafik: Tilemap 3
Tilemap 3

Beispiel 2 herunterladen: Tilemap Beispiel 2

Map 2 optimiert

Jetzt noch einen Tipp, wie man die Map und die Sprites besser verwalten kann. Der erste Schritt besteht darin, dass man ganz von den ASCII-Zeichen loslässt und stattdessen einfache Zahlen nimmt. Dazu muss man sich wieder eine Tabelle erstellen, damit man weiß, was zu was hingehört.

Zuordnungstabelle
Beschreibung ASCII Zeichen Grafik
Boden 0 Boden Grafik
Wand 1 Wand Grafik
Kerkertür 2 Kerkertür Grafik
Zellentür 3 Zellentür Grafik
Schatz 4 Schatz Grafik
Monster 5 Monster Grafik
Held 6 Held Grafik

Die Map-Deklaration verändert sich dadurch automatisch zu:

// die map
unsigned int tilemap[map_hoehe][map_breite] =
{
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 4, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 3, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1,
1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
};

Doch das war nur eine Vorbereitung für die eigentliche „Optimierung“. Als zweiten Schritt werden die Sprites als ein Array angelegt.

// Globale Variable, beschreibt die Anzahl der Sprites,
// die in der Map verwendet werden
const unsigned int sprite_anzahl = 7;

// 7 Sprites erstellen
CSprite map_sprites[sprite_anzahl];

Jetzt müssen noch die Grafiken geladen werden. Man muss nur aufpassen, dass zu jedem Sprite die passende Grafik zugeordnet wird. Spätestens nach dem Ausführen wird man es sehen ob die Zuordnung richtig war ;)

// Grafiken laden
map_sprites[0].Create(d3d.GetDevice(), L"Texturen/boden.png");
map_sprites[1].Create(d3d.GetDevice(), L"Texturen/wand.png");
map_sprites[2].Create(d3d.GetDevice(), L"Texturen/kerkertuer.png");
map_sprites[3].Create(d3d.GetDevice(), L"Texturen/zellentuer.png");
map_sprites[4].Create(d3d.GetDevice(), L"Texturen/schatz.png");
map_sprites[5].Create(d3d.GetDevice(), L"Texturen/monster.png");
map_sprites[6].Create(d3d.GetDevice(), L"Texturen/held.png");

Dadurch dass die Sprites in einem Array liegen und die Map Zahlen beinhaltet, vereinfacht sich der Draw-Algorithmus enorm.

// Das Zeichnen anfangen
d3d.BeginScene();

// alle Zeilen durchlaufen
for(unsigned int i = 0; i < map_hoehe; i++)
{
    // alle Spalten durchlaufen
    for(unsigned int j = 0; j < map_breite; j++)
    {
        // boden zeichnen
        map_sprites[0].SetPosition(j*tile_breite, i*tile_hoehe);
        map_sprites[0].Draw();

        // je nach Sprite-Nummer in der Map, anderes Sprite zeichnen
        unsigned int sprite_nummer = tilemap[i][j];

        // kleine Vorsichtsmaßnahme
        if(sprite_anzahl <= sprite_nummer)
            continue;

        map_sprites[ sprite_nummer ].SetPosition(j*tile_breite, i*tile_hoehe);
        map_sprites[ sprite_nummer ].Draw();
    }
}

// Das Ende des Zeichnenvorgangs signalisieren
d3d.EndScene();

Beispiel 3 herunterladen: Tilemap Beispiel 3

So das war’s für den Anfang. Ich hoffe ich konnte zumindest einen kleinen Einblick in die 2D Welt gewähren. Im zweiten Teil des Tutorials bringen wir etwas Bewegung ins Spiel ;)

Viel Spaß beim Programmieren!

Quellen:
cover
Titel:Spieleprogrammierung. Konzeption, Entwicklung, Programmierung, m. CD-ROM. Das bhv Taschenbuch
Autor: Lennart Steinke
Seiten: 767
ISBN: 3826680758

Schreibe einen Kommentar