Tutorials - Make your Game Data driven

Sprachenübersicht/Programmierung/C / C++/ C#/Scripting

Make your Game Data driven

Diese Seite wurde 4453 mal aufgerufen.

Dieser Artikel wurde in einem Wikiweb System geschrieben, das heißt, Sie können die Artikel jederzeit editieren, wenn Sie einen Fehler gefunden haben, oder etwas hinzufügen wollen.

Editieren Versionen Linkpartnerschaft Bottom Printversion

Keywords: scripting, Datei lesen, script editor, Anleitung, C++, Beispiel, KI

Inhaltsverzeichnis



Orginal Version

Vorwort Top



Scriptsprache, was ist das?

Eine Scriptsprache ist eine Art selber erfundene Programmiersprache. Sie ist dazu da, den eigentlichen Programmaufbau zu vereinfachen und schnelle Änderungen zuzulassen. Der Code wird in eine Datei geschrieben, meist eine Binär- oder Textdatei. In einem Programm(z.B. in Visual C++) werden dann die Daten ausgelesen, interpretiert und mittels Variablen ins Programm eingebaut.
Anwendung: Scriptsprachen werden oft in Spielen verwendet. Sie sind z.B. bei einem Rollenspiel nützlich für die Verwaltung der verschiedenen Ereignisse oder bei einem Strategiespiel für die verschiedenen Werte(Kosten, Ressourcen, Einheitsstärke, etc...). Es lassen sich leicht durch eine Zeile oder nur wenige Zeichen im Script Werte hinzufügen oder verändern. Der Programmaufbau bleibt dabei gleich.
Der Code des Tutorials ist der Einfachheit wegen für eine Konsolenanwendung geschrieben. Falls du noch keine Erfahrung mit dem Umgang mit Windowsprogrammen hast, ist das ein weiterer Vorteil. Für die anderen wird es kein großes Problem sein, den Code in ein Windowsprogramm zu packen. Voraussetzungen für dieses Tutorial: Du solltest zum besseren Verständnis des Quellcodes die Grundlagen der Sprache C++ beherrschen und einen Editor bzw. Compiler und Linker für diese Sprache besitzen. Weitere Kenntnisse wirst du dir im Laufe des Tutorials erarbeiten.

Kapitel 1 - Dateiarbeit Top



Scriptsprachen erfordern die Arbeit mit Dateien, da dies eines ihrer wichtigsten Elemente ist. In diesem Kapitel wirst du ein kleines Programm mit einer Klasse erstellen, die Daten aus einer Binärdatei ausliest und hineinschreibt.

Wir werden zuerst ein kleines Konsolenprogramm erstellen. Klicke auf Neu->Datei->Projekte->Win32-Konsolenanwendung und tippe einen Projektnamen ein(z.B. Kapitel1).
Erstelle eine Quellcode-Datei(Name: z.B. "Main.cpp") und füge sie dem Projekt hinzu.
Schreibe folgenden Code hinein.

Code:



#include <iostream>
int main(void)
{
   return 0;
}



Nun wollen wir eine Klasse für die Dateiarbeit schreiben. Füge eine Headerdatei zum Projekt hinzu und nenne sie Dateiklasse. Nun wird der Header stdio.h eingefügt und eine Klasse mit drei Funktionen erstellt.

Code:

#include <stdio.h>

class cccDataccc
{
public:

    char ReadData(char* Pfad);
    void WriteData(char CharVar, char* Pfad);
    bool GetSuccessful(void) {return cSuccessful;};

private:

    //Enthält Ergebnis der cccDataccc-Funktionen
    bool cSuccessful;
}



Die erste Funktion ReadData soll eine Variable vom Typ char aus der Datei auslesen und zurückgeben. Die zweite Funktion WriteData soll eine Variable in die Datei schreiben. Die letzte Funktion gibt eine boolesische Variable zurück, die enthält, ob die Variable mit ReadData gelesen werden konnte.
Nun ergänzen wir den Code der Memberfunktionen:

Code:


//Liest eine char Variable aus einer binären Datei
//Parameter:
// char* Pfad: Pfad, an dem die Datei zu finden ist
//Rückgabewert:
// char charVar: aus Datei gelesen
char cccDataccc::ReadData(char* Pfad)
{

    //Datei öffnen
    FILE* dataFile=fopen(Pfad,"rb+");

    //Wenn der Cursor nicht am Dateiende ist...
    if(!feof(dataFile))
    {

        //char-Variable anlegen
        char charVar;
        //...Bytes der Größe eines chars aus der Datei lesen
        fread(&charVar,sizeof(charVar),1,dataFile);
        //Variable konnte gelesen werden
        cSuccessful=true;
        return charVar;

    }
    //Cursor am Dateiende oder Datei leer
    //Variable konnte nicht gelesen werden
    else cSuccessful=false;

    //Datei schließen
    fclose(dataFile);

    //0 wird bei Fehler zurückgegeben
    return 0; 

}


