2D Spiele

Top  Previous  Next

Dieses Mal erstellen wir einen 2D Level Generator, der für Logikspiele, RPGs und auch Actionspiele genutzt werden kann. Beginnen wir mit 2dtiles1.c:

 

aum83_workshop1

 

Die schönen Grafiken sind nicht von mir, sie stammen aus dem Lost Garden Grafikpaket. Können Sie sich vorstellen, wie lange ich gebraucht habe, dieses Level aus den einzelne Feldern zusammenzusetzen? Nun, weniger als 1 Minute, da alle wichtigen Daten aus dieser Datei gelesen werden (level.txt):

 

aum83_workshop2

 

Jeder Buchstabe steht für ein 2D Feld: "w" steht für Wasser, "s" ist ein Steinfeld, "g" bedeutet Gras und so weiter. Wir setzen ein Komma an das Ende jeder Zeile und der Code liest diese Daten und erzeugt daraus unser 2D Level. Sehen wir uns an wie das funktioniert:

 

#define init_x 64

#define init_y 90

#define tile_width 64

#define tile_height 52

 

STRING* data_str = "#20"; // the games can use up to 20 horizontal tiles

STRING* temp_str = "#20"; // just a temporary string

 

PANEL* temp_pan;

 

function main()

{

       fps_max = 60;

       video_mode = 8; // 1024x768

       video_depth = 32; // 32 bit mode

       video_screen = 1; // start in full screen mode

       vec_set(screen_color, vector(178, 153, 128)); // set a pleasant, blueish background color

       read_level_data(); // read the level elements

}

 

Wir initialisieren init_x und init_y anfangs so, dass sie die Koordinaten der oberen linken Ecke des 2D Levels entsprechen. Dies werden die Koordinaten des ersten Wasserfeld-Panels meiner Demo sein. Wir definieren auch die Größen tile_width und tile_height. Diese sind auf meine Felder abgestimmt, falls Sie Ihre eigenen Grafiken verwenden, müssen die Größen ggf. angepasst werden.

 

Der "temp_pan" Panel Pointer ist ein temporärer Panel Pointer, der es uns erlaubt ein Feld zu erstellen, es an der richtigen Stelle zu plazieren und sichtbar zu machen, so dass danach das nächste Feld erstellt werden kann und so weiter. Wir erstellen die Panels nacheinander (das geht sehr schnell), brauchen also nur einen Pointer.

 

Die main() Funktion tut das Übliche, sie gibt einen angenehmen Blauton als Hintergrundfarbe vor und ruft dann die "read_level_data" Funktion auf.

 

function read_level_data()

