Eine Funktion besteht aus einer Liste von Anweisungen für die Engine. Wird die Funktion ausgeführt, werden die Anweisungen eine nach der anderen ausgeführt. Eine solche Aweisung kann das Zuweisen eines Wertes an eine Variable sein oder eine andere Anwender-definierte Funktion bzw. bzw. auch eine interne Engine-Funktion aufrufen. Eine Funktion wird folgendermaßen definiert:
Dies erstellt eine Funktion mit dem angegebenen Namen (Name). Um ihr Information zu übermitteln, lassen sich optionale Parameter - wie etwa Variablen, Vektoren, numerische Ausdrücke oder Pointer - beim Aufrufen der Funktion übergeben. Die Funktion greift auf die Parameter unter dem in Klammern angegebenen Namen wie auf eine normale Variable zu. Wie bei allen Variablen muss darauf geachtet werden, keine reservierten Namen, wie x, y, z, my, you etc., für die Parameter zu verwenden. Die Funktionen werden zwischen die geschweiften Klammern geschrieben. Die Definition einer einfachen Funktion ohne Parameter sieht so aus:
function beep3() { beep(); beep(); beep(); } ... beep3(); // beeps 3 times
Diese Funktion wird immer dann ausgeführt, wenn die Engine einer Zeile begegnet, die beep3() enthält. Das nächste Beispiel ist eine funktion die ein var als Parameter nimmt:
function beeps(times) { while (times > 0) { beep(); times -= 1; wait(1); } } ... beeps(7); // beeps seven times
Sollte außerhalb der Funktion eine Variable oder ein anderes Objekt mit demselben Namen existieren, beziehen sich die Anweisungen der Funktion auf den 'lokalen' Parameter und nicht auf die 'globale' Variable. Die Funktion kann, wie im Beispiel 'times', jedweden Parameter ändern. Der Originalparameter in der aufrufenden Funktion bleibt aber unverändert. Die Parameter können weggelassen werden, in dem Fall bleibt der Platz innerhalb der Klammern leer.
LC In lite-C kann eine Funktion eine beliebige Anzahl an Parametern eines jeden Typs haben. Handelt es sich bei dem Typen nicht um ein var, muß er, wie in "function beeps (int times,float loudness)", vor dem Namen des Parameters angegeben werden. In C-Skript kann eine Funktion jedoch nur bis zu 4 Parameter beinhalten und dabei muß es ich entweder um ein var, oder einen var-Pointer handeln. Da ein var auch Handles oder Objekt-Pointer speichern kann, läßt sich auch ein Handle oder ein Pointer an ein beliebiges Objekt einer Skript-Funktion übergeben. Dann müssen Sie es lediglich einem 'wirklichen' Pointer zuweisen ehe direkt auf seine Parameter zugreifen können.
// this function takes a var set to an entity pointer as argument. function ent_init(ent) { my = ent; // necessary, because my.bright = on works, but ent.bright = on won't work! my.bright = on; my.abient = 100; ent_animate(ent,"walk",50,0); // it does not matter whether you pass my or ent here } ... ent_init(you); ...
// this function takes an entity pointer as argument. function ent_init(ENTITY* ent) { set(ent,BRIGHT); ent.ambient(100); ent_animate(ent,"walk",50,0); } ... ent_init(you); ...
Funktionen akzeptieren auch Pointer auf Arrays oder Structs als Parameter. Um anzuzeigen, dass es sich bei einem Parameter um einen ein var-Pointer und nicht um eine einzelne Zahl handelt, schreiben wir In C-Skript ein "&" vor den Namen des Parameters. In Lite-C kennzeichnen wir dies hingegen dadurch, dass wir jeden anderen nicht-var-Parameternamen mit seinem var* Typus einleiten. Wird ein Array-Pointer an eine Funktion übergeben, kann auf seine Elemente wie üblich durch [..] -Indices zugegriffen werden:
function vector_add2(&sum,&v1,&v2) { // calculate the sum of two vectors sum[0] = v1[0] + v2[0]; sum[1] = v1[1] + v2[1]; sum[2] = v1[2] + v2[2]; } var vector1[3] = 1,2,3; var vector2[3] = 4,5,6; var vector3[3]; ... vector_add2(vector3,vector1,vector2); // vector3 now contains 5,7,9
function vector_add2(var* sum,var* v1,var* v2) { // calculate the sum of two vectors sum[0] = v1[0] + v2[0]; sum[1] = v1[1] + v2[1]; sum[2] = v1[2] + v2[2]; } var vector1[3] = { 1,2,3 }; var vector2[3] = { 4,5,6 }; var vector3[3]; ... vector_add2(vector3,vector1,vector2); // vector3 now contains 5,7,9
An Funktionen, die var-Pointer erwarten, lassen sich auch Struct-Parameter übergeben. Das ist oftmals zum Übergeben von Entity- oder View-Positionen, von Winkeln oder Skills an eine Funktion nützlich:
function vector_direction(&result,&from,&to) { // calculate the direction angles between two positions result[0] = to[0] - from[0]; result[1] = to[1] - from[1]; result[2] = to[2] - from[2]; vec_to_angle(result,result); } var direction[3]; ... vector_direction(direction,my.x,your.x); // calculates the angle pointing from my to you vector_direction(direction,camera.x,nullvector); // calculates the angle from the camera to the level origin
function vector_direction(var* result, var* from, var* to) { // calculate the direction angles between two positions result[0] = to[0] - from[0]; result[1] = to[1] - from[1]; result[2] = to[2] - from[2]; vec_to_angle(result,result); } var direction[3]; ... vector_direction(direction,my.x,your.x); // calculates the angle pointing from my to you vector_direction(direction,camera.x,nullvector); // calculates the angle from the camera to the level origin
Sie können sowohl eine Variable, als auch einen Variablen-Parameter an eine Funktion übergeben oder auch einen Variablen-Pointer. Allerdings gibt es einen feinen Unterschied. Wenn die Funktion die Variable verändert, operiert sie im ersten Fall lediglich mit einer Kopie des Variablen-Inhalts, d.h., die Veränderung geschieht lediglich intern, im Rahmen der Funktion. Die übergebene Variable selbst bleibt unverändert. Wird aber ein Variablen-Pointer übergeben, operiert die Funktion direkt mit dem Inhalt und daher wird die Variable 'global' modifiziert.
Der momentate Zustand einer Funktion, ihre lokalen Variablen und die vordefinierten my- und you-Pointer werden während eines wait()-Aufrufs konserviert und mit der game_save function gespeichert. Dadurch wird sichergestellt, dass, wenn Sie ein vorher gespeichertes Game laden, sämtliche Funktionen auch wieder an dem Punkt weiterlaufen, an dem sie beim Speichern waren. Naja, meistens. Im Gegensatz zu my und you werden Pointer nicht gespeichert und daher nach Laden eines Spiels innerhalb von Funktionen ungültig. Das kann aber nur während wait() time geschehen und so sollten Sie sich der Tatsache bewußt sein, daß Ihre Pointer beim Laden eines Spiels nach wait() ungültig werden können. Brauchen Sie Pointer, die in Ihrer Funktion während wait() time gültig bleiben, konvertieren Sie diese vor wait() in ein Handle und danach zurück in einen Pointer.
Warte-Funktionen werden in einer Scheduler-Liste gespeichert und in der Reihenfolge ihrer letzten wait()-Ausführung aufgerufen. Durch die Variable proc_mode (lite-C) bzw. die Funktion proc_late (C-Skript) lässt sich diese Reihenfolge verändern.
function beep_sum(beep1,beep2); // prototype of function beep_sum function use_beep_sum() { beep_sum(3,4); // beep_sum is used here, and needs the prototype defined before } function beep_sum(beep1,beep2) // this is the real function { beep1 += beep2; while (beep1 > 0) { beep(); beep1 -= 1; wait(1); } }
LC In Lite-C kann ein Prototyp auch als Funktionspointer verwendet werden. Beispiel:
long MessageBox(HWND,char *,char *,long); // prototype of function MessageBox ... MessageBox = DefineApi("user32!MessageBoxA"); // set the MessageBox pointer to a function in the user32.dll
function average(a,b) { return((a+b)/2); } // this is called this way: x = average(123,456); // assign the average of 123 and 456 to xLC Während C-Skript lediglich ein var zurückliefern kann, können Lite-C, C, oder C++ jede Variable und jeden Pointer-Typen zurückliefern. Anstelle von "function" wird die Funktion dann aber mit dem zurückgebenen Typus, wie etwa float, ENTITY* oder was auch immer, definiert. Beispiele (Lite-C):
PANEL* emptypanel(layer) { return(pan_create("",layer)); } float fAverage(float a, float b) { return((a+b)/2.0); } ... float x = fAverage(123.0,456.0); // assign the average of 123 and 456 to x
Um anzuzeigen, dass eine Funktion nichts zurückliefert, kann Sie mit dem Typus void definiert werden:
void beep_twice() { beep(); beep(); }
Das ist der Grund weshalb Sie die Main-Funktion manchmal als void main() anstelle von function main() definiert vorfinden. Trotzdem kann der Typ function auch zum Zürückliefern von nichts bentutzt werden, beides ist gleichwertig.
function factorial(x) { if (x <= 1) { return(1); } if (x >= 10) { return(0); } // number becomes too big... return (x*factorial(x-1)); }
Rekursive Funktionen müssen mit Bedacht benutzt werden und sind nur etwas für fortgeschrittene Anwender. Wenn Sie einen Fehler machen, z.B. eine Funktion, die sich unendlich selbst aufruft, kann es Ihnen leicht passieren, dass Sie einen sogenannten 'stack overflow' produzieren und der bringt den Computer zum Absturz. Die Stack-Grösse ermöglicht eine Rekursionstiefe von etwa 10.000 ineinander verschachtelte Funktionsaufrufe.
double square(double x) { return(x*x); } int square(int x) { return(x*x); } ... double x = square(3.0); // calls the first square function int i = square(3); // calls the second square function
!! Gehen Sie beim Aufrufen überladener Funktionen sicher, dass die Argument-Typen exakt auf eine der definierten Funktionen passt. Die automatische Typenkonvertierung und Pointererkennung des lite-C-Kompilierers greifen hier nicht. Das bedeutet, dass Sie tatsächlich den C++ &-Operatoren für Pointer benutzen müssen. Will die Funktion einen STRING*, reicht es nicht, ein char* zu übergeben. Behalten Sie dies im Kopf beim Übergeben von Konstanten an überladene Funitionen, die möglicherweise von anderem Typus sind als Sie das erwarten. Textkonstanten wie "dies ist ein Text" sind vom Typ char* (nicht STRING*), ganze Zahlen wie 12345 sind vom Typ long (nicht var), Konstanten mit Dezimalen wie 123,456 sind vom Typ double (nicht float) und Einzelbuchstaben-Konstanten wie 'A' or 'B' gehören ebenfalls zum Typus long (nicht char). Sorgen Sie also dafür, dass für alle Argumententypen, die vorkommen können überladene Funktionen existieren. Oder Sie verwenden zum Konvertieren der Argumente in den richtigen Typus Typecasting. Beispiel:
int square(int x) { return(x*x); } var square(var x) { return(x*x); } ... int i = square(3); // calls the first square function var a = square((var)3); // calls the second square functionA7.75 Wird trotzdem kein exakt passender numerischer Parameter gefunden, konvertiert der Kompilierer numerische Typen zum Finden der nächstbesten Übereinstimmung unter den überladenen Funktionen. Wird überhaupt keine Übereinstimmung gefunden, gibt der Kompilierer eine Fehlermeldung aus.
Direkt nach der main()-Funktion werden die Funktionen, die mit ..._startup enden ausgeführt und LC vor dem Initialisieren von Engine und Video. Auf diese Weise kann jedes Skript seine eigene Startup-Funktion haben, die seine Variablen oder Objekte initialisiert. Beispiel:
function debug_startup() // automatically started if this script is included { set(debug_panel,SHOW); while (1) { // run forever debug_panel.pos_y = screen_size.y - 15; fps = 0.9*fps + 0.1/time_step; wait(1); } }
LC Funktionen, die mit ..._event enden, werden dem entsprechenden Event automatisch zugewiesen. Eine Event-Funktion wird durch einen bestimmten Auslöser, - etwa eine Entity-Kollision, einen Knopf- oder Mausklick oder bei Spielstart beim Laden des Levels - gestartet. Eine Liste von Events findet sich im Kapitel Vordefinierte Variablen. Beispiel:
// the following function is automatically assigned to the on_x event function on_x_event() { printf("[X] key pressed!"); }
!! Eine Event-Funktion kann während der Ausführung einer jeglichen Engine-Funktion, die Events erlaubt - etwa einer Kollisions- oder wait()-Anweisung -, starten. Sie kann starten, wenn Sie es gar nicht erwarten. Daher muß darauf geachtet werden, daß Event-Funktionen keine globalen Variablen oder andere Funktionen, die sich darauf beziehen verändern.
Aktionen stellen eine weitere spezielle Art von Funktionen dar. Sie haben keine Parameter und liefern keinen Wert zurück, erscheinen aber in der Action-Pop-Up-Liste in WEDs Eeigenschaftspanel. Aktionen lassen sich also per WED Entities zuweisen und werden nach Laden des Levels automatisch ausgeführt. Die Namen von Aktionen sind auf 20 Zeichen begrenzt. Außer diesen gibt es keine weiteren Unterschiede zwischen Funktionen und Aktionen. Hiert unser übliches Beispiel einer Entity-Aktion, welche lediglich aus drei Anweisungen besteht und die Entity rotieren läst. Beispiel:
action ent_rotate() { while (1) { my.pan += 3*time_step; wait(1); } }
In einer Multiplayerumgebung laufen Aktionen normalerweise nur auf dem Server; so beeinflußen sie die gesamte Spiele-Welt. Alle anderen Funktionen, die üblicherweise dem Benutzer-Interface zugewiesen sind - also Funktionen, die an Tasten oder Panel-Buttons gebunden sind - laufen dort, wo sie ausgelöst werden. in der Regel auf einem Client. In einer Ein-Spieler-Umgebung sind Server und Client derselbe PC und daher braucht es Sie nicht zu kümmern, wo Ihre Funktionen laufen.