Tutorials - Scriptsprachen Tutorial

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

Scriptsprachen Tutorial

Diese Seite wurde 3446 mal aufgerufen.

Diese Artikel wurde als uneditierbar gepostet, und darf, wenn nicht ausdrücklich erlaubt nicht auf anderen Seiten verbreitet, oder editiert werden! Printversion

Keywords: Anleitung, Tutorial, scripten, scriptsprachen, hilfe, scriptspache programmieren, entwickeln, C++, Editor

Inhaltsverzeichnis



Vorwort Top



Dies ist ein Tutorial über die Verwendung von Skripts. Es erklärt das Erstellen eines Skripteditors, und das Verwenden von Skripts in Programmen.

Dieses Tutorial wurde für eine C++ Konsolenanwendung konzipiert. Vorraussetzung für das Verständnis sind Erfahrung mit Klassen, Dateiarbeit und dynamischen Datenstrukturen(Listen).

Für Anregungen, Kritik, Lob etc. zu diesem Tutorial stehe ich (Christopher Dresel) gerne zur Verfügung.

Email: dresel [a_t] gmx.at

Was ist ein Skript? Top



Ein Skript ist eine Abfolge von vorher definierten Aktionen (ähnlich einer C++-Anweisung), die in einer (Binär)Datei gespeichert sind. Diese werden dann von Programmen geladen und ausgeführt. Skripts werden bevorzugt in Spielen verwendet, um den Spielablauf zu steuern. So lassen sich Dinge im Spiel ändern (in dem der Skript geändert wird), ohne dass das Projekt neu kompiliert werden muss. Dies wiederum spart Zeit und Arbeit.

Eine Aktion könnte zum Beispiel sein:

Move MainPlayer to 12 34

Diese Aktion bewegt den Hauptspieler auf die Position 12 34. Würde man diese Aktion mit einer Funktion vergleichen, wären die schwarzen Wörter der Funktionsname, und die grünen die Parameter. Nur gibt es bei diesen Aktionen keinen Rückgabewert. MainPlayer könnte zum Beispiel auch Endboss heißen, und die Position könnte statt 12 34 auch 98 76 lauten. Diese Parameter nennen wir ab jetzt Aktionseinträge.

Der Effizienz halber wird der Skript als Binärdatei gespeichert. Die Beschreibung (oder auch Templates genannt, siehe nächstes Kapitel) der Aktionen werden vorher in einer Textdatei gespeichert. Dieses Template gibt an, wie der Aktionstext lautet, und wie viele bzw. was für Einträge (Parameter) die Aktion enthält. Diese Templates wiederum werden dann von einem einfachen Skripteditor geöffnet, um das erstellen von Skripts einfacher zu gestalten.

Das Programm, das den Skript aufruft, durchwandert jede Aktion und ruft die dazugehörige Funktion auf. Meistens werden die abgespeicherten Aktionseinträge dann als Parameter an diese Funktion übergeben.

Die ActionTemplateklasse Top



Wie schon vorher erwähnt, werden die Templates der Aktionen in einer Textdatei gespeichert. Für was brauchen wir diese Templates? Wir werden sie später für den Skripteditor verwenden. Für das Ausführen von Skripts ist die ActionTemplateklasse nicht notwendig.

Nehmen wir die vorherige Aktion

Move MainPlayer to 12 34

Bei einem Template gibt es hier keine Einträge, sondern die Definitionen der Einträge. Diese geben die Datentypen und die Auswahlmöglichkeiten (z.B. einen Zahlenbereich von 0 ? 255) an. Dort wo die Einträge folgen, ersetzen wir sie durch eine Tilde "~".

Die vorherige Aktion würde demnach lauten:

Move ~ to ~ ~

Nun wissen wir, dass diese Aktion drei Einträge enthält. Diese werden danach nun definiert. Es folgt zuerst der Datentyp, danach der Wertebereich.

Für den ersten Eintrag währe das:

CHOICE 2