{

       var level_handle, string_size, index, layer_value;

       VECTOR tile_pos;

       layer_value = 10;

       tile_pos.x = init_x;

       tile_pos.y = init_y;

       level_handle = file_open_read("level.txt"); // open the level file

       wait (3); // wait a bit

       while (file_str_read(level_handle, data_str) != -1) // run the loop until we reach the end of the file

       {

 

Diese Funktion setzt die Anfangsposition auf die Werte, die durch init_x und init_y gegeben sind (die obere linke Ecke) und öffnet dann die level.txt Datei. Die Schleife nach dieser Anweisung wird ausgeführt bis das Ende der Datei erreicht ist, also bis file_str_read den Wert -1 zurückliefert.

 

               index = 0;

               string_size = str_len(data_str);

               while (index < string_size) // go through all the characters of the string (row), one by one

               {

                       str_cpy (temp_str, data_str); // let's copy data_str to temp_str (we don't want to destroy it)

                       str_clip(temp_str, index); // cut the needed number of characters from the beginning of the string

                       str_trunc(temp_str, string_size - index - 1); // and cut the needed number of characters from the end as well

 

An diesem Punkt wurde eine ganze Zeile aus level.txt gelesen, also könnte data_str folgendermaßen aussehen: wgggggggdgggsw. Wir müssen auf die einzelnen Zeichen zugreifen, um die entsprechenden Felder erstellen zu können. Zunächst speichern wir die Länge des Strings in der Variablen string_size. Dann wird eine Schleife aufgerufen, die uns die Zeichen liefert, indem Teile vom Anfang und vom Ende des Strings entfernt werden. Da diese Operationen die ursprünglichen Daten zerstören würden, machen wir eine Kopie von data_str in temp_str. Auf diese Weise kann die Schleife laufen bis alle Zeilen erstellt wurden.

 

                       if (str_cmp(temp_str, "d") == 1) // the character that was read is a "d"?

                       {

                               temp_pan = pan_create("bmap = dirt.png;", layer_value); // then create a dirt panel

                               temp_pan.pos_x = tile_pos.x; // and place it at its proper position

                               temp_pan.pos_y = tile_pos.y; // on the x and y axis

                               temp_pan.flags |= VISIBLE; // and then make it visible

                       }

 

Falls das Zeichen des Strings beispielsweise ein "d" ist (ein Feld mit Dreck), wird das dirt.png Panel an der Position erstellt, die durch tile_pos.x und tile_pos.y gegeben ist, bevor wir es sichtbar machen. Beachten Sie, dass wir keinen festen Layer Wert verwenden, sondern eine Variable. Auf diese Weise können wir kontrollieren ob die unteren Felder über den anderen erscheinen oder nicht.

 

In meinem Beispiel wird der "layer" Wert der Zeilen erhöht, je tiefer man auf dem Bildschirm ist. Andernfalls würde das Resultat so aussehen:

 

aum83_workshop3

 

Dies könnte für Ihre Grafiken genau das Richtige sein. Der Code für die restlichen Felder (Gras, Wasser, etc.) ist ähnlich, also kommen wir zu interessanteren Teilen.

 

                       if (str_cmp(temp_str, "g") == 1)

                       {

                               temp_pan = pan_create("bmap = grass.png;", layer_value);

                               temp_pan.pos_x = tile_pos.x;

                               temp_pan.pos_y = tile_pos.y;

                               temp_pan.flags |= VISIBLE;

                       }

                       if (str_cmp(temp_str, "p") == 1)

                       {

                               temp_pan = pan_create("bmap = plain.png;", layer_value);

                               temp_pan.pos_x = tile_pos.x;

                               temp_pan.pos_y = tile_pos.y;

                               temp_pan.flags |= VISIBLE;

                       }

                       if (str_cmp(temp_str, "s") == 1) // stone wall

                       {

                               temp_pan = pan_create("bmap = stone.png;", layer_value);

                               temp_pan.pos_x = tile_pos.x;

                               temp_pan.pos_y = tile_pos.y;

                               temp_pan.flags |= VISIBLE;

                       }

                       if (str_cmp(temp_str, "t") == 1)

                       {

                               temp_pan = pan_create("bmap = terrain.png;", layer_value);

                               temp_pan.pos_x = tile_pos.x;

                               temp_pan.pos_y = tile_pos.y;

                               temp_pan.flags |= VISIBLE;

                       }

                       if (str_cmp(temp_str, "w") == 1)

                       {

                               temp_pan = pan_create("bmap = water.png;", layer_value);

                               temp_pan.pos_x = tile_pos.x;

                               temp_pan.pos_y = tile_pos.y;

                               temp_pan.flags |= VISIBLE;

                       }

 

Bislang hat der Code geprüft, welcher Buchstabe gelesen wurde und hat das erste Feld erstellt. Der Code unten erhöht tile_pos.x und bereit so alles für das zweite Feld vor. Wir erhöhen index, isolieren das nächste Zeichen unseres Strings und verwenden erneut "str_cmp".

 

                       tile_pos.x += tile_width; // move on to the following tile (horizontally)

                       index += 1;

               }

 

Die letzten Zeilen werden am Ende der Feldreihe ausgeführt. Zunächst wird layer_value erhöht, um sicher zu gehen, dass die nächste Reihe über der vorigen erscheint. Das obige Bild zeigt was geschieht, wenn "layer_value += 1;" auskommentiert wird. Dann bewegen wir uns wieder an den linken Bildschirmrand und erhöhen tile_pos.y entsprechend. Wenn alle Felder erstellt wurden, wird level.txt geschlossen.

 

               layer_value += 1;

               tile_pos.x = init_x; // move to the beginning of the following row

               tile_pos.y += tile_height; // x is reset, y is incremented

       }

       file_close(level_handle); // so let's close the file

}

 

Das ist es im Wesentlichen schon! Ein einfaches Code-Schnipselchen, das funktioniert und auch für große 2D Level genutzt werden kann, selbst wenn sie die Bildschirmgrenzen überschreiten. Vergessen wir nicht, dass wir eine 2D / 3D Engine verwenden, also nutzen wir ein wenig 3D! Öffnen Sie 2dtiles2.c:

 