//Schreibt eine char Variable in eine binäre Datei
//Parameter:
// char CharVar: in Datei schreiben
// char* Pfad: Pfad, an dem die Datei zu finden ist
//Rückgabewert:
// keine
void cccDataccc::WriteData(char CharVar, char* Pfad)
{

    //Datei öffnen
    FILE* dataFile=fopen(Pfad,"rb+");

    //CharVar als char in Datei schreiben
    fwrite(&CharVar,sizeof(CharVar),1,dataFile);

    //Datei schließen
    fclose(dataFile); 

}



Beide Funktionen haben den Parameter Pfad, die den Pfad zur Scriptdatei enthält. Diese Datei wird am Anfang geöffnet. Als nächstes wird die Variable gelesen/geschrieben, wobei beim Lesen mit feof(FILE) geprüft werden muss, ob der Cursor am Ende der Datei ist, in diesem Fall ob die Datei leer ist, da nur eine Variable gelesen wird.
Um nun auch etwas von der Klasse zu haben, müssen wir sie auch ins Hauptprogramm implementieren.

Code:


#include <iostream.h>
#include "Dateiklasse.h"


char variable1;
char variable2;
char Pfad[]="Data.bin";

int main(void)
{

    //Klassenobjekt anlegen
    cccDataccc DateiKlasse;

    //char vom Benutzer fordern
    cout<<"Bitte geben Sie einen char ein: ";
    cin>>variable1;

    //diesen char in die Datei "Data.bin" schreiben
    DateiKlasse.WriteData(variable1,Pfad);

    //Variable des Types char aus der Datei "Data.bin" lesen
    variable2=DateiKlasse.ReadData(Pfad);

    //Gelesene Variable ausgeben
    cout<<"Gelesene Variable: "<<variable2<<"
"; 

   return 0;
}



Wichtig: Am Anfang des Programms muss die Headerdatei, die die Klasse enthält eingebunden werden. Darum
#include "Dateiklasse.h".
Als nächstes werden zwei char Variablen und ein Feld für den Dateipfad deklariert. Nun wird eine char-Variable vom Benutzer eingelesen und an die Funktion WriteData der vorher erstellten Klasse DateiKlasse übergeben.
Als letztes wird die zweite char-Variable mit dem mit ReadData gelesenen Wert belegt und ausgegeben.
Wichtig: Die Datei "Data.bin" muss bereits im Verzeichnis der Exe-Datei existieren. Um das zu garantieren, öffne den Windows Texteditor(Start->Programme->Zubehör->Editor) und klicke auf Datei->Speichern unter. Als Dateinamen gibst du jetzt "Data.bin" (mit Anführungszeichen!) ein und speicherst in dem Ordner, in dem sich auch das ganze Projekt befindet.

Kapitel 2 - Quantitatives Lesen und Schreiben Top



Nun haben wir die Grundlagen der Scriptsprachen programmiert. Da es aber in den meisten Fällen erforderlich ist, mehrere Daten aus einer Datei zu lesen, werden wir nun die Dateiklasse erweitern. Das Ziel dieses Kapitels ist ein Programm, das fünf Variablen vom Benutzer einliest, diese in eine Scriptdatei schreibt, am Schluss wieder ausliest und nacheinander als char-Variablen ausgibt.

Zuerst fügen wir eine Konstruktor und einen Destruktor hinzu, wobei im letzteren immer die Datei geschlossen wird, falls das noch nicht getan worden ist.
Folgende zwei Zeilen kommen hinter das Schlüsselwort public: in der Klassendefinition.

Code:


cccDataccc();
~cccDataccc();



Die variable cFile fügen wir zur Klasse unter der boolesischen cSuccessful Variable hinzu. Sie enthält immer einen Zeiger auf die aktuell geöffnete Datei. Falls keine geöffnet ist, enthält er 0.

Code:


FILE* cDataFile;


Nun wird der Code des Kon-/Destruktors in die Headerdatei Dateiklasse.h geschrieben.

Code:


//Konstruktor, setzt Zeiger der Datei auf NULL
cccDataccc::cccDataccc()
{

    cDataFile=NULL; 

}

//Destruktor, Schließt falls nötig die geöffnete Datei
cccDataccc::~cccDataccc()
{

    if(cDataFile!=0)
    {

        fclose(cDataFile);
        cDataFile=NULL; 

    } 

}



Damit nun auch immer der Zeiger auf die Datei verwendet wird, ersetzen wir überall in der Klasse die Variable dataFile durch die Klassenvariable cDataFile und löschen in der ReadData- und WriteData-Funktion die folgenden Zeilen.

Code:


//Datei schließen
fclose(dataFile);



Für das reibungslose Funktionieren müssen wir noch am Anfang der beiden Funktionen den Aufruf der Dateiöffnen-Funktion löschen und durch folgendes ersetzen.

Code:


/Nur wenn die Datei nicht (mehr) geöffnet ist
if(cDataFile==0)
{

    //Datei öffnen
    cDataFile=fopen(Pfad,"rb+"); 

}
//Sonst wird der Zeiger der geöffneten Datei verwendet
//Und an der alten Position weitergeschrieben/gelesen