Diese "Datentyp" gibt an, dass es sich um eine Auswahl von Strings handelt. Nach CHOICE geben wir der Einfachheit halber die Anzahl der Auswahlmöglichkeiten, in diesem Fall 2, an. Was jetzt noch fehlt sind die Einträge selber, denn ein Skripteditor muss wissen, was für Auswahlmöglichkeiten es gibt.

Die gesamte Definition für den ersten Eintrag lautet:

CHOICE 2
"MainPlayer"
"EndBoss"

Nun wurde festgelegt, dass der erste Aktionseintrag eine Auswahl aus zwei Strings ist. Zur Auswahl stehen "MainPlayer" und "EndBoss".

Das gesamte Template für diese Aktion würde dann folgendermaßen Aussehen:

Move ~ to ~ ~

CHOICE 2
"MainPlayer"
"EndBoss"

INT 0 255
INT 0 255

Der erste Eintrag wurde vorher erklärt, und die zwei nachfolgenden sollten auch kein Problem darstellen. Die zwei Zahlen nach dem INT geben lediglich an, zwischen welchem Zahlenbereich (minimaler Wert bzw. maximaler Wert) der Eintrag liegen muss.

Diese Angaben zu einer Aktion werden uns die Arbeit mit einem Skripteditor enorm erleichtern. Denn bei einem Skripteditor suchen wir (Anwender) uns die passende Aktion, und deswegen muss der Editor wissen, wie viele und was für Einträge die Aktion enthält.

Nun müssen wir diese Aktionen und deren Einträge in Klassen unterbringen. Wir werden diese auf drei Klassen aufteilen. Die erste übernimmt die Einträge, die zweite die Aktionen (die wiederum Einträge enthalten), und die letzte übernimmt die Verwaltung einer Aktionsliste.

Ich benenne die Klassen folgend:

1. ActionEntryTemplate
2. ActionTemplate
3. ActionTemplateClass

Mit der ersten Klasse werden wir uns jetzt beschäftigen. Diese Klasse speichert das Template eines Aktionseintrages. Das heißt Datentyp und Wertebereich.

Code:


#include <stdio.h>

#ifndef _ENTRYTYPES_
#define _ENTRYTYPES_

//Die verschiedenen Datentypen
enum EntryTypes { _NONE = 0, _TEXT, _BOOL, _INT, _FLOAT, _CHOICE };

#endif _ENTRYTYPES_

class ActionEntryTemplate
{
public:

    ActionEntryTemplate(void);
    ~ActionEntryTemplate();

    long EntryType;

    union {
        long NumChoices;
        long lMin;
        float fMin;
    };

    union {
        long lMax;
        float fMax;
        char **Choices;
    };
};



Zu Beginn der Klasse haben wir eine Aufzählung der Datentypen, die in einem Aktionseintrag vorkommen können. Die Klasse selber speichert nun diesen Datentyp ab. Außerdem enthält diese Klasse zwei Unions, die bei bestimmten Datentypen gebraucht werden. Und zwar bei INT, FLOAT und CHOICE. Bei INT oder FLOAT wird hier die minimale bzw. maximale Größe der Zahl abgespeichert. Bei CHOICE die Anzahl der Auswahlmöglichkeiten und ein Textarray, das diese Auswahlmöglichkeiten enthält. Ein Text oder ein Bool brauchen keine weiteren Angaben (möglicherweise möchte man die maximale Länge des Textes angeben, wir tun dies hier nicht). Die gesamte Klasse ist public, da der Nutzen bei einer private Deklarierung kleiner ist als der Schreibaufwand. Der Konstrukter initialisiert die Werte, und der Destruktor gibt Choices frei.


Die zweite Klasse speichert nun das Template einer Aktion.

Code:


#include "ActionEntryTemplate.h"

class ActionTemplate
{
public:

    ActionTemplate(void);
    ~ActionTemplate();

    long m_ActionType;
    char m_Text[256];
    short m_NumEntries;

    ActionEntryTemplate *m_Entries;
    ActionTemplate *m_Next;
};