aum83_workshop4

 

Dieses Mal haben wir einen Titel für das Spiel und einige grüne Partikel. Diese Demo verwendet das obige Beispiel, sehen wir uns also an, was neu hinzukommt.

 

function main()

{

       fps_max = 60;

       video_mode = 8; // 1024x768

       video_depth = 32; // 32 bit mode

       video_screen = 1; // start in full screen mode

       vec_set(sky_color, vector(178, 153, 128)); // set a pleasant, blueish background color

       temp_pan = pan_create("bmap = title.tga;", 100);

       temp_pan.pos_x = 325;

       temp_pan.pos_y = 25;

       temp_pan.flags |= VISIBLE;        

       level_load(NULL);

       read_level_data(); // read the level elements

}

 

Die main Funktion sieht der aus der vorigen Demo sehr ähnlich, aber es gibt zwei kleine, signifikante Unterschiede:

- Zunächst verwenden wir sky_color anstelle von screen_color, weil wir nun in einem 3D Level arbeiten, also kann screen_color nicht mehr das tun, was es soll.

- Der zweite Unterschied ist die "level_load(NULL);" Anweisung, welches ein leeres 3D Level lädt, so dass wir die 3D Fähigkeiten der Engine nutzen können. Sobald diese Anweisung ausgeführt ist, können wir 3D Entities erstellen und diese im Level platzieren, sie bewegen und so weiter. Beachten Sie auch den Code, der die Titelbitmap erstellt, sie positioniert und sichtbar macht.

 

function snow_startup()

{

       VECTOR temp;

       while (1)

       {

               temp.x = 1000 - random(2000);

               temp.y = 1000 - random(2000);

               temp.z = 200;

               effect(snow_effect, 1, temp.x, nullvector);

               wait (1);

       }

}

 

function snow_effect(PARTICLE *p)

{

       p->vel_x = 1 - random(2);

       p->vel_y = 1 - random(2);

       p->vel_z = -5 + random(3);

       p.lifespan = 120;

       p.alpha = 40 + random(50);

       p.bmap = snow_png;

       p.size = 3;

       p.flags |= (BRIGHT | MOVE);

       p.event = NULL;

}

 

Der Code für die Schneepartikel ist typisch; er erstellt kleine snow.png Bitmaps in einem Gebiet zwischen -1000 und +1000 Quants in x und y Richtung sowie 200 Quants über dem Ursprung in z Richtung. Die Partikel haben zufällige vel_x und vel_y Werte und einen negativen vel_z Wert, damit sie nach unten fallen. Wir verwenden keine Event Funktion für die Partikel, weil wir sie nicht transparent machen wollen.

 

Ok, aber wie bekommen wir es hin, dass der Schnee über unseren Feldern angezeigt wird? Die Panels sind doch immer über den Partikeln, richtig? Wie Sie wissen, bekommt das GameStudio beinahe täglich neue Features hinzu und eines dieser neuen Features ist es, dass nun Panels mit negativen Layer Werten definiert werden können, die hinter die Partikeleffekte gerendert werden. Dies geschieht, wenn ich layer_value am Anfang der Funktion read_level_data auf -100 setze:

 

aum83_workshop5

 

Nun sind die Panels komplett verschwinden. Dies liegt daran, dass sky_color nicht transparent ist. Ändern wir dies direkt:

 

       vec_set(sky_color, vector(0, 0, 0)); // set a transparent sky color

 

aum83_workshop6

 

Nun kommen wir der Sache näher! Das Level ist zurück, die Partikel erscheinen darüber, aber die schwarzen Ränder sehen nicht so gut aus, vor allem weil psychodelische Partikelspuren zu sehen sind. Dies lässt sich leicht beheben, indem man ein blaues Panel verwendet, das einen schwarzen (RGB = 000) Bereich freilässt, durch den die 2D Felder sichtbar werden.

 

aum83_workshop7

 

Ich bin ein netter Mensch, daher habe ich die border.tga Datei mit in den Projektordner gelegt, falls Sie einen ähnlichen Effekt brauchen. Eine andere Option wäre es, den Schnee mit ent_create zu erstellen und Sprites zu verwenden, die einen größeren Layer-Wert haben, dies würde aber mehr Ressourcen verbrauchen als Partikel es tun. Nachdem dies also alles funktioniert, schauen wir uns die 2dtiles3.c Demo an, die einige Gameplay Elemente hinzufügt.

 