Jetzt müssen wir nur noch eine Memberfunktion zum Schließen der Datei schreiben. Das ist in diesem Programm notwendig, da in einem Programmdurchlauf geschrieben und gelesen wird und es soll ja erreicht werden, dass nach dem Schreiben wieder vom Anfang der Datei gelesen werden kann. Also fügen wir zum "Public-Block" in der Datei "Dateiklasse.h" diese Funktionsdefinition hinzu:

Code:


void SchliesseDatei(void);
Der Code der Funktion ist der selbe wie der des Destruktors:
//Schließt eine geöffnete Datei
//Parameter:
// keine
//Rückgabewert:
// keiner
void cccDataccc::SchliesseDatei(void)
{

    if(cDataFile!=0)
    {

        fclose(cDataFile);
        cDataFile=NULL; 

    } 

}



Um nun auch etwas von dem neuen Code zu haben, müssen wir noch ein paar Dinge im Hauptprogramm (Main.cpp) ändern.
Zuerst nehmen wir statt den einzelnen char-Variablen ein char-Feld und fordern den Benutzer auf, nacheinander 5 Charzeichen einzugeben. Lösche alle Variablen, die vor der main-Funktion deklariert wurden und schreibe stattdessen die folgenden Variablen:

Code:


char variable[5];
char variableGelesen[5];
char Pfad[]="Data.bin";