Die Variable m_ActionType gibt die Position in der Liste an, in der sich das Template befindet. Ein abgespeicherter Skript enthält dann diese Nummer, die dann in einer Switchanweisung zur gewünschten Funktion gebracht wird. Ein Vergleichen der Aktionsstrings wäre langsamer. Die Liste der Aktionen ist notwendig, da es ja beliebig viele Aktionen geben kann. Die Variable m_Text speichert den Aktionstext (z.B. "Move ~ to ~ ~"). m_NumEntries gibt die Anzahl der Templates eines Aktionseintrages an (in vorigem Fall drei). Danach folgt ein Array von Templates eines Aktionseintrages, und einen Zeiger auf das nächste Template einer Aktion. Auch hier ersparen wir uns unnötige Arbeit, indem wir die gesamte Klasse public machen. Der Konstruktor übernimmt die Initialisierungen, und der Destruktor gibt m_Entries frei. Außerdem löscht er m_Next, sodass bei der Liste nur der Kopf gelöscht werden muss.

Die dritte und letzte Klasse, die ActionTemplateClass-Klasse verwaltet nun ein Array aus Templates von Aktionen.

Code:


#ifndef _ACTIONTEMPLATE_
#define _ACTIONTEMPLATE_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "ActionTemplate.h"

class ActionTemplateClass
{
public:

    ActionTemplateClass(void);
    ~ActionTemplateClass();

    bool LoadActionTemplate(char* Filename);
    void Free(void);

    long GetNumActions(void);
    ActionTemplate* GetActionHead(void);
    ActionTemplate* GetAction(long Num);

    long GetNumEntries(long ActionNum);
    ActionEntryTemplate* GetEntry(long ActionNum, long EntryNum);
    
private:

    long m_NumActions;
    ActionTemplate *m_ActionHead;

    bool GetNextQuotedLine(char* Data, FILE *fp, long MaxSize);
    bool GetNextWord(char *Data, FILE *fp, long MaxSize);
};

#endif



Die Funktion LoadActionTemplate übernimmt einen String und lädt die ActionTemplate-Liste. Free gibt die Liste wieder frei. GetNumActions liefert die Anzahl der Templates der Aktionen. GetActionHead liefert den Kopf der ActionTemplate-Liste. GetAction liefert das gewünschte Template der Aktionen. GetNumEntries liefert die Anzahl der Aktionseinträge eines Templates der Aktionen und GetEntry liefert einen bestimmten Aktionseintrag eines gewünschten Templates der Aktionen. m_NumActions speichert die Anzahl der ActionTemplates, m_ActionHead den Kopf der ActionTemplate-Liste. GetNextQuotedLine liest aus einer Datei das nächste in Komma gesetztes Wort und speichert dieses in Data. GetNextWord liest das nächste Wort.

In dem Beispiel "ActionTemplateClass" wird der Umgang mit der Klasse gezeigt. Auch sind die einzelnen Funktion aufgelistet und mit Kommentaren erklärt.

Zusammenfassung: Top



Die ActionTemplateClass-Klasse enthält eine Liste von ActionTemplates. Diese wiederum enthalten ein Array von ActionEntryTemplates.

Die ActionTemplateClass verwaltet die ActionTemplates. Sie hat Funktion zum Laden des ActionTemplates aus einer Textdatei.

In einem ActionTemplate wird der Text der Aktion sowie die Anzahl der Einträge gespeichert. Außerdem enthält die Klasse zwei Pointer. Einen auf das nächste ActionTemplate-Element, sowie einen auf die ActionEntryTemplates.

In einem ActionEntryTemplate wird der Datentyp (Int, Float, Bool, Text, Choice) gespeichert. Bei Int und Float werden die Min- und Maxwerte bei Choice die Anzahl der Auswahlmöglichkeiten und die Auswahlmöglichkeiten selbst gespeichert. Die Klasse enthält einen Pointer auf das nächste Element.

Der Skripteditor Top



Da wir nun die Aktionen und deren Einträge aus einer Textdatei lesen, und in eine Liste schreiben können, haben wir nun die erste Voraussetzung für einen SkriptEditor erfüllt. Wir kennen nun die einzelnen Aktionen und deren Einträge. Da wir aber nur Klassen haben, die die Templates für die Skripts speichern, brauchen wir nun Klassen, die einen tatsächlichen Skript speichern können.

