Ich habe die Ehre das erste deutschsprachige Tutorial auf www.rpdev.net zu veröffentlichen. Es soll sich dieses mal auch mit Spieleprogrammierung beschäftigen – genauer gesagt, mit einem Kartenformat in isometrischer Ansicht, auf der sich dann irgendwann ein “Held” bewegen kann. Was das ist und wie man es benutzt, erfahrt ihr hier.

Vorwort

Gelangweilt von den 2D Standarddraufsichtskarten, die es wie Sand am Meer gibt? Mal etwas machen, was sich von anderen etwas abhebt? Zugegeben ein Spiel was sich von anderen abhebt, sollte mehr als nur isometrische Spielkarten aufweisen. Aber falls du dein Spiel genau damit ausstatten willst, sei hiermit eine kleine Hilfestellung gegeben.

Ich möchte wirklich nur einen kleinen Einstieg geben und keine ausführliche Beschreibung, wie man ein hochkomplexes isometrisches Kartenformat entwickelt. Der veröffentlichte Source soll daher auch nur eine Anregung sein. Ich habe ihn in C++ und teilweise Pseudocode geschrieben.

Was ist eine isometrische Karte?

Das ist nichts weiter als eine ganz normale Spielkarte mit einer speziellen Perspektive. Stell dir eine rechteckige Karte vor (Draufsicht) und drehe diese um 45 Grad und dann kippe sie nach Hinten etwas ab.

Es gibt auch viele bekannte Spiele, die auf solchen Karten basieren

  • Sid Meier’s Civilization
  • Final Fantasy Tactics
  • Age of Empires I

Vor- und Nachteile?!

Das sei einfach mal in kurzen Stichpunkten zusammengefasst:

Vorteile:

  • Perspektive wirkt sehr räumlich.
  • Weniger weit verbreitet als Draufsichtkarten.
  • Isometrische Tiles sind nicht schwerer herzustellen.

Nachteile:

  • Indizes können leicht zur Verwirrung führen.
  • „Koordinaten von Bildschirm zu Karte“-Problem ist etwas aufwendiger.

Also los!

Wie jedes Kartenformat, bauen wir unsere Karte aus einzelnen sogenannten Tiles auf. Ein Tile (Kachel) ist später nichts weiter, als ein einzelnes Feld unserer Karte. Sie beinhaltet einen Verweis auf eine Textur (damit wir wissen, wie das Tile später aussieht), speichert ihre Position und soll außerdem einen Wert speichern ob die Kachel betretbar ist.

Die Textur hat in diesem Fall eine Dimension von 64*32. Es darf auch eine andere Größe gewählt werden, wichtig ist dabei nur, dass die Breite das doppelte der Höhe beträgt.

struct TILE
{
	textureformat*	texture_;			//Verweis auf Textur (pseudocode)
	int		position_x_, position_y_;	//Koordinaten
	bool		is_enterable_;			//Wert ob das Tile betretbar ist
};

Nun sollte man sich eine Klasse anlegen, die ein zweidimensionales Array vom Datentyp TILE* speichern kann. Ich nenne dies hier einfach mal isometric_map.

Um das zweidimensionale Array zu erzeugen, verwende ich den STL vector, da er seine Größe dynamisch verändern kann, und man somit die Karte im nachhinein beliebig ändern kann. Das hat für diesen Zweck schon erhebliche Vorteile. Wer mit dem STL-vector nicht so vertrau ist, kann auch statische Größen wählen. Ferner ist es nützlich einen Wert zu haben, der die Kartengröße angibt und außerdem sollte eine jede Karte einen treffenden Namen haben.

class isometric_map
{
private:
	vector<vector<TILE*> > 	isomap_;	//Unser Spielfeld
	int			size_;		//Die Größe das Spielfeldes
	const char*		levelname_;	//Der Levelname
	//statische Größen
	//TILE*			isomap_[5][5];

public:
	isometric_map();
	~isometric_map();
};

Aufgrund einer Anmerkung (vielen dank dafür), möchte ich noch betonen, dass man auch mit einem eindimensionalen Array ein zweidimensional simulieren kann, indem man die Koordinaten in einen index umrechnet.