Lösche den Code der main-Funktion(alles, was zwischen

Code:


int main(void)
{



und der letzten geschweiften Klammer steht).
Schreibe statt dessen den Code fürs Einlesen von allen fünf Variablen. Dafür benutzen wir eine for-Schleife:

Code:


//char 1-5 vom Benutzer fordern
for(short counterWrite=1; counterWrite<=5; counterWrite++)
{

    cout<<"Bitte geben Sie den "<<counterWrite<<". char ein: ";
    cin>>variable[counterWrite-1];
    //Variable in Scriptdatei schreiben
    DateiKlasse.WriteData(variable[counterWrite-1],"Data.bin"); 

}



Als nächstes muss die Datei mit

Code:


//Scriptdatei schließen DateiKlasse.SchliesseDatei();



geschlossen werden und die Variablen mit einer zweiten for-Schleife wieder aus der Scriptdatei gelesen werden:

Code:


//char 1-5 vom Benutzer fordern
for(short counterRead=1; counterRead<=5; counterRead++)
{

    //Variable aus Scriptdatei lesen
    variableGelesen[counterRead-1]=DateiKlasse.ReadData("Data.bin");
    cout<<counterRead<<". char: "<<variableGelesen[counterRead-1]<<"\n"; 

}



In diesem Programm wird zuerst die Klasse erstellt, als zweites char-Variablen eingelesen und in die Scriptdatei geschrieben. Dann wird die Datei geschlossen, erneut in der ReadData-Funktion geöffnet und die gelesenen Variablen auf dem Bildschirm ausgegeben. Im Destruktor, der automatisch am Ende des Programms aufgerufen wird, wird die Datei dann entgültig geschlossen.

Kapitel 3 - Qualitatives Lesen und Schreiben Top



In Scriptsprachen ist es manchmal nützlich oder sogar erforderlich, dass verschiedene Datentypen geschrieben und gelesen werden. Bis jetzt konnte das Programm nur char-Variablen lesen(Also einen Zahlenumfang von 0-255). Nun wollen wir uns mal über die Qualität der Scriptwerte kümmern. Wir werden in diesem Kapitel das Programm aus Kapitel 2 so erweitern, dass man die Grundtypen von C++ in die Scriptdatei lesen und schreiben kann. Falls es dir zu wenige Typen sind, kannst du dir jederzeit welche mit ein paar Zeilen Code hinzufügen.

Als erstes muss in der Datei "Dateiklasse.h" der Header windows.h eingebunden werden, damit das Programm auch Zugriff auf weitere Datentypen hat.

Code:


#include <windows.h>



Dann erstellen wir selbst einen Typen, der Information darüber enthält, welchen Datentypen den Memberfunktionen übergeben werden.

Code:


enum TYPES{TYPE_char, TYPE_bool, TYPE_short, TYPE_int, TYPE_float};



Jetzt kann eine Variable vom Typ TYPES die Werte TYPE_char, TYPE_bool, TYPE_short, TYPE_int und TYPE_float enthalten. Nun fügen wir der Write- und ReadData-Funktion der Dateiklasse einen Parameter dieses Typs hinzu:

Code:


char ReadData(char* Pfad, TYPES Typ);
void WriteData(char charVar, char* Pfad, TYPES Typ);



Auch der Funktionskopf muss umgeändert werden.

Code:


//Liest eine char Variable aus einer binären Datei
//Parameter:
// char* Pfad: Pfad, an dem die Datei zu finden ist
// TYPES Typ: Typ, der der Funktion übergeben wurde
//Rückgabewert:
// char charVar: aus Datei gelesen
char cccDataccc::ReadData(char* Pfad, TYPES Typ)
{

//Schreibt eine char Variable in eine binäre Datei
//Parameter:
// char CharVar: in Datei schreiben
// char* Pfad: Pfad, an dem die Datei zu finden ist
// TYPES Typ: Typ, der der Funktion übergeben wurde
//Rückgabewert:
// keine
void cccDataccc::WriteData(char CharVar, char* Pfad, TYPES Typ)
{



Nun ersetzen wir überall in der Datei CharVar durch Var, da ja unterschiedliche Typen gelesen werden sollen, und in den beiden Memberfunktionen folgende Zeilen

Code:


//...Bytes der Größe eines chars aus der Datei lesen
fread(&charVar,sizeof(charVar),1,dataFile);

//CharVar als char in Datei schreiben
fwrite(&CharVar,sizeof(CharVar),1,dataFile);



durch eine switch-Anweisung:
(in der ReadData-Funktion)


Code:


//Je nach Datentyp Bytes unterschiedlicher Menge lesen
switch(Typ)
{

    case TYPE_char:
    {

        //Variable des richtigen Typs anlegen
        char var;
        //...Bytes der Größe eines chars aus der Datei lesen
        fread(&var,sizeof(char),1,cDataFile);
        //Gelesene Variable zurückgeben.
        return var; 

    }
    case TYPE_bool:
    {

        //Variable des richtigen Typs anlegen
        bool var;
        //...Bytes der Größe eines bool aus der Datei lesen
        fread(&var,sizeof(bool),1,cDataFile);
        //Gelesene Variable zurückgeben.
        return var; 

    }
    case TYPE_short:
    {

        //Variable des richtigen Typs anlegen
        short var;
        //...Bytes der Größe eines short aus der Datei lesen
        fread(&var,sizeof(short),1,cDataFile);
        //Gelesene Variable zurückgeben.
        return var; 

    }
    case TYPE_int:
    {

        //Variable des richtigen Typs anlegen
        int var;
        //...Bytes der Größe eines int aus der Datei lesen
        fread(&var,sizeof(int),1,cDataFile);
        //Gelesene Variable zurückgeben.
        return var; 

    }
    case TYPE_float:
    {

        //Variable des richtigen Typs anlegen
        float var;
        //...Bytes der Größe eines float aus der Datei lesen
        fread(&var,sizeof(float),1,cDataFile);
        //Gelesene Variable zurückgeben.
        return var; 

    }
    default:
    {

        //Variable des richtigen Typs anlegen
        BYTE var;
        //1 Byte aus der Datei lesen
        fread(&var,1,1,cDataFile);
        //Gelesene Variable zurückgeben.
        return var; 

    } 

}



(in der WriteData-Funktion)

Code:


//Je nach Datentyp Bytes unterschiedlicher Menge schreiben
switch(Typ)
{

    case TYPE_char:
    {

        //Variable des richtigen Typs anlegen
        char var=(char)Var;
        //var als char in Datei schreiben
        fwrite(&var,sizeof(char),1,cDataFile);
        break; 

    }
    case TYPE_bool:
    {

        //Variable des richtigen Typs anlegen
        bool var=(bool)Var;
        //Var als bool in Datei schreiben
        fwrite(&var,sizeof(bool),1,cDataFile);
        break; 

    }
    case TYPE_short:
    {

        //Variable des richtigen Typs anlegen
        short var=(short)Var;
        //Var als short in Datei schreiben
        fwrite(&var,sizeof(short),1,cDataFile);
        break; 

    }
    case TYPE_int:
    {

        //Variable des richtigen Typs anlegen
        int var=(int)Var;
        //Var als int in Datei schreiben
        fwrite(&var,sizeof(int),1,cDataFile);
        break; 

    }
    case TYPE_float:
    {

        //Variable des richtigen Typs anlegen
        float var=(float)Var;
        //Var als float in Datei schreiben
        fwrite(&var,sizeof(float),1,cDataFile);
        break; 

    }
    default:
    {

        //Variable des richtigen Typs anlegen
        BYTE var=(BYTE)Var;
        //Var mit einem Byte in Datei schreiben
        fwrite(&var,1,1,cDataFile);
        break; 

    } 

}



Damit nun auch im Extremfall größere Werte(wie z.B. long double) zurückgegeben werden können, müssen jetzt noch die Variablentypen der Rückgabewerte und Parameter von char auf long double geändert werden.

Code:


long double ReadData(char* Pfad, TYPES Typ);
void WriteData(long double Var, char* Pfad, TYPES Typ);


//Liest eine Variable aus einer binären Datei
//Parameter:
// char* Pfad: Pfad, an dem die Datei zu finden ist
// TYPES Typ: Typ, der der Funktion übergeben wurde
//Rückgabewert:
// long double Var: aus Datei gelesen
long double cccDataccc::ReadData(char* Pfad, TYPES Typ)
{

    //Nur wenn die Datei nicht (mehr) geöffnet ist
    if(cDataFile==0)
    {

        //Datei öffnen
        cDataFile=fopen(Pfad,"rb+"); 

    }
    //Sonst wird der Zeiger der geöffneten Datei verwendet
    //Und an der alten Position weitergeschrieben/gelesen

    //long double-Variable anlegen
    long double Var; 




//Schreibt eine Variable in eine binäre Datei
//Parameter:
// long double Var: in Datei schreiben
// char* Pfad: Pfad, an dem die Datei zu finden ist
// TYPES Typ: Typ, der der Funktion übergeben wurde
//Rückgabewert:
// keine
void cccDataccc::WriteData(long double Var, char* Pfad, TYPES Typ)
{



Zuletzt müssen wir noch beim Aufruf der Funktionen in der Datei "Main.cpp" den letzten Parameter übergeben.

Code:


//Variable in Scriptdatei schreiben
DateiKlasse.WriteData(variable[counterWrite-1],"Data.bin",TYPE_char);


//Variable aus Scriptdatei lesen
variableGelesen[counterRead-1]=DateiKlasse.ReadData("Data.bin",TYPE_char);



Für TYPE_char kannst du jetzt alle anderen definierten Typen angeben. Wenn du einen weiteren Typ verwenden willst, musst du nur in der enum-anweisung den Typ hinzufügen und im switch-Ausdruck der beiden Memberfunktionen den neuen Typ ergänzen.


Code:


//Beispiel für LONG
enum TYPES{TYPE_char, TYPE_bool, TYPE_short, TYPE_int, TYPE_float, TYPE_LONG};

//[...]

//ReadData-funktion
case TYPE_ LONG:
{

    //Variable des richtigen Typs anlegen
    LONG var;
    //...Bytes der Größe eines LONG aus der Datei lesen
    fread(&var,sizeof(LONG),1,cDataFile);
    //Gelesene Variable zurückgeben.
    return var; 

}


//WriteData-funktion
case TYPE_ LONG:
{

    //Variable des richtigen Typs anlegen
    LONG var=( LONG)Var;
    //Var als LONG in Datei schreiben
    fwrite(&var,sizeof(LONG),1,cDataFile);
    break; 

}



Kapitel 4 - Klasse für Scriptbefehl Top



In diesem Kapitel erstellen wir eine Klasse, mit der man leicht einen ganzen Scriptbefehl(meistens Anreihung verschiedener Daten) lesen und schreiben kann. Das wird erreicht, indem entweder eine bestimmte Anzahl an Variablen, die ein Scriptbefehl enthält, definiert wird und sooft aus der Datei Daten gelesen werden, oder indem man einen Wert für ein Zeichen reserviert, dass das Ende des Scriptbefehls kennzeichnet. Das hat aber nur einen Sinn, wenn alle verwendeten Datentypen gleich viele Bytes verbrauchen. In diesem Kapitel programmieren wir die erste Variante; d.h. es ist die Anzahl der Variablen konstant, aber die Typen dürfen unterschiedlich sein.
Zuerst erstellen wir einen neuen Header für die Scriptklasse(z.B. Scriptklasse.h). Binde in diesen Header jetzt die ReadWriteKlasse ein.

Code:

#include "ReadWriteKlasse.h"



Entferne in 'Main.cpp' diese Zeile und schreibe statt dessen:

Code:


#include "Scriptklasse.h"



Nun wird die ReadWriteKlasse indirekt über die Scriptklasse ins Hauptprogramm eingebunden.
Als nächstes fügen wir die Scriptklasse mit einer Funktion und drei Variablen hinzu:

Code:


const int ANZAHL_VARIABLEN=20;

class cccScriptccc
{
public:

    void DefineScript(TYPES Ausdruecke[ANZAHL_VARIABLEN], int AnzahlAusdruecke); 

private:

    cccDataccc cDateiklasse;
    int cAnzahlAusdruecke;
    TYPES cScriptausdruecke[ANZAHL_VARIABLEN]; 

};



cAnzahlAusdruecke entgält die Anzahl der Werte eines Scriptbefehls und das Feld cScriptausdruecke speichert die Typen der Variablen in der richtigen Reihenfolge. Die Konstante ANZAHL_VARIABLEN enthält die maximale Menge der Ausdrücke. Wenn du mehr als zwanzig brauchst, kannst du sie auch noch erhöhen.
Nun schreiben wir für diese Klasse eine Memberfunktion, mit der die Anzahl eines Scriptbefehls und die Reihenfolge der Variablentypen festgelegt wird.

Code:


//Definiert die Variablentypen eines Scriptausdrucks
//Parameter:
// TYPES Ausdruecke[ANZAHL_VARIABLEN]: Feld, das die Typen der Variablen enthält, -1 bedeutet kein Typ
// TYPES AnzahlAusdruecke: Endhält die Anzahl der Werte in einem Scriptbefehl
//Rückgabewert:
// keiner
void cccScriptccc::DefineScript(TYPES Ausdruecke[ANZAHL_VARIABLEN], int AnzahlAusdruecke)
{

    //In einer Schleife die Scriptvariablentypen der Klasse belegen
    for(int counter=0; counter< AnzahlAusdruecke; counter++)
    {

        cScriptausdruecke[counter]=Ausdruecke[counter]; 

    }
    //Anzahl der Ausdrücke des Scriptbefehls übernehmen
    cAnzahlAusdruecke=AnzahlAusdruecke; 

}



In dieser Funktion werden nun die Parameter in die Variablen der Klasse kopiert und der Befehl definiert. Das Feld cScriptausdruecke enthält nun die Variablentypen in der richtigen Reihenfolge und cAnzahlAusdruecke die Anzahl der Variablen.
Diese Funktion muss immer aufgerufen werden, wenn noch kein Scriptbefehl definiert ist, oder wenn eine neue Definition erforderlich ist.
Nun benötigen wir noch jeweils eine Funktion zum lesen und schreiben eines ganzen Scriptbefehls. Die folgende Memberfunktion WriteScripfbefehl schreibt aus den übergebenen Werten einen Scriptbefehl mit der vorher definierten Reihenfolge der Typen.

Code:


//Schreibt einen Scriptbefehl in eine Datei
//Parameter:
// char* Pfad: Der Pfad, an der sich die Scriptdatei befindet
// bool DateiNeuOeffnen: Soll die Datei neu geöffnet werden, steht diese Variable auf true
// long double Variablen[ANZAHL_VARIABLEN]: Enthalten die übergebenen Variablen in der richtigen Reihenfolge
//Rückgabewert:
// keiner
void cccScriptccc::WriteScriptbefehl(char* Pfad, bool DateiNeuOeffnen, long double Variablen[ANZAHL_VARIABLEN])
{

    //Wenn die Datei neu geöffnet werden soll...
    if(DateiNeuOeffnen==true)
    {

        //Scriptdatei schließen
        cDateiklasse.SchliesseDatei(); 

    }
    //In einer Schleife die Variablen des Scriptbefehls durchgehen...
    for(int counter=0; counter<cAnzahlAusdruecke; counter++)
    {

        //...und je nach Typ in die Scriptdatei schreiben
        cDateiklasse.WriteData(Variablen[counter],Pfad,cScriptausdruecke[counter]); 

    } 

}



Der erste Parameter Pfad enthält den Pfad, an dem sich die Scriptdatei befindet. Steht die boolesische Variable DateiNeuOeffnen auf 'true', so wird die Datei neu geöffnet und der Schreibcursor an den Anfang gesetzt.
Das Feld Variablen enthält die Variablen, die geschrieben werden sollen in der richtigen Reihenfolge. Diese werden nun mit der WriteData-Funktion der ReadWriteKlasse in die Scriptdatei geschrieben.
Jetzt wird aber noch eine Funktion zum Lesen eines Scriptbefehls benötigt. Die folgende Memberfunktion liest diesen aus der angegebenen Datei.

Code:


//Liest einen Scriptbefehl aus einer Datei
//Parameter:
// char* Pfad: Der Pfad, an der sich die Scriptdatei befindet
// bool DateiNeuOeffnen: Soll die Datei neu geöffnet werden, steht diese Variable auf true
//Rückgabewert:
// bool: gibt false zurück, falls eine der Variablen nicht gelesen werden konnte bool cccScriptccc::ReadScriptbefehl(char* Pfad, bool DateiNeuOeffnen)
{

    //Wenn die Datei neu geöffnet werden soll...
    if(DateiNeuOeffnen==true)
    {

        //Scriptdatei schließen
        cDateiklasse.SchliesseDatei(); 

    }
    //In einer Schleife die Variablen des Scriptbefehls durchgehen...
    for(int counter=0; counter<cAnzahlAusdruecke; counter++)
    {

        //...und je nach Typ aus der Scriptdatei lesen
        cScriptvariablen[counter]=cDateiklasse.ReadData(Pfad,cScriptausdruecke[counter]);
        //Wenn die Variable nicht gelesen werden konnte...
        if(cDateiklasse.GetSuccessful()==false)
        {

            //Scriptbefehl konnte nicht vollständig gelesen werden
            return false; 

        } 

    }
    //Scriptbefehl konnte vollständig gelesen werden
    return true; 

}



Auch hier wird, falls angegeben, die Datei am Anfang neu geöffnet, um zu sichern, dass vom Anfang an gelesen wird.
Die Funktion gibt 'false' zurück, falls der Cursor am Ende der Datei angekommen ist, der Scriptbefehl also nicht vollständig gelesen werden konnte. Die Werte werden nun in einer Schleife in dem Feld cScriptvariablen gespeichert. Dieses fügen wir noch der Scriptklasse unter cScriptausdruecke hinzu:

Code:


long double cScriptvariablen[ANZAHL_VARIABLEN];



Damit nun auch Funktionen außerhalb der Klasse Zugriff auf die gelesenen Variablen haben, schreiben wir noch eine kleine Funktion, die den gewünschten Wert zurückgibt. Vor dem Schlüsselwort private: fügen wir nun den Funktionskopf der Funktion hinzu.

Code:


long double GetScriptvariable(int Count);



Der Funktion selbst wird dann ein Indexwert übergeben, von dem sie dann das richtige Element des cScriptvariablen-Feldes zurückgibt:

Code:


//Gibt eine der gelesenen Scriptvariablen zurück
//Parameter:
// int Count: Feldindex aus dem die Variable zurückgegeben werden soll
//Rückgabewert:
// long double cScriptvariablen[Count]: Scriptvariable
long double cccScriptccc::GetScriptvariable(int Count)
{

    return cScriptvariablen[Count]; 

}



Damit die Klasse auch etwas nützt, muss im Hauptprogramm noch einiges verändert werden. Lösche nun wieder den Code der main-Funktion und definiere am Anfang die Scriptklasse:

Code:


//Klassenobjekt anlegen
cccScriptccc ScriptKlasse;



Baue nun wieder eine Schleife für die Zuweisung der Variablen

Code:


//Variable 1-5 vom Benutzer fordern
for(short counterWrite=1; counterWrite<=5; counterWrite++)
{

    cout<<"Bitte geben Sie den "<< counterWrite<<". Wert ein: ";
    cin>>variable[counterWrite-1]; 

}



und ändere den Typ des variable-Feldes in 'long double' um, damit der Wertebereich nicht auf den char-bereich eingeschränkt ist. Zusätzlich brauchen wir ein Feld, das die Variablentypen in der richtigen Reihenfolge enthält.

Code:


TYPES scriptTypes[5]={TYPE_int,TYPE_short,TYPE_char,TYPE_bool,TYPE_float};



Wir nehmen hier die Kombination: int, short, char, bool, float. D.h. ein Scriptbefehl soll fünf Variablen in dieser Reihenfolge mit diesen Typen enthalten. Dieser Scriptbefehl muss nun noch definiert werden:

Code:


//Scriptbefehl definieren
ScriptKlasse.DefineScript(scriptTypes,5);



Hier übergeben wir das Feld, das die Typen enthält und die Anzahl der Elemente. Alle nachfolgenden Scriptaufrufe werden nun mit dieser Typreihenfolge geschrieben bzw. gelesen. Um den Scriptbefehl nun in eine Datei zu schreiben, muss noch die Funktion WriteScriptbefehl aufgerufen werden.

Code:


//Scriptbefehl in Datei schreiben
ScriptKlasse.WriteScriptbefehl(Pfad,true,variable);



Dieser Funktion sind nun die vom Benutzer eingegebenen Variablen übergeben worden.
Wenn du statt dessen einen Scriptbefehl auslesen willst, ersetze diese beiden Zeilen durch folgenden Code:

Code:


bool scriptRead=ScriptKlasse.ReadScriptbefehl(Pfad,true);

if(scriptRead ==true)
{

    //char 1-5 als Scriptbefehl auslesen und ausgeben
    for(short counterRead=1; counterRead<=5; counterRead++)
    {

        //Variable aus Scriptdatei lesen
        variableGelesen[counterRead-1]=ScriptKlasse.GetScriptvariable(counterRead-1);
        cout<<counterRead<<". Wert: "<<variableGelesen[counterRead-1]<<"\n"; 

    } 

}
else
{

    cout<<"Fehler aufgetreten\n"; 

}



Hier wird zunächst versucht, den Scriptbefehl zu lesen. War dies erfolgreich, enthält die boolesische Variable scriptRead true. In diesem Fall werden die gelesenen Variablen wieder in einer Schleife durch die Funktion GetScriptvariable ausgelesen und auf den Bildschirm geschrieben.

Kapitel 5 - Übertragung auf ein Spiel Top



In diesem Kapitel siehst du, wie man diese beiden selbst erstellten Klassen in einem Spiel einsetzen kann. Der Quellcode dieses Kapitels ergibt kein ausführbares Programm, sondern dient lediglich zur Veranschaulichung der Übertragung der Scriptklassen auf ein Spiel. Wir nehmen als Beispiel ein Strategiespiel mit verschiedenen Einheiten. Als Vereinfachung nehmen wir insgesamt drei Einheiten. Jede dieser Einheiten besitzt vier Eigenschaften. Die X- und Y-Koordinaten, die Energie, die der Einheit noch zur Verfügung steht und den Waffenschaden, den sie an ihren Gegnern hinterlässt. Dafür definieren wir vier Felder:

Code:


long double x[3];
long double y[3];
long double energie[3];
long double waffenschaden[3];



Der Indexwert kennzeichnet dann die verschiedenen Einheiten(x[1], y[1], energie[1] und waffenschaden[1] gehören zusammen und ergeben eine Einheit).
Angenommen, du willst nun einen Editor für dieses Spiel programmieren, mit dem man per Mausklick oder auch per Tastatur die Gegner setzen kann und diese in eine Scriptdatei geschrieben werden sollen. Nun musst du den Benutzer des Programms irgendwie diese Werte festlegen lassen. Dann schreibst du eine Funktion, die die Werte eines bestimmten Indexes in eine Scriptdatei schreibt. Das könnte z.B. so aussehen:

Code:


//Fasst die Attribute eines Gegners in ein Feld zusammen und schreibt sie in
//eine Scriptdatei
//Parameter:
// int Index: Indexwert des Gegners
// char* Pfad: Pfad, an dem die Datei zu finden ist
//Rückgabewert:
// keine
void WriteEnemyInScript(int Index, char* Pfad)
{

    //Die Typen der Atributvariablen in richtiger Reihenfolge
    TYPES enemyScriptTypes[4]={TYPE_int,TYPE_int,TYPE_float,TYPE_float};

    //Definieren des Scriptbefehls
    ScriptKlasse.DefineScript(enemyScriptTypes,4);

    //Übertragen der Attribute eines Gegners auf ein Feld
    long double enemyAttributs[4]={x[Index],y[Index],energie[Index],waffenschaden[Index]};

    //Schreiben der Attribute
    ScriptKlasse.WriteScriptbefehl(Pfad,false,enemyAttributs); 

}



Hier wird nun zuerst der Scriptbefehl definiert. Damit energie und waffenschaden auch Kommazahlen annehmen können, haben wir für die beiden letzten Typen TYPE_float gewählt. Dann wird ein Feld mit vier Elementen mit den Attributen des Gegners belegt. Zuletzt wird dieses Feld mittels WriteScriptbefehl in die Scriptdatei geschrieben.
Diesen Code kannst du sowohl für einen Editor, als auch für eine Savedatei benutzen. In dieser werden dann die Informationen über den aktuellen Spielstand gespeichert. Die Scriptdatei enthält dann eben den ursprünglichen Spielstand, der bei jedem neuen Spiel wieder hergestellt wird.
Um diese Werte aus der Scriptdatei im Spiel wieder auszulesen, musst du wieder eine Funktion schreiben, die genau das Gegenteil der WriteEnemyInScript-Funktion durchführt:

Code:


//Liest die Attribute eines Gegners aus der Scriptdatei und verteilt sie
//über die im Spiel vorkommenden Variablen
//Parameter:
// int Index: Indexwert des Gegners
// char* Pfad: Pfad, an dem die Datei zu finden ist
//Rückgabewert:
// keine
void ReadEnemyOfScript(int Index, char* Pfad)
{

    //Die Typen der Atributvariablen in richtiger Reihenfolge
    TYPES enemyScriptTypes[4]={TYPE_int,TYPE_int,TYPE_float,TYPE_float};

    //Definieren des Scriptbefehls
    ScriptKlasse.DefineScript(enemyScriptTypes,4);

    //Lesen der Attribute
    bool attributsRead=ScriptKlasse.ReadScriptbefehl(Pfad, false);

    //Wenn die Attribute gelesen werden konnten...
    if(attributsRead==true)
    {

        //Zuweisen der gelesenen Variablen zu den Attributen eines Gegners
        x[Index]=ScriptKlasse.GetScriptvariable(0);
        y[Index]=ScriptKlasse.GetScriptvariable(1);
        energie[Index]=ScriptKlasse.GetScriptvariable(2);
        waffenschaden[Index]=ScriptKlasse.GetScriptvariable(3); 

    }
    else
    {

        //Fehler aufgetreten 

    } 

}



In der ReadEnemyOfScript-Funktion wird wie in der Schreibfunktion zuerst der Scriptbefehl definiert. Dann wird er ausgelesen. Falls kein Fehler aufgetreten ist, die boolesische Variable attributsRead also auf 'true' steht, werden nun die gelesenen Variablen wieder in richtiger Reihenfolge den Spielvarioablen zugewiesen.
Ein solcher Scriptbefehl könnte dann z.B. etwa so aussehen:

Code:


3 €TC ®#A



Aus dieser Zeile kann ein Spieler des Strategiespiels kaum erkennen, welche Attribute dieser Gegner besitzt und sie auch nicht gezielt verändern. Das kann er nur mit Erfolg, wenn er den Code deines Spiels hat, oder wenn er die Scriptdefinition kennt.
Für die Anwendung von Scriptsprachen auf ein Rollenspiel kannst du z.B. Ereignisse in die Scriptdatei schreiben. Du vergibst dann jedem Ereignis eine bestimmte Nummer. Mit dieser Zahl und mit weiteren Daten(z.B. Zeitpunkt/Ort des Ereignisses) kannst du dann im Spiel die Ereignisse interpretieren und ablaufen lassen.
Du musst einfach etwas Fantasie haben und dir eine Scriptsprache ausdenken. Es wäre auch möglich, mit if-Anweisungen zu arbeiten. Z.B. wenn Variable1 den Wert '0' enthält, ergibt das zusammen mit Variable2 den Wert X. Enthält aber Variable1 den Wert 1, ergibt das mit Variable2 den Wert Y. Für X bzw. Y kannst du dann eine Rechnung, wie z.B. "Variable1* Variable2+10", im Programm einfügen.


Anmerkung: Top



Um Mißverständnissen vorzubeugen: Es handelt sich hierbei nicht um eine Scriptsprache. Vielmehr beschreibt es eine Möglichkeit Spieldaten in eine Datei auszulagern um Änderungen am Spiel schneller Vornehmen zu können.
Falls man eine richtige Scriptsprache verwenden möchte empfiehlt es sich eine Vorhandene zu verwenden, da dies ein zu komplexes Thema ist. Meine Empfehlung ist LUA. Diese Scriptsprache ist sehr schnell, besitzt einen eingebauten Garbage Collector und es lassen sich einfach und schnell Funktionen exportieren udn importieren.

Gibt es noch irgendwelche Fragen, oder wollen Sie über den Artikel diskutieren?

Editieren Versionen Linkpartnerschaft Top Printversion

Haben Sie einen Fehler gefunden? Dann klicken Sie doch auf Editieren, und beheben den Fehler, keine Angst, Sie können nichts zerstören, das Tutorial kann wiederhergestellt werden

Sprachenübersicht/Programmierung/C / C++/ C#/Scripting/Make your Game Data driven