Ein Skript enthält ein oder mehrere Aktionen, diese wiederum enthalten Aktionseinträge (Zahlen bzw. Text). Der Aufbau ist logischerweise ähnlich wie bei der ActionTemplateClass-Klasse.

In diesem Kapitel gibt es drei zusätliche Klassen:

1. ActionEntry
2. Action
3. Script

Die erste Klasse speichert einen Aktionseintrag und sieht folgendermaßen aus.

Code:


#include "stdio.h"

#ifndef _ENTRYTYPES_
#define _ENTRYTYPES_

enum EntryTypes { _NONE = 0, _TEXT, _BOOL, _INT, _FLOAT, _CHOICE };

#endif _ENTRYTYPES_

class ActionEntry
{
public:

    ActionEntry(void);
    ~ActionEntry();

    long EntryType;

    union {
        long IOValue;
        long Length;
        long Selection;
        bool bValue;
        long lValue;
        float fValue;
    };

    char *Text;
};



Die Aufzählung der Datentypen sollte vom vorherigen Kapitel bekannt sein. EntryType speichert den Datentyp, auch das ist bekannt. Die folgende Union enhält nun den Eintrag, der benötigt wird. IOValue wird für den Lade/Speichervorgang verwendet, Length falls der Datentyp TEXT entspricht, Selection bei CHOICE, bValue bei BOOL, lValue bei INT und fValue bei FLOAT. Text speichert den Text, falls vorhanden.

Und nun zur zweiten Klasse.

Code:


#include "stdio.h"
#include "ActionEntry.h"

class Action
{
public:

    Action(void);
    ~Action();

    operator =(Action &rhs);

    long m_ActionType;

    long m_NumEntries;
    ActionEntry* m_Entries;

    Action *m_p;
    Action *m_n;
};



Die Klasse enthält einen überladenen = Operator, dieser wird für den SkriptEditor benötigt. m_ActionType speichert den Index der Aktion (die Position in der Liste, Anzahl der ActionTemplates ? 1). m_NumEntries speichert die Anzahl der Aktionseinträge (diese stehen zwar in der ActionTemplate-Liste, beim Ausführen von Skripts sollte man aber ohne sie auskommen). m_Entries speichert die Einträge, und m_p bzw. m_n sind Pointer auf den Vorgänger bzw. Nachfolger.

Und die letzte Klasse verwaltet nun den Skript.

Code:


#include "Action.h"

class Script
{
public:
    
    Script(void);
    ~Script();

    void Free(void);
    bool LoadScript(char *Filename);
    bool SaveScript(char *Filename);

    long m_NumActions;
    Action *m_Head;
};



Free gibt den von der Klasse reservierten Speicher frei, LoadScript lädt einen Skript, SaveScript speichert diesen. m_NumActions speichert die Anzahl der Aktionen, m_Head ist der Pointer auf den Aktionskopf.

Die Lade- und Speicherfunktionen sind im Skripteditorbeispiel erklärt.

Nun haben wir auch die zweite Vorraussetzung für den Skripteditor erfüllt. Unsere Skripteditor muss nun folgende Aufgaben erfüllen können:

1. Laden eines ActionTemplates
2. Laden eines Skriptes
3. Speichern eines Skriptes
4. Ausgabe des ActionTemplates
5. Ausgabe des aktuellen Skriptes (falls vorhanden)
6. Erstellen eines neuen (leeren) Skriptes
7. Hinzufügen einer Aktion des Skriptes
8. Entfernen einer Aktion des Skriptes
9. Vertauschen zweier Aktionen des Skriptes

Die ersten drei Funktionen sollten keine Probleme bereiten, da hier nur die entsprechenden Funktionen aufgerufen werden müssen. Bei der Ausgabe müssen wir den Skript bzw. das AktionTemplate nur Schritt für Schritt durchwandern, und mit cout am Bildschirm ausgeben. Beim Erstellen eines neuen Skriptes wird Script.Free aufgerufen. Die wirklich schwierigeren Aufgaben sind die letzten drei. Und auf diese möchte ich nun näher eingehen.