vector<TILE*> array_;

//Umrechnung
isomap_[x][y] = array[x*size_ + y];

Nur was muss die Klasse isometric_map jetzt alles leisten können? Das ist abhängig davon wie leistungsstark eure Karte werden soll. Hier eine grobe (subjektive) Unterteilung:

1 Etwas was sie können muss: (wird im Tutorial behandelt!)

  • setzen, ändern und laden der Parameter
  • laden und zeichnen der Karte
  • bewegen der Karte

2 Etwas was sie können sollte: (wird nicht im Tutorial behandelt!)

  • überschreiben der Textur einer beliebigen Kachel
  • löschen und hinzufügen von Kacheln

3 Etwas was sie können darf:

  • verschiedene Layer
  • platzieren von Objekten auf der Karte
  • verwalten von Events(wenn man eine Kachel betritt, passiert etwas o.ä.)

Man sieht schnell, dass ein starkes Kartenformat nicht einfach so vom Himmel fällt, sondern einiges an Arbeit bedeutet. Für ein Spiel wäre es natürlich sehr hilfreich, wenn die Karte alle 3 Punkte bedient.

Kommen wir zum ersten Punkt:

Punkt Nummer 1 - Die Standardkarte

Die Klasse wird hier um einige wichtige Funktionen erweitert:

class isometric_map
{
private:
	vector<vector<TILE*> > 	isomap_;	//Unser Spielfeld
	int			size_;		//Die Größe das Spielfeldes
	const char*		levelname_;	//Der Levelname
	//statische Größen
	//TILE*			isomap_[5][5];

public:
	isometric_map();
	~isometric_map();

	void initialize(void);

	void set_size(int new_size);
	void set_levelname(const char* levelname);

	int get_size(void) const;
	const char* get_levelname(void) const;

	void load_map(void);
	void move_map(void);

	void draw_map(void);
};

Setzen und ändern der Parameter

Standardmäßig wird in der Initialisierungsmethode die Erstbelegung der Werte wiefolgt vorgenommen:

void initialize(void)
{
	isomap_.clear();
	size_			= 5;
	level_name_	= testmap;
}

Nun müssen wir nur noch an die Werte rankommen. Das ist leicht! Da die Klassenattribute private deklariert sind, brauchen wir einfach nur öffentliche Methoden um darauf zugreifen zudürfen

void set_size(int new_size)
{
	size_ = new_size;
}

void set_levelname(const char* levelname)
{ 
	levelname_ = levelname;
}

Analog die Methoden um uns die Werte zurückliefern zu lassen.

int get_size(void) const
{
	return(size_);
}

const char* get_levelname(void) const
{
	return(levelname_);
}

Laden und Zeichnen der Karte

Der Ladevorgang ist etwas anders, als bei einer Karte mit Draufsicht, da wir ja wissen, dass die Karte um 45 Grad gedreht ist. Das heißt eine Kachelreihe geht nicht senkrecht oder waagerecht, sondern diagonal. Und das heißt wiederrum die Kacheln sind bezüglich der horizontalen oder vertikalen Achse versetzt. Wir müssen daher ledeglich die Position anpassen. Dazu brauchen wir aber ein paar Daten - die Bildschirmdimension und die Kacheltexturdimension.

//wer keine define-makros verwenden möchte kann sie auch als globale Konstanten deklarieren
#define	WINDOW_X		640
#define	WINDOW_Y		480

#define	TILE_WIDTH		64
#define	TILE_HEIGHT		32

Nun können wir die methode implementieren, die uns die Karte erstellt:

void load_map(void)
{
	vector<TILE*> temp_;

	for(x_ = 0; x_ < size_of_map_; x_ ++)
	{
		for(y_ = 0; y_ < size_of_map_; y_ ++) 		{ 			TILE* buffer = new TILE; 	 			//standardtexture verwenden (z.b. gras) 			buffer->texture	= defaulttexture_;

			buffer->position_x_ = (WINDOW_X  / 2 - TILE_WIDTH / 2) - y_ * TILE_WIDTH / 2  + x_ * TILE_WIDTH / 2;
			buffer->position_y_ = (WINDOW_Y / 2 - TILE_HEIGHT	/ 2*size_)+ y_* TILE_HEIGHT / 2 + x_* TILE_HEIGHT / 2;

			buffer->collision_	= true;

			temp_.push_back(buffer);
		}

		isomap_.push_back(temp_);
		temp_.clear();
	}
}

