C4Script-Tutorial/4. Codesteuerung
Aus Clonk Wiki
Oft soll ein Teil des Codes nur dann ausgeführt werden, wenn eine Bedingung erfüllt ist. Auf diese Weise kann zum Beispiel der Zauberclonk die Ausführung eines Zaubers abbrechen, wenn nicht genügend Zauberenergie vorhanden ist. In anderen Fällen soll ein bestimmer Codeabschnitt mehrere Male wiederholt werden - so kann beispielsweise der Bausatz alle vorhandenen Baupläne auslesen und anzeigen.
Inhaltsverzeichnis |
Bedingungen
Unter einer Bedingung versteht man einen Codeteil, der nur ausgeführt wird, wenn die Bedingung erfüllt ist. Dies ist eigentlich ganz einfach, und sieht so aus:
if(Bedingung)
Anweisung;
Eingeleitet wird die Bedingung durch das Schlüsselwort if (if: engl. wenn), gefolgt von der Bedingung, die erfüllt sein muss. Ist sie das, wird Anweisung ausgeführt. Bedingung ist dabei ein boolescher Wert (und wie aus der Konvertierungstabelle aus dem letzten Kapitel hervorgeht, kann jeder Typ nach bool konvertiert werden). Die Bedingung gilt dabei als erfüllt, wenn der Wert true ist, und als nicht erfüllt, wenn er false ist. Als Beispiel werfe ich mal folgenden Code in den Raum:
if(FindObject(GOLD))
CreateObject(CNKT,
100,
100);
Zuerst wird mit FindObject nach einem Goldklumpen gesucht, dessen Adresse bei Erfolg zurückgegeben wird (0 andernfalls). Die Adresse wird in einen bool umgewandelt (die Adresse ist bei Erfolg ungleich 0, ergibt also true) und folglich wird der Bausatz (CNKT) an der Stelle 100/100 erstellt. Wird kein Goldklumpen gefunden, gibt FindObject 0 zurück, was bei der Umwandlung in bool false ergibt: Der Bausatz wird nicht erzeugt.
Probiere dieses Script am besten einmal mit und ohne Goldklumpen im Erdreich aus (Um sicher zu stellen, dass alles Gold weg ist, kannst du "RemoveAll(GOLD)" in der Konsole ausführen).
Es wird die unmittelbar nach if folgende Anweisung ausgeführt, wenn die Bedinung erfüllt ist. Darauffolgende Anweisungen werden unabhängig davon immer ausgeführt. Um mehrere Anweisungen nur dann auszuführen, wenn eine Bedingung erfüllt ist, können Blöcke genutzt werden:
- if(FindObject(GOLD))
- {
CreateObject(CNKT);
Message("Es gibt mindestens einen Goldklumpen!");- }
Übrigens, falls es dir noch nicht aufgefallen ist: Nach einem if darf kein Semikolon gesetzt werden. Als Anweisung gilt hier das if und die nachfolgende Anweisung und deshalb darf erst am Schluss ein Semikolon stehen. Würde man nach if bereits ein Semikolon setzen, so würde die Engine davon ausgehen, dass, wenn die Bedingung erfüllt ist, keine Anweisung auszuführen wäre, da die gesame Anweisung an diesem Punkt zu Ende ist.
Jede Bedingung kann optional noch einen else-Zweig (else: engl. sonst) erhalten, der nur dann ausgeführt wird, wenn die Bedingung nicht erfüllt ist:
- if(Bedingung)
Anweisung;- else
Andere_Anweisung;
Bedingungen lassen sich natürlich auch schachteln. Dabei ist aber zu beachten, dass sich ein else immer auf das if direkt davor bezieht:
- if(FindObject(GOLD))
if(FindObject(ROCK))
Log("Es gibt Goldklumpen und Steine!");- else
Log("Es gibt keine Goldklumpen.");
Probiere diesen Code mal ohne Gold im Erdreich aus. Wider Erwarten wird die Meldung "Es gibt keine Goldklumpen." nicht geloggt. Warum? Ganz einfach: Das else bezieht sich nicht auf das oberste if, sondern auf das zweite. Ein else bezieht sich immer auf das direkt vorangehende if, soweit dieses nicht bereits durch ein else "benutzt" wird.
Um den Missstand zu umgehen, kann folgendes gemacht werden:
- if(FindObject(GOLD))
if(FindObject(ROCK))
Log("Es gibt Goldklumpen und Steine!");
else;- else
Log("Es gibt keine Goldklumpen.");
Das erste else bezieht sich nun auf das if direkt darüber, also das zweite. In diesem Fall wird nun keine Aktion vorgenommen (bzw. eine leere Anweisung ausgeführt). Man könnte hier auch eine Meldung à la "Es gibt zwar Gold, aber keine Steine" ausgeben. Interessant ist nun, dass sich das zweite else auf das erste if bezieht (wie es auch sein soll) und der Code daher nun auch so funktioniert wie er soll.
Diese Lösung ist trotzdem nur bedingt zufriedenstellend. Bei einer größeren Anzahl von if wäre es umständlich für jedes if noch ein leeres else einzubauen, nur damit man ein else für das erste if verwenden kann. Eine elegantere Lösung sähe so aus:
- if(FindObject(GOLD))
- {
if(FindObject(ROCK))
Log("Es gibt Goldklumpen und Steine!");- }
- else
Log("Es gibt keine Goldklumpen.");
Durch den Block wird nun eindeutig, worauf sich das else bezieht.
Vergleiche
Oft müssen als Bedingung zwei Werte miteinander verglichen werden. Hierzu gibt es die folgenden Vergleichsoperatoren, die entweder true oder false ergeben:
| Operator | Name | Beschreibung |
|---|---|---|
| == | Gleich | Testet auf Gleichheit. Ergibt true, wenn die beiden Werte gleich sind. Nicht zu verwechseln mit dem Zuweisungsoperator (=). Funktioniert nicht für Strings! |
| != | Ungleich | Testet auf Ungleichheit. Ergibt true, wenn die beiden Werte nicht gleich sind. |
| < | Kleiner | Ergibt true, wenn der erste Wert kleiner als der zweite ist. Funktioniert nur mit Werten vom Typ int. |
| <= | Kleiner-Gleich | Ergibt true, wenn der erste Wert kleiner oder gleich dem zweiten ist. Funktioniert ebenfalls nur mit int. |
| > | Größer | Ergibt true, wenn der erste Wert größer als der zweite ist. Auch das funktioniert natürlich nur mit ints. |
| >= | Größer-Gleich | Ergibt true, wenn der erste Wert größer oder gleich dem zweiten ist. Auch hier gilt: Nur auf ints inwendbar. |
| eq | String-Gleich | Veraltet! Bei #strict (1): Gibt true zurück, wenn zwei Strings (Typ string) gleich sind. |
| ne | String-Ungleich | Veraltet! Bei #strict (1): != für Strings. |
Wie die Operatoren funktionieren, sollte nun klar sein. Einige Beispiele sollen zur Verdeutlichung jedoch noch folgen:
- #strict 2
- public
func
Add(val1,
val2) - {
return
val1
+
val2;- }
- protected
func
Initialize() - {
var
i
=
7,
j
=
3,
k
=
CLNK,
h;
if(i
==
j)
Log("i ist gleich j.");
if(i
!=
4)
Log("i ist nicht 4.");
if((h
=
Add(i
+
1,
j))
>
(i
*
2))
Log("i+1+j ist größer als i*2.");
if(j
-
5
<=
h)
Log("j - 5 ist kleiner oder gleich h.");
if(k
!=
CNKT)
Log("In k ist nicht die ID des Bausatzes gespeichert.");
if(GetAction(k
=
FindObject(k))
==
"Walk")
Jump(k);- }
Auch hier sei gesagt, dass dies kein sinnvoller Code darstellt, sondern lediglich eine Spielerei ist, um Vergleichsoperatoren zu demonstrieren.
Das Ganze sieht auf den ersten Blick vielleicht etwas kompliziert aus, doch keine Angst, das ist es nicht. Gehen wir Schritt für Schritt durch den Code:
Zuerst wird die uns bereits bekannte Add-Funktion implementiert. Diese werden wir in einer der Vergleiche benötigen. In Initialize dann werden die einzelnen Vergleiche ausgeführt:
- Zeile 11
- Hier wird geprüft, ob der Wert der Variablen i gleich dem Wert in j ist. Da in i der Wert 7 und in j der Wert 3 gespeichert ist, ist dies nicht der Fall, der ==-Operator gibt also false zurück und die Meldung wird nicht ausgegeben.
- Zeile 12
- Anschließend wird überprüft, ob i nicht gleich 4 ist. Da i 7 ist, ist dies korrekt, der !=-Operator gibt also TRUE zurück und die Meldung wird ausgegeben.
- Zeile 13
- In der nächsten Zeile wird es etwas komplizierter: Zuerst wird die Funktion Add aufgerufen, mit den beiden Parametern i + 1 und j. Dazu wird zuerst i + 1 ausgewertet, was 8 gibt, und dann an Add als Parameter übergeben.
- Add addiert 8 und 3 (j) dann zusammen, und gibt das Ergebnis zurück. Diese 11 wird nun der Variablen h zugewiesen und gleichzeitig vom Zuweisungsoperator (=) zurückgegeben. Nun wird überprüft, ob diese 11 größer ist als die Anweisung (i * 2). i * 2 ergibt 14.
- Da 11 kleiner als 14 ist, liefert der >-Operator false zurück und die Meldung wird nicht ausgegeben. Zu beachten ist, dass in h nun weiterhin der Wert 11 gespeichert ist.
- Zeile 14
- Die nächste Anweisung prüft nun, ob j - 5 kleiner oder gleich der in h gespeicherten 11 ist. j ist nach wie vor 3, 3 - 5 ergibt -2 und ist somit kleiner oder gleich 11 - die Meldung wird ausgegeben.
- Zeile 15
- Die nächste Zeile demonstriert einen Vergleich zweier Werte vom Typ id: In k ist die id CLNK gespeichert. Es wird geprüft, ob diese nicht gleich der id CNKT (Bausatz) ist. Dies ist natürlich der Fall, daher wird die Meldung ausgegeben.
- Zeile 16
- Zuerst wird ein Objekt vom Typ k gesucht - k ist CLNK, es wird also ein Clonk gesucht. Das Ergebnis (also entweder die object auf einen gefundenen Clonk oder 0, falls es keinen gibt) wird in k gespeichert - damit hat die Variable ihren Typ von id in object gewechselt. Von diesem gefundenen Clonk wird die Action abgefragt und mit "Walk" verglichen. Ist dies der Fall, so wird mithilfe der Funktion Jump der nun in k gespeicherte Clonk zum Springen veranlasst.
- Mit dieser Methode kann natürlich nur ein Clonk gefunden werden. Will man alle Clonks auf der Karte durchsuchen und eventuell springen lassen, so muss dies mit Schleifen, die wir wenig später kennenlernen werden, geschehen.
Logische Operatoren
Mit logischen Operatoren kann man Bedingungen verknüpfen. In C4Script gibt es folgende logische Operatoren:
| Operator | Name | Ausdruck | Beschreibung |
|---|---|---|---|
| ! | Not | !Ausdruck | Ergibt true, wenn der Audruck false ist false, wenn der Ausdruck true ist, negiert den Ausdruck also. |
| && | And | Ausdruck1 && Ausdruck2 | Ergibt true, wenn Ausdruck1 und Ausdruck2 beide true sind. |
| || | Or | Ausdruck1 || Ausdruck2 | Ergibt true, wenn Ausdruck1 oder Ausdruck2 true sind (oder beide). |
Beim logischen Not ist die Operatorpriorität zu beachten, wie folgendes Beispiel zeigt:
if(!
4
==
5)
Anweisung();In diesem Fall wird die Anweisung nicht ausgeführt, auch wenn dies auf den ersten Blick nicht danach aussieht. Logische Operatoren haben nämlich eine höhere Priorität als Vergleichsoperatoren. Es wird also zuerst !4 ausgewertet (4 ist ungleich 0, also true, der !-Operator macht daraus false) und anschließend == (dabei wird false zurück in einen int konvertiert, was 0 ergibt), und das dann mit der 5 verglichen. Der gesamte Ausdruck ergibt also false.
Durch Klammersetzung kann das Problem umgangen werden:
if(!(4
==
5))
Anweisung();Nun wird zuerst die 4 mit der 5 verglichen (was false ergibt) und anschließend negiert (woraus dann true ensteht). Dadurch wird die Anweisung also ausgeführt.
Hinweis: Man hätte natürlich auch gleich den !=-Operator verwenden können, der auf Ungleichheit prüft. Dadurch können unnötige Klammern vermieden werden.
Schleifen
while
Eine Schleife sorgt dafür, dass ein bestimmter Code mehrmals durchlaufen wird. Die while-Schleife ist sehr ähnlich zu if, mit dem einzigen Unterschied, dass die Anweisung solange ausgeführt wird, bis die Bedingung nicht mehr erfüllt ist. Bleibt die Bedingung immer erfüllt, so tritt die Programmausführung nie aus der Schleife aus und die Engine bleibt stehen. Eine solche Schleife nennt man Endlosschleife.
Mit Schleifen kann man z.B. mehrere Objekte auf einmal erzeugen. Wenn man 20 Objekte vom Typ GOLD an 50/50 erstellen will, kann man natürlich einfach 20 Mal CreateObject() in den Code schreiben. Viel eleganter ist das aber mit einer Schleife:
#strict 2
protected
func
Initialize()
{
var
counter
=
0;
while(counter
<
20)
{
CreateObject(GOLD,
50,
50,
-1);
++
counter;
}
}
Zuerst wird eine Variable namens counter angelegt. Das ist kleiner als 20, also wird der sogenannte Rumpf der Schleife ausgeführt. Ein Goldklumpen wird erzeugt und die Variable mit Hilfe des ++-Operators um 1 erhöht. Es hätte denselben Effekt, wenn man counter += 1 geschrieben hätte.
Anschließend beträgt der Wert der Variablen 1. Da das immernoch kleiner als 20 ist, wird der Schleifenrumpf erneut ausgeführt. Dies geht solange, bis die Variable counter den Wert 20 erreicht hat. Dann ist die Bedingung in der while-Schleife nicht mehr erfüllt und die Codeausführung geht nach der Schleife weiter.
Es ist nun vielleicht etwas öde, dass man mit der hier vorgestellten Methode nur 20 Goldklumpen an der Position 50/50 erstellen kann. Vielleicht würde man sie ja lieber in 20er-Schritten von der Position 50/50 nach rechts verteilen, sodass sie also nicht alle am gleichen Fleck herunterfallen. Auch das ist mit Schleifen einfach machbar:
#strict
protected
func
Initialize()
{
var
counter
=
0;
while(counter
<
20)
{
CreateObject(GOLD,
50
+
20
*
counter,
50,
-1);
++
counter;
}
}
Der einzige Unterschied zum vorigen Code besteht in Zeile 8. Als X-Koordinate wird nun nicht mehr fest 50 angegeben, sondern der Ausdruck 50 + 20 * counter. Was hat es damit auf sich?
Ganz einfach: Beim ersten Schleifendurchlauf ist der Wert von counter noch 0, der Audruck ergibt also 50 (20 * 0 ergibt 0, 50 + 0 ergibt 50). Man beachte hier wieder die Operatorenpriorität von * gegenüber +. Beim zweiten Durchlauf dann ist der Wert von counter 1, der zweite Goldklumpen wird also an der X-Position 70 erstellt(50 + 20 * 1). Dies geht wieder solange, bis counter 20 ist (20 selbst gehört jedoch nicht mehr dazu, denn 20 ist nicht kleiner als 20).
In diesem Beispiel haben wir die Schleife hauptsächlich als Zählschleife verwendet. Für diese Art der Schleife ist jedoch die for-Schleife gedacht, die wir auch gleich besprechen werden. Die while-Schleife sollte nur dann benutzt werden, wenn sich die for-Schleife nicht anbietet, da diese komfortabler und vor allem lesbarer ist.
Ein Beispiel für einen solchen Fall stellt das Durchsuchen aller Objekte im Spiel dar. Folgender Script verdoppelt alle Objekte beim Spielstart:
- #strict
- protected
func
Initialize() - {
var
obj;
while(obj
=
FindObject(0,
0,0,0,0,
0,
0,0,
0,
obj)
)
CreateObject(
GetID(obj),
GetX(obj),
GetY(obj),
GetOwner(obj)
);- }
Mithilfe von FindObject werden alle Objekte im Spiel durchsucht. Gibt man 0 als ersten Parameter für FindObject, so werden alle Objekte unabhängig von derer ID gefunden. Interessant ist aber der letzte Parameter: Dieser Parameter bestimmt, hinter welchem Objekt nicht mehr gesucht wird. Anfangs ist der Paramter 0, es werden also absolut alle Objekte gefunden. Gleichzeitig wird die Variable obj aber auf das gefundene Objekt gesetzt. Im nächsten Schleifendurchgang wird als letzter Parameter dann das eben gefundene Objekt angegeben - dies bewirkt, dass dieses Objekt und alle zuvor gefundenen nun nichtmehr gefunden werden - würde man das nicht machen, würde man immer dasselbe Objekt finden und hätte eine Endlosschleife.
Im Schleifenrumpf wird einfach ein neues Objekt mit der ID vom gefundenen Objekt, an der Position vom gefundenen Objekt und mit dem gleichen Besitzer des gefunden Objekts erzeugt. Da man natürlich nicht alle derartigen Funktionen auswendig lernen kann, sind diese alle in der Entwicklermodus-Dokumentation aufgelistet. Wie man damit umgeht, lernen wir jedoch in einem späteren Kapitel. Jetzt gehts erstmal zur...
for-Schleife
Wie bereits gesehen, wird sehr oft eine Schleife benötigt, die z.B. einen bestimmten Code mehrfach ausführt. Nehmen wir wieder das Beispiel von oben. Mit einer for-Schleife sieht obiger Code folgendermaßen aus:
#strict 2
protected
func
Initialize()
{
for(var
counter
=
0;
counter
<
20;
++
counter)
CreateObject(GOLD,
50
+
20
*
counter,
50,
-1);
}
Was hat sich geändert? Die drei Anweisungen "var counter = 0", "counter < 20" und "++ counter" stehen jetzt alle als eine Art Parameter in der for-Schleife. Das ist insofern praktisch, als dass man jetzt auf einen Blick sieht, dass die Schleife mit 0 beginnt, bei 20 aufhört und die Zählvariable bei jeder Ausführung eins hoch setzt.
Speziell steht jetzt die Anweisung "++ counter" nicht mehr am Ende der Schleife, wo man sie ggf. vergessen würde. Würde man diese Anweisung beim Scripten vergessen, würde counter nie geändert, counter < 20 wäre immer erfüllt und die Schleife würde somit nie enden. Man hätte eine Endlosschleife konstruiert.
Schauen wir uns die for-Konstruktion nochmal im Detail an. Zuerst fällt auf, dass for sich von den meisten Funktionen dadurch unterscheidet, dass die "Parameter" nicht mit einem Komma (","), sondern mit Semikolon (";") getrennt werden. Das liegt daran, dass es sich ja in Wirklichkeit nicht um Parameter handelt, die an for übergeben werden, sondern um Code, der von der for-Schleife an bestimmten Stellen ausgeführt wird.
Es bleibt also zu klären, was die drei Anweisungen jeweils machen.
- Die erste Anweisung ist eine Initialisierung. Dies soll zuallererst ausgeführt werden, bevor irgendwas in der Schleife gemacht wird. Hier kann man, wie im Beispiel, z.B. der Zählervariable ihren ersten Wert zuweisen.
- Die zweite Anweisung ist die Schleifenbedingung. Die Schleife wird, wie bei der while-Schleife, so lange ausgeführt, bis diese Bedigung false ergibt. Diese Bedingung wird auch bei der ersten Ausführung überprüft, wenn die Bedingung also für den Anfangswert nicht erfüllt ist, wird die Schleife sofort abgebrochen!
- Die dritte Anweisung ist die Inkrementation (lat. zunehmen, hochzählen). Dies wird jedes Mal ausgeführt, nachdem die auf for folgenden Anweisungen ausgeführt wurden. Hier ist der Platz, um z.B. die Zählervariable hochzuzählen.
Diese Definition der for-Schleife ist sehr allgemein und erlaubt eine Menge Variationen. Einige Beispiele:
Was machen die einzelnen Schleifen? Gehen wir sie mal einzeln durch:
- Zeile 6-7
- Der einzige Unterschied zum vorher gehenden Beispiel ist, dass jetzt "counter += 2" statt "counter ++" zum Hochzählen verwendet wird. Wie bereits erwähnt zählt der +=-Operator die Variable um den Wert hoch, der danach kommt - in diesem Fall also 2. Also zählt diese Schleife in 2er-Schritten von 0 bis 18 (18 ist die letzte Zahl kleiner 20, die die Bedingung erfüllt).
- Zeile 8-9
- Hier haben sich einige Dinge geändert. Der Anfangswert ist 10, abgebrochen wird nicht mehr wenn counter einen Maximalwert erreicht, sondern wenn er unter 0 fällt. Außerdem wird jetzt counter nicht mehr hoch- sondern runtergezählt. Diese Schleife beginnt also mit 10 und zählt runter, bis sie bei 0 angekommen ist.
- Zeile 10-11
- Diese Schleife ist relativ untypisch, da es sich nicht mehr um eine Zählschleife im eigentlichen Sinne handelt. Statt die Variable hochzuzählen, wird sie nach jedem Durchlauf mit 2 multipliziert. Das geschieht so lange, bis counter größer wird als 100. Hier wird also mit 1, 2, 4, 8, 16, 32, 64 die 2-er Potenzreihe ausgegeben.
Eine weitere nicht "offensichtliche" Anwendung der for-Schleife betrifft spezielle Funktionen wie Contents:
- #strict 2
- protected
func
Initialize() - {
var
clonk
=
FindObject(CLNK);
var
obj;
for(var
i
=
0;
obj
=
Contents(i,
clonk);
i++)
Log("%s trägt einen %s!",
GetName(clonk),
GetName(obj));- }
Was passiert hier? Die Anweisung "obj = Contents(i, obj)" erscheint reichlich falsch als Ausführbedingung, da sie ja schließlich auf den ersten Blick vor allem obj einen neuen Wert zuweist. Was hat das damit zu tun, ob die Schleife fortgeführt wird?
Tatsächlich ist diese Verwendung der for-Schleife ein wenig seltsam, aber nicht ungeschickt. Gehen wir den Code also Schritt für Schritt durch:
- Zeile 5
- Es wird mit der bekannten FindObject-Funktion ein beliebiger Clonk rausgesucht. Es wird an dieser Stelle davon ausgegangen, dass ein solcher existiert, clonk also einen sinnvollen Objektverweis erhält. Außerdem wird eine Variable mit dem Namen obj erzeugt, die wir später brauchen werden.
- Zeile 6
- Zuerst mal die bekannten Sachen: Die Anweisung "var i = 0" erzeugt wieder eine Zählervariable. Solche kurzen Namen wie "i" oder "j" sind übrigens üblich, wenn es um kurze Zählschleifen geht. Genauso vertraut ist die letzte Anweisung: "i++". Die Zählvariable wird, wie gehabt, bei jedem Schleifendurchlauf eins hochgezählt. Die Schleife beginnt also mit 0 und zählt solange hoch, bis die Bedingung nicht mehr erfüllt ist. Was ist aber die Bedingung?
- Die Engine-Funktion Contents sucht ein Inhaltsobjekt aus dem Clonk anhand seiner Nummer heraus (wie üblich: das erste Objekt im Clonk hat die Nummer 0). Das heißt, hier wird der Variable obj ein Verweis auf das i-te Objekt im Clonk zugewiesen. Existiert kein solches Objekt, so wird in obj dagegen 0 gespeichert.
- Wo ist jetzt also die Bedingung? Wie du dich vielleicht erinnerst, gibt der Zuweisungsoperator "=" den Wert, den er zugewiesen hat, zurück - in diesem Fall also den Rückgabewert von Contents. Da es sich dabei um einen Objektverweis, also Typ object handelt, findet automatisch eine Konvertierung nach bool statt. Existiert ein Objekt mit der Nummer i im Clonk, ergibt das true (da der Objektverweis gültig ist), ansonsten false (da 0 zurück gegeben wird).
- Zusammengefasst: Die Schleife läuft so lange, bis kein Objekt mit der Nummer i existiert. Da i jedes Mal hochgezählt wird, geht die Schleife somit alle Inhaltsobjekte vom Clonk durch und bricht dann ab. Das letzte gefundene Objekt befindet sich immer in obj. Das funktioniert deshalb, weil die Bedingung immer ausgeführt wird, bevor die Anweisungen nach for zur Ausführung kommen.
- Zeile 7
- Hier wird die bereits bekannte Funktion Log zusammen mit der bisher unbekannten Funktion GetName dazu verwendet, den Inhalt des Clonks ins Log zu schreiben. GetName bestimmt zu einem Objekt seinen Namen, d.h. Objektdefinitionsname für "normale" Objekte bzw. Clonkname für Clonks, die von einem Spieler gesteuert werden.
Heißt der Clonk also "Twonky" und trägt einen Stein und einen Bausatz (was er normalerweise nicht kann, aber darüber setzen wir uns einfach mal hinweg), so wird die folgende Logausgabe erzeugt:
Twonky trägt einen Stein! Twonky trägt einen Bausatz!
Schleifenkontrolle
Nicht in allen Fällen reicht die Schleifenkontrolle durch die Fortführbedingung aus. Im folgenden Beispiel soll zum Beispiel der Clonk mit dem Namen "Twonky" gesucht werden:
- var
obj,
found; - while(obj
=
FindObject(CLNK,0,0,0,0,0,0,0,0,obj))
if(obj->GetName()
eq
"Twonky")
found
=
obj;
Die Schleife funktioniert ähnlich wie eine bereits oben beschriebene Schleife: Sie nutzt FindObject, um alle Objekte im Spiel mit der ID CLNK (also alle Clonks) durchzugehen. Sie prüft dann für jedes so gefundene Objekt, ob es den Namen "Twonky" hat. Ist dies der Fall, wird der Objektverweis in der Variable found gespeichert.
Allerdings ist dieses Stück Code nicht ganz zufriedenstellend. Die Schleife geht in jedem Fall alle Clonks im Spiel durch - selbst nachdem Twonky bereits gefunden wurde. Nachdem found gesetzt wurde, gibt es eigentlich keinen Grund, warum man die Schleife fortführen sollte. Aber wie bricht man die Schleife ab?
Für diesen Fall gibt es einen speziellen Befehl in C4Script: break
Wird break ausgeführt, so wird die Schleife, in der sich die Ausführung gerade befindet, sofort abgebrochen. Mithilfe dieses Befehls kann man die Suchschleife schöner schreiben:
- var
obj; - while(obj
=
FindObject(CLNK,0,0,0,0,0,0,0,0,obj))
if(obj->GetName()
eq
"Twonky")
break;
Die Schleife bricht sofort ab, falls Twonky gefunden wird. Hier wird auch keine zweite Variable für das gefundene Objekt mehr benötigt, da nach der Schleife obj natürlich immer noch denselben Wert hat wie bei Schleifenabbruch - als das gesuchte Objekt.
Außer break gibt es noch einen weiteren Befehl, um Schleifen direkt zu konstrollieren: continue.
Dieser Befehl bricht, wie der Name schon sagt, die Schleife nicht ab, sondern setzt sie sofort fort. Beachte dabei "sofort" - falls die Schleife einen Block enthält, werden nachfolgende Befehle nicht abgearbeitet!
Angenommen also "Twonky" hätte einen Zwillingsbruder, der zu allem Überfluss noch genauso heißt. Dabei befindet sich unser Twonky in einem Dorf auf der linken Seite der Landschaft, wogegen der andere am rechten Rand wohnt.
Entsprechend sollten alle Objekte am rechten Rand nicht geprüft werden. Der Code dazu sieht zum Beispiel folgendermaßen aus:
- var
obj; - while(obj
=
FindObject(CLNK,0,0,0,0,0,0,0,0,obj))
{
if(obj->GetX()
>
LandscapeWidth()
/
2)
continue;
if(obj->GetName()
eq
"Twonky")
break;- }
Mit GetX und LandscapeWidth wird geprüft, ob sich der Clonk in der linken oder der rechten Landschaftshälfte befindet. Befindet er sich in der rechten, so wird continue ausgeführt, was dazu führt, dass Zeile 5 und 6 übersprungen und die Schleife einfach fortgeführt wird. Damit wird, falls der rechte Twonky gefunden wird, die Schleife nicht abgebrochen.
Rekursion
Unter Rekursion versteht man das Aufrufen einer Funktion von sich selbst heraus. Das wird in C4Script zwar nur seltenst benötigt, soll hier aber der Vollständigkeit halber erwähnt werden. Dazu sei zunächst folgendes Codebeispiel gegeben:
- public
func
ChainMessage() - {
var
obj;
while(obj
=
FindObject(GetID(),
-100,
-100,
200,
200,
0,0,0,0,
obj)
)
obj->ChainMessage();
Message("Hallo, Welt!",
this()
);- }
Diese Funktion namens ChainMessage geht alle sich in der Nähe befindlichen Objekte durch und ruft in ihnen wiederrum die Funktion ChainMessage auf. Daraufhin wird eine Nachricht über dem Objekt ausgegeben.
Das hat zur Folge, dass, wenn viele dieser Objekte nebeneinander liegen, und auf das erste in der Kette ChainMessage aufgerufen wird, über allen anderen Objekten die Nachricht ebenfalls erscheint. Die Nachricht wird sozusagen über die einzelnen Objekte und deren ChainMessage-Funktion übertragen.
Der obige Codeteil hat jedoch ein Problem: Wenn man das so ausprobiert und es zwei Objekte in Reichweite gibt, stürzt die Engine ab. Warum das? Ganz einfach: ChainMessage prüft nicht, ob das Objekt, welches es gefunden hat, nicht bereits selbst ChainMessage ausführt. Die beiden Objekte spielen also Ping-Pong mit dem ChainMessage-Aufruf, bis es der Engine zuviel wird (genau genommen passiert ein Stack Overflow: Es werden zuviele Funktionen verschachtelt aufgerufen, sodass der für Funktionsaufrufe reserverierte Speicherplatz (der Stack) ausgeht).
Und damit wären wir bei einer wichtigen Bedingung fuer die Rekursion: Wie die anderen Schleifenarten, muss auch hier für einen Abbruch gesorgt werden, sonst würde sie theoretisch ewig laufen (und praktisch durch das Abstürzen der Engine terminiert werden). In obigem Beispiel könnte man eine lokale Variable vor dem Durchlaufen der Suchschleife auf true setzen, und hinterher wieder auf false. Die Rekursion wird dann nur ausgeführt, wenn diese Variable beim Zielobjekt noch auf false steht. Versuche dir genau klar zu machen, warum damit die Rekursion rechtzeitig abgebrochen wird!
- local
recur_running; - public
func
ChainMessage() - {
// Bereits für dieses Objekt aufgerufen?
if(recur_running)
return();
recur_running
=
true;
// Für alle restlichen Objekte aufrufen
var
obj;
while(obj
=
FindObject(GetID(),
-100,
-100,
200,
200,
0,0,0,0,
obj)
)
obj->ChainMessage();
// Variable zurücksetzen, Nachricht ausgeben
recur_running
=
false;
Message("Hallo, Welt!",
this()
);- }
Die Funktion RecurRunning wird hier verwendet, damit das Objekt mitteilen kann, ob es sich gerade in der Rekursion befindet. Man könnte auch direkt auf die lokale Variable des anderen Objektes zugreifen, doch das ist keine schöne Loesung, da man damit eine eigentlich private Variable berührt, die das Objekt sonst nirgends von sich aus frei zugänglich macht. Durch den Zugriff über eine Funktion tut es das und lässt den Zugriff darauf explizit zu, und verhindert dadurch nebenbei auch, dass jemand (versehentlich oder nicht) die Variable ändert.
Hinweis: Auch die Lösung mit einer lokalen Variable ist hier sicherlich nicht die beste Lösung für das Problem (Schließlich wird sie nicht über das ganze Objekt hinweg genutzt, sondern (fast) nur in ChainMessage), im Rahmen des Tutorials sei sie aber mal hinreichend zufriedenstellend.