Hinzufügen einer Aktion des Skriptes Top



Code:


//Welcher Aktionstype (Anzahl der Aktionen - 1)
cout << "\nAktionstype: ";
cin >> ActionType;

//Erzeugt einen Standardskript
hSAction = CreateScriptAction(ActionType);
hSActionPtr = hScript.m_Head;

if(hSActionPtr == NULL)
{
    //Falls Skript leer wird Head zugewiesen
    hScript.m_Head = hSAction;

    hSAction->m_n = NULL;
    hSAction->m_p = NULL;
}
else
{
    //Durchwandern bis das letzte Element gefunden
    while(hSActionPtr->m_n != NULL) hSActionPtr = hSActionPtr->m_n;

    hSActionPtr->m_n = hSAction;

    hSAction->m_n = NULL;
    hSAction->m_p = hSActionPtr;
}

cout << "\n" << hATClass.GetAction(ActionType)->m_Text << "\n";

//Alle Einträge werden durchwandert
for(i = 0; i < hSAction->m_NumEntries; i++)
{
    //Welcher Eintragstype?
    switch(hSAction->m_Entries[i].EntryType)
    {
    case _TEXT:
        {
            cout << "\nAktionseintrag #" << i << " - TEXT\n";
            cout << "\nText: ";

            cout.flush();
            gets(TextBuffer);

            hSAction->m_Entries[i].Length = strlen(TextBuffer) + 1;

            hSAction->m_Entries[i].Text = new char[strlen(TextBuffer) + 1];

            strcpy(hSAction->m_Entries[i].Text, TextBuffer);
        }
        break;

    case _BOOL:
        {
            cout << "\nAktionseintrag #" << i << " - BOOL\n";
            cout << "0: false\n";
            cout << "1: true\n";

            cout << "\nWert: ";

            /*Falls notwendig, könnte hier noch eine Abfrage gemacht werden, ob die Zahl != 0 oder Zahl != 1*/

            cin >> j;
            
            if(j == 0) hSAction->m_Entries[i].bValue = false;
            else hSAction->m_Entries[i].bValue = true;
        }
        break;

    case _INT:
        {
            cout << "\nAktionseintrag #" << i << " - INT\n";
            cout << "Min: " << hATClass.GetAction(ActionType)->m_Entries[i].lMin << "\n";

            cout << "Max: " << hATClass.GetAction(ActionType)->m_Entries[i].lMax << "\n";

            cout << "\nWert: ";

            /*Falls notwendig, könnte hier noch eine Abfrage gemacht werden, ob die Zahl < Min bzw. > Max ist*/

            cin >> hSAction->m_Entries[i].lValue;
        }
        break;

    case _FLOAT:
        {
            cout << "\nAktionseintrag #" << i << " - FLOAT\n";
            cout << "Min: " << hATClass.GetAction(ActionType)->m_Entries[i].fMin << "\n";

            cout << "Max: " << hATClass.GetAction(ActionType)->m_Entries[i].fMax << "\n";

            cout << "\nWert: ";

            /*Falls notwendig, könnte hier noch eine Abfrage gemacht werden, ob die Zahl < Min bzw. > Max ist*/

            cin >> hSAction->m_Entries[i].fValue;
        }
        break;

    case _CHOICE:
        {
            cout << "\nAktionseintrag #" << i << " - CHOICE\n";

            for(j = 0; j < hATClass.GetAction(ActionType)->m_Entries[i].NumChoices; j++)
            {
                cout << j << ": " << hATClass.GetAction(ActionType)->m_Entries[i].Choices[j] << "\n";
            }

            cout << "\nAuswahl: ";
            
            /*Falls notwendig, könnte hier noch eine Abfrage gemacht werden, ob die Auswahl > NumChoice ist*/

            cin >> hSAction->m_Entries[i].Selection;
        }
        break;
    }
}

//Skript wurde um 1 Aktion erhöht
hScript.m_NumActions++;



Zuerst wird die gewünschte Aktion abgefragt. Danach wird die Anzahl der Aktionen um 1 erhöht. Danach wird ein Standardskript erstellt (Initialisierungen mit 0 + Speicherreservierung). Falls der Skript noch keine Aktionen enthielt, wird Head gesetzt. Ansonsten wird der Skript solange durchwandert, bis das letzte Element gefunden wurde. Danach werden Vorgänger und Nachfolger gesetzt. Danach wird eine For-Schleife durchwandert. Zuerst wird der Datentyp abgefragt, und falls notwendig (Text) wird Speicher reserviert. Dies wird solange getan, bis alle notwendigen Werte eingegeben wurde. Ich habe in diesem Beispiel keine Min-Max Abfrage gemacht, wer möchte kann diese noch einfügen.

Löschen eines Eintrages.

Code:


//Löscht einen Eintrag

cout << "Loesche Eintrag #: ";
cin >> h;

hSAction = hScript.m_Head;
if(hSAction == NULL) break;

for(i = 0; i < h; i++)
{
    hSAction = hSAction->m_n;
    if(hSAction == NULL) break;
}

if(hSAction == hScript.m_Head)
{
    hScript.m_Head = hSAction->m_n;

    if(hScript.m_Head != NULL) hScript.m_Head->m_p = NULL;
}
else
{
    hSAction->m_p->m_n = hSAction->m_n;
    if(hSAction->m_n != NULL) hSAction->m_n->m_p = hSAction->m_p;
}

hSAction->m_n = NULL;
hSAction->m_p = NULL;

delete hSAction;

//Skript wurde um 1 Aktion verringert
hScript.m_NumActions--;



Zuerst wird die Nummer der Aktion abgefragt, die gelöscht werden soll. Falls es diese Nummer nicht gibt, wird abgebrochen. Ansonsten wird zu dieser Nummer gesprungen. Falls die Nummer der Kopf der Liste war, wird ein neuer Kopf zugewiesen und der Vorgänger auf NULL gesetzt. Ansonsten werden Vorgänger des Nachfolgers und Nachfolger des Vorgängers gesetzt. Danach wird der Nachfolger (ist zwingend das sonst der Rest der Liste gelöscht wird) auf NULL und der Vorgänger auf NULL gesetzt und der gewünschte Knoten wird gelöscht. Danach wird hScript.m_NumActions verringert.

Vertauschen zweier Knoten

Code:


//Vertauscht zwei Einträge

cout << "Vertausche Eintrag #: ";
cin >> h;

hSAction = hScript.m_Head;
if(hSAction == NULL) break;

for(i = 0; i < h; i++)
{
    hSAction = hSAction->m_n;
    if(hSAction == NULL) break;
}

cout << "Mit Eintrag #: ";
cin >> h;

hSActionPtr = hScript.m_Head;
if(hSActionPtr == NULL) break;

for(i = 0; i < h; i++)
{
    hSActionPtr = hSActionPtr->m_n;
    if(hSActionPtr == NULL) break;
}

hSActionChange = *hSAction;
hSAction = *hSActionPtr;
hSActionPtr = *hSActionChange;

//Damit m_NumEntries nicht von hSActionChange beim Beenden gelöscht wird
hSActionChange->m_NumEntries = NULL;



Zuerst werden beide Pointer auf den gewünschten Eintrag gesetzt. Falls die Einträge nicht vorhanden sind, wird abgebrochen. Danach werden die Werte vertauscht (da der =Operator überladen wurde, werden nicht die Adressen der Vorgänger bzw. Nachfolger vertauscht), indem ein Hilfselement verwendet wird.

Nun ist der Skripteditor fast fertig. Es fehlt nur noch ein Menü, wo die verschiedenen Aktionen ausgewählt werden können. Dieser Quelltext und auch der restliche Teil befindet sich im Beispiel "ScriptEditor".

Skripts in einem Programm ausführen

Ein Großteil der Arbeit ist jetzt erledigt. Das Ausführen von Skripts ist verglichen mit dem Rest ein Kinderspiel.