Naja die Anpassung der Kachelpositionen sieht ja auch sehr gewaltig aus, ist aber im Grunde einfach und zwingend logisch.

(WINDOW_X  / 2 - TILE_WIDTH / 2)

bewirkt nichts weiter, als dass unsere Karte beim Zeichnen zentriert dargestellt wird. Damit haben wir die halbe Bildschirmbreite minus die halbe Kachelbreite, sodass die Kachel nicht ab der Bildschirmmitte gezeichnet wird, sondern etwas vorher, damit die Kachel mittig auf dem Bildschirm ist. Jetzt müssen wir sie nur noch entsprechend ihren Indizes etwas verschieben.

Betrachten wir uns dazu den Aufbau der Karte, wie folgt:

Man sieht, dass die Reihen eine bestimmte Ausrichtung haben. Wir müssen die Kachelreihe y einfach um die halbe Kachelbreite nach links und um die halbe Kachelbreite nach unten verschieben.

Und jede Kachelreihe x um die gleiche Dimension nach rechts. Die Kachel (x,y) = (1,2) müssen wir somit zuerst um eine halbe Kachelbreite nach rechtsunten verschieben. und dann 2 mal die halbe Kachelbreite nach links und dann noch mal nach unten verschieben.

Das erreichen wir indem wir den Offset aufaddieren

buffer->position_x_	= (WINDOW_X  / 2 - TILE_WIDTH / 2) - y_ * TILE_WIDTH / 2  + x_ * TILE_WIDTH / 2;
buffer->position_y_ = (WINDOW_Y / 2 - TILE_HEIGHT	/ 2*size_)+ y_* TILE_HEIGHT / 2 + x_* TILE_HEIGHT / 2;

Jetzt zeichnen wir die Karte einfach. Da wir jetzt die Koordinaten einer jeden Kachel gespeichert haben, ist das Zeichnen nun denkbar einfach. Wir durchlaufen einfach unser 2D-Array und zeichnen jedes Tile

void draw_map(void)
{
	for(x_ = 0; x_ < size_; x_ ++)
	{
		for(y_ = 0; y_ < size_; y_ ++)
		{
			//zeichne Karte (pseudocode)
			zeichne(isomap_[x_][y_]);
		}
	}
}

Bewegen der Karte

Hier müssen wir nur jede Kachelposition um x_offset und y_offset verschieben. Da wir die Koordinaten lokal gespeichert haben, sollte das keine Schwierigkeiten machen. Wieder Array durchlaufen und jedes Tile um den offset verschieben.

void move_map(int x_offset, int y_offset)
{
	for(x_ = 0; x_ < size_of_map_; x_ ++)
	{
		for(y_ = 0; y_ < size_of_map_; y_ ++) 		{ 			isomap_[x_][y_]->position_x_ += x_offset_;
isomap_[x_][y_]->position_y_ += y_offset_;
		}
	}
}

Und Voila, damit kann man schon einmal ganz einfache isometrische Karten, in sogenannter Diamondform, erstellen.

Jetzt könnte man anfangen, die restlichen Punkte (was “sollte” und was “darf” die Karte können) angehen, aber da dieses Tutorial hier endet, bleibt das entweder Stoff für euch selbst, oder Stoff für ein nächstes Tutorial. Hier ein kleiner Ausblick, was aus viel Experimentierfreude werden kann.

Das ist ein Screenshot aus einem Privatprojekt von mir. Die Texturen sind allerdings nicht von mir, sondern habe ich (nach Anmeldung) auf www.isogames.de herunterladen können.

Somit endet mein Tutorial (erstes überhaupt) und ich hoffe ich konnte euch damit Anregung, Tipps oder Neugier auf mehr machen, und stehe natürlich für Rückfragen oder Kommentare jederzeit zur Verfügung.

In diesem Sinne, viel Erfolg wünscht stefan