aum83_workshop8

 

Nun steuern wir einen schönen Spieler Charakter mit Hilfe der Pfeiltaste und sammeln große gelbe Edelsteine ein, indem wir sie berühren. Jedes Mal wenn wir einen Edelstein einsammeln, wird ein neuer an einer anderen Stelle erstellt und die Punktzahl wird erhöht. Schauen wir uns an, was sich gegenüber der bisherigen Demo geändert hat.

 

PANEL* player_pan;

PANEL* gem_pan;

 

PANEL* score_pan =

{        

       digits (460, 725, "Score: %.0f", arial_font, 1, game_score);

       flags = SHOW;

}

 

Zunächst gibt es jetzt zwei Panel Pointer für den Spieler und den Edelstein und ein Punkte Panel, mit einem "digits" Element.

 

function player_startup()

{

       VECTOR player_speed;

       var horizontal_speed = 0;

       var vertical_speed = 0;

       player_pan = pan_create("bmap = player.png;", 150);

       player_pan.pos_x = 383;

       player_pan.pos_y = 225;

       player_pan.flags |= VISIBLE;

       player_pan.center_x = player_pan.size_x * 0.5;

       player_pan.center_y = player_pan.size_y;

 

Die Funktion, die den Spieler kontrolliert, erstellt das Panel und platziert es bei x = 383 und y = 225 auf dem Bildschirm. Man hätte alternativ auch ein spezielles 2D Feld im level.txt für den Spieler definieren und es zusammen mit einem Grasteil an die entsprechende Stelle setzen können. Wir haben nur einen Frame unseres Spielers, also drehen wir ihn etwas während er sich bewegt, um für Animation zu sorgen. Der Rotationspunkt liegt an seinen Füßen.

 

       while (1)

       {

               vec_set(player_speed.x, accelerate (horizontal_speed, 2 * (key_cur - key_cul), 0.3));

               player_pan.pos_x += player_speed.x;

               vec_set(player_speed.y, accelerate (vertical_speed, 2 * (key_cud - key_cuu), 0.3));

               player_pan.pos_y += player_speed.y;

               if (key_cul + key_cur + key_cuu + key_cud)

               {

                       player_pan.angle += 0.3 * sin(40 * total_ticks);

               }

               else

               {

                       player_pan.angle = 0;

               }

               wait (1);

       }

}

 

Wir verwenden "accelerate", um den Spieler in x und y Richtung zu bewegen. Wir können verschiedene Werte für Beschleunigung (2) und Reibung (0.3) verwenden. Falls mindestens eine der Pfeiltasten gedrückt wird, dreht sich das Panel ein wenig, ansonsten wird der Winkel auf 0 gesetzt.

 

function gem_startup()

{

       VECTOR gem_pos;

       while (1)

       {

               gem_pan = pan_create("bmap = gem.png;", 150);

               gem_pan.pos_x = 200 + random(560);

               gem_pan.pos_y = 150 + random(400);

               gem_pan.flags |= VISIBLE;

               while ((abs(player_pan.pos_x - gem_pan.pos_x) > 30) || (abs(player_pan.pos_y - gem_pan.pos_y) > 30))

               {

                       wait (1);

               }

               snd_play(gem_wav, 100, 0);

               game_score += 5 + random(10); // add a random score value to game_score

               ptr_remove(gem_pan); // remove the gem - the player has collected it!

               wait (1);

       }

}

 

Die Funktion für den Edelstein platziert diesen an einer zufälligen Position im Level und macht ihn sichtbar. Wir verwenden eine primitive Form der Kollisionerkennung (wenigstens funktioniert es!), um festzustellen, ob der Spieler nah genug an den Edelstein herangekommen ist. Sobald dies der Fall ist, ertönt gem.wav, die Punktzahl wird um einen Wert zwischen 5 und 15 erhöht und das Edelstein Panel wird entfernt, so dass ein neuer Edelstein an einer zufälligen Stelle erstellt werden kann.

 

Momentan kann der Spieler über Wasser und sogar aus dem Level hinausgehen, aber das beheben wir in der nächsten Ausgabe. Bis dann!