Für das Ausführen der Skripts werden wir eine Klasse schreiben. Ich nenn sie mal ScriptManager. Diese Klasse hat fürs erste 2 Funktionen die für das Ausführen des Skriptes verantwortlich sind, 1 Skriptfunktion die einen Text in die Konsole schreibt und eine Script-Variable die den aktuellen Skript speichert. Die Klasse hat nun folgenden Aufbau.

Code:


#ifndef _SCRIPTMANAGER_
#define _SCRIPTMANAGER_

#include "iostream.h"

#include "Script.h"

//Diese Aufzählung muss die selbe Reihenfolge wie das Actiontemplate haben,
//mit dem der Skript erstellt wurde
enum
{
    SCRIPT_PRINT = 0
};

class ScriptManager
{
public:

    bool ExecuteScript(char *Filename);
    Action* Process(Action* hAction);

    //Eigene Funktionen
    Action* Script_Print(Action *hAction);

private:

    Script CurrentScript;
};

#endif _SCRIPTMANAGER_



Wir nehmen an, dass das ActionTemplate genau eine Aktion hat, und zwar:

Print ~
TEXT

Diese Aktion soll einen Text in der Konsole ausgeben. Die Aufzählung scheint momentan sinnlos, aber später wird sie sich als nützlich erweisen.

Schauen wir uns jetzt die Funktionen an.

Code:



bool ScriptManager::ExecuteScript(char* Filename)
{
    Action* hAction;

    if(CurrentScript.LoadScript(Filename) == false) return false;

    hAction = CurrentScript.m_Head;

    while(hAction != NULL)
    {
        hAction = Process(hAction);
    }

    return true;
}

Action* ScriptManager:rocess(Action *hAction)
{
    switch(hAction->m_ActionType)
    {
        case SCRIPT_PRINT: return Script_Print(hAction);
    }

    return NULL;
}

Action* ScriptManager::Script_Print(Action *hAction)
{
    cout << hAction->m_Entries[0].Text << "\n";

    return hAction->m_n;
}



Die erste Funktion übernimmt den Dateinamen. Danach kommt eine Abfrage, ob der Skript erfolgreich geladen wurde. Der lokalen Variable hAction, die eine einzelne Aktion repräsentiert, wird auf den Beginn des Skriptes gesetzt. Solange hAction nicht Null ist wird Process aufgerufen.

Process übernimmt die Aktion, überprüft den Typ und ruft die entsprechende Funktion aus. In unserem Beispiel würde Script_Print aufgerufen werden. Script_Print gibt dann mit cout einen Text aus (der im ersten Aktionseintrag gespeichert wurde), und liefert die nächste Aktion als Rückgabewert. Sollte der Skript zu Ende sein, wird NULL zurückgeliefert, und die ExecuteScript-Funktion bricht ab.

Dieser Aufbau sieht vielleicht unnötig (kompliziert) aus, aber bei vielen Aktionen schafft das Übersichtlichkeit.

Diese kleine Beispiel zeigt, wie man Text ausgeben kann. Aber was ist, wenn man zB. if/else Strukturen verwenden will?

Sehen wir uns die vorherige Klasse nun mal erweitert an.

Code:


#ifndef _SCRIPTMANAGER_
#define _SCRIPTMANAGER_

#include "iostream.h"

#include "Script.h"

//Diese Aufzählung muss die selbe Reihenfolge wie das Actiontemplate haben,
//mit dem der Skript erstellt wurde
enum
{
    SCRIPT_END_PROGRAMM = 0,
    SCRIPT_END_SCRIPT,
    SCRIPT_IF_FLAG_THEN,
    SCRIPT_IF_VAR_THEN,
    SCRIPT_ELSE,
    SCRIPT_END_IF,
    SCRIPT_SET_FLAG,
    SCRIPT_SET_VAR,
    SCRIPT_LABEL,
    SCRIPT_GOTO,
    SCRIPT_PRINT
};

class ScriptManager
{
public:

    ScriptManager(void);

    void ZeroFlags(void);
    void ZeroVars(void);

    bool ExecuteScript(char *Filename);
    Action* Process(Action* hAction);

    //Standardfunktionen
    Action* Script_EndProgramm(Action *hAction);
    Action* Script_EndScript(Action *hAction);
    Action* Script_IfFlagThen(Action *hAction);
    Action* Script_IfVarThen(Action *hAction);
    Action* Script_Else(Action *hAction);
    Action* Script_EndIf(Action *hAction);
    Action* Script_SetFlag(Action *hAction);
    Action* Script_SetVar(Action *hAction);
    Action* Script_Label(Action *hAction);
    Action* Script_Goto(Action *hAction);

    //If/then-funktion
    Action* Script_IfThen(Action *hAction, bool Skipping);

    //Eigene Funktionen
    Action* Script_Print(Action *hAction);

private:

    Script CurrentScript;

    bool Flags[256];
    int Vars[256];
};

#endif _SCRIPTMANAGER_



Dieser Klasse wurden nun Funktionen hinzugefügt, mit der sich ein Skript "steuern" lassen. Mit den Variablen Flags und Vars lassen sich jetzt If/Else Strukturen verwirklichen.

Die wohl schwierigste Funktion werde ich nun erklären.

Code:



Action* ScriptManager::Script_IfThen(Action *hAction, bool Skipping)
{
    while(hAction != NULL)
    {
        //Falls ein Else vorkommt, wird Skipping getauscht
        if(hAction->m_ActionType == SCRIPT_ELSE)
            Skipping = (Skipping == true) ? false : true;

        //Bei EndIf If-Else-Prozedure verlassen
        if(hAction->m_ActionType == SCRIPT_END_IF)
            return hAction->m_n;

        //Überspringe Aktion
        if(Skipping == true) hAction = hAction->m_n;
        else
        {
            //Verlasse If-Else-Prozedure, falls ein Goto vorkommt
            if(hAction->m_ActionType == SCRIPT_GOTO)
                return Process(hAction);
        
            //Führe Aktion aus, bis Ende erreicht
            if((hAction = Process(hAction)) == NULL)
                return NULL;
        }
    }

    //Ende des Scripts erreicht
    return NULL;
}



Diese Funktion ist für das Überspringen, nicht gewünschter Aktionen zuständig. Skipping gibt an, ob der momentane Teil übersprungen werden soll. Falls ein SCRIPT_ELSE vorkommt, wird Skipping umgedreht. Bei einem SCRIPT_END_IF wird die Script_IfThen-Klasse abgebrochen. Danach wird Skipping überprüft. Bei true wird übersprungen und die nächste Aktion genommen und wieder von vorne angefangen. Ansonsten wird die Aktion ganz normal bearbeitet. Bei einem SCRIPT_GOTO wird außerdem die Script-IfThen-Klasse abgebrochen. Falls das Ende des Skriptes erreicht wird, wird auch abgebrochen.

Die anderen Funktionen sind leicht zu verstehen und befinden sich im Beispielordner "Executing Scripts".

Hier ist nun ein Skriptbeispiel, wie man den Spielablauf steuern kann:

If Flag 0 equals false
Print "Besiege Diablo"
If Flag 0 equals true
Print "Du hast es geschafft, jetzt besiege Baal"

Nun müsste man Diablo noch mit einem Skript koppeln, der Flag[0] auf true setzt, wenn er getötet wurde.

Set Flag 0 to true

Man kann dieser Klasse jederzeit neue Vars oder Flags hinzufügen. Zum Beispiel für Quests, Gespräche mit Dorfbewohnern oder Items. Diese halten das System übersichtlich(er). Für kleinere Projekte reichen aber diese beiden (Flags und Vars).

Es kann sein, dass man bei einem Spiel auch Klassen an den SkriptManager übergeben muss. Bei eigenen Funktionen sollte man nicht vergessen, eine Konstante in der Aufzählung hinzuzufügen und vor allem die Reihenfolge bei ActionTemplate und Aufzählung beibehalten, und als Rückgabewert immer die nächste Aktion liefern.

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

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