LSL-Kurs
Hier findet ihr meinen Grundkurs zum Einstieg in die Scriptsprache LSL. Das Tutorial für den LSLEditor ist weiter unten, nämlich hier.
Einleitung
Willkommen zum Programmierkurs in der LSL-Scriptsprache.
LSL heißt ausgeschrieben "Linden Scripting Language", ist also die hauseigene Programmiersprache von Linden Labs. Sie ähnelt C oder Java, wer darin schon mal etwas programmiert hat, wird vieles wiedererkennen. Damit können wir Scripte schreiben - das sind spezielle Texte, mit denen wir das Verhalten von Objekten beeinflussen können, etwa eine Drehung erzeugen. Zur Abarbeitung eines Scriptes wird kein Extra-Programm benötigt.
Solange ein Script nur im Inventar liegt, ist es nutzlos. Erst wenn man es in das Inventar eines Objektes befördert, wird es abgearbeitet. Dabei kann es sich um beliebige Objekte handeln - etwa einen Würfel, eine Flasche oder alles, was man an den Körper anhängen kann wie einen Rock z.B.
Möglichkeiten der Scripterstellung
Um Scripte zu erstellen, gibt es mehrere Möglichkeiten. Die erste ist im Inventar, klickt oben auf "Erstellen", dann Neues Script. Daraufhin wird der SL-eigene Editor geöffnet. Die zweite Möglichkeit besteht darin, ein Skript in einem Objekt zu erzeugen. Klickt dazu mit der rechten Maustaste auf den Boden, dann auf "Erstellen" und dann noch einmal mit der linken Maustaste. Damit habt ihr einen Würfel erschaffen, das Bearbeitungsfenster sollte auch offen sein. Klickt dort auf den Pfeil nach rechts und dann auf den Reiter "Inhalt". Hier klickt ihr auch wieder auf "Neues Script".
Die dritte Möglichkeit ist die Verwendung eines externen Editors. Ich würde euch den LSLEditor ans Herz legen - mit dem arbeite ich auch immer, weil der eingebaute Editor doch recht mager ist. Der Download-Link ist am Ende des Textes. Euer Script bekommt ihr dann mit der Kopieren/Einfügen-Methode nach SL. Der LSLEditor hat viele Vorteile gegenüber dem eingebauten, unter anderem verfügt er über eine Codevervollständigung, Syntaxprüfung und testen kann man das Script auch. Zudem liegt ihm die englischsprachige Wiki als Hilfedatei bei.
Ein erstes Script
Egal nach welcher Methode ihr den Editor geöffnet habt, seht ihr schon das SL-Standardscript vor euch, was so aussehen sollte:
default
{
state_entry()
{
llSay(0, "Hello, Avatar!");
}
touch_start(integer total_number)
{
llSay(0, "Touched.");
}
}
Das erste, was auffällt, sind die vielen geschweiften Klammern. Diese umschließen Programmabschnitte, die jeweils zwischen einer öffnenden und einer schließenden Klammer liegen. Es müssen also immer alle öffnenden und schließenden Klammern paarweise vorhanden sein, sonst bekommt ihr eine Fehlermeldung. Zu einem Programmabschnitt gehört alles, was zwischen diesen geschweiften Klammern liegt, das können auch andere Programmabschnitte sein.
Das erste Wort "default" bezeichnet einen sogenannten State, also einen Zustand, besser gesagt einen Programmabschnitt. "default" ist dabei der Standardzustand eines Scriptes und muss immer vorhanden sein. Er ist also der Startpunkt des Scriptes. Es können auch mehrere Zustände vorhanden sein, aber dazu kommen wir später. Unser Script verbleibt bei der Abarbeitung in diesem Zustand und wartet darauf, dass etwas passiert.
Der Bereich dieses States reicht von der folgenden öffnenden geschweiften Klammer bis zur letzten schließenden. Die letzte schließende Klammer ist demnach das Ende des Scriptes.
Des weiteren haben wir hier zwei Events (Ereignisse), die abgearbeitet werden, wenn etwas passiert. (Eigentlich sind es event-handler - Programmteile, die auf Ereignisse reagieren - aber der Einfachheit halber werden sie als Events bezeichnet) Das Event "state_entry" wird ausgeführt, wenn der "default"-Zustand erreicht wird, also jedes Mal, wenn das Script gestartet wird. Das Event "touch_start" wird ausgeführt, wenn das Objekt, in dem sich das Script befindet, berührt wird. Auch hier sind beide Male die Programmabschnitte in geschweifte Klammern gesetzt. Es gibt eine ganze Anzahl von Events, die wir verwenden können, die meisten haben englische Bezeichnungen, aus denen hervorgeht, wann sie eintreten. Nachlesen könnt ihr das z.B. in der Wiki, die Links befinden sich am Ende des Textes.
Innerhalb der beiden Ereignisse steht jeweils eine Programmanweisung oder kurz Befehl bzw. Funktion, dort passiert also etwas. Es können auch viel mehr Befehle vorhanden sein, wir haben aber hier nur einen. Die Anweisung llSay(Kanal, Text) bewirkt, dass ein Text im Chat auf einem bestimmten Kanal ausgegeben wird, wobei Kanal 0 der offene Chat ist, alle anderen Kanäle sind intern. Es gibt über 200 solcher Befehle und Funktionen, ebenfalls nachzulesen in der Wiki. Alle Befehle fangen mit "ll" an (Abkürzung für Linden Labs) und müssen am Ende der Zeile mit einem Semikolon abgeschlossen werden.
Es ist auch guter Programmierstil, wenn alle Events und Befehle, die innerhalb von States bzw. Events vorkommen, mit der Tabulatortaste eingerückt werden, das macht die Scripte lesbarer.
Also noch mal ganz kurz: States beinhalten Events und Events beinhalten Anweisungen.
Ausführen eines Scriptes
Wenn wir das Script nochmals analysieren, sollte Folgendes passieren. Wenn wir das Objekt erzeugen oder aus dem Inventar holen, wird der Text "Hello, Avatar!" ausgegeben. Berühren wir das Objekt, so wird "Touched." ausgegeben. Versuchen wir's doch gleich mal.
Gebt mal ganz am Ende ein Leerzeichen ein und klickt auf "Speichern", nach kurzer Zeit sollte die Meldung "Compile successful" erscheinen. Der Editor führt während des Speichervorgangs eine Syntaxprüfung durch, d.h. er prüft, ob alles richtig geschrieben wurde. Im Fehlerfall erhaltet ihr eine entsprechende Meldung und es wird auch gezeigt, wo der Fehler auftrat. Entfernt mal das Semikolon unter "Avatar" und versucht erneut zu speichern. Es sollte ein "Syntax Error" gemeldet werden.
Macht das Semikolon wieder rein, speichert und schließt den Editor. Erzeugt ein Objekt wie oben beschrieben und kopiert euer Skript in dessen Inhalt, wenn ihr es nicht schon habt. Schließt das Bearbeitungsfenster wieder. Klickt auf das Objekt und beobachtet, ob es sich so verhält wie erwartet. Es sollte aber alles klappen.
Öffnet den Würfel wieder und klickt doppelt auf das Script, um es im Editor anzuzeigen. Ändert nun die beiden Texte in den Anführungszeichen in "Hallo Du!" bzw. "Du hast mich angefasst!". Speichert, schließt alles und testet erneut.
Öffnet wieder das Script im Editor und ändert den ersten Parameter der zweiten llSay-Anweisung von 0 auf 1. Speichern und testen. Diesmal dürfte kein Text erscheinen, wenn ihr das Objekt berührt. Das liegt daran, dass nur der Kanal 0 zum offenen Chat gehört und auf dem Bildschirm ausgegeben wird, alle anderen Kanäle müssen durch spezielle Events "abgehört" werden.
Datentypen und Variablen
Es gibt in LSL verschieden Datentypen, die wir verwenden können. Zwei davon kennen wir bereits: Ganze Zahlen und Texte. Ganzzahlige Werte heißen integer und befinden sich in einem Wertebereich von ca. ±2 Milliarden. Texte heißen strings, was so viel wie Zeichenketten bedeutet. Ändert nun das Standardscript wie folgt:
integer Kanal = 0;
string Nachricht;
default
{
state_entry()
{
Nachricht = "Hallo Du";
llSay(Kanal, Nachricht);
}
touch_start(integer total_number)
{
Nachricht = "Du hast mich angefasst.";
llSay(Kanal, Nachricht);
}
}
Wir haben hier zwei Variablen verwendet, nämlich Kanal und Nachricht. Variablen dienen zum Aufbewahren von Daten, in unserem Fall einer Zahl und einer Zeichenkette. Sie können nur innerhalb des Programmabschnittes verwendet werden, in dem sie definiert sind, und sie müssen vor der Verwendung definiert sein (in der Form Datentyp Name; siehe Beispiel). Eine Ausnahme bilden Variablen, wenn sie vor "default" stehen wie bei uns, dann sind es sogenannte Globale Variablen, die überall im Script benutzt werden können.
Man kann Variablen einen Anfangswert zuweisen, wie hier bei der Variablen Kanal. Eine Zuweisung geschieht durch das Gleichheitszeichen und einem Wert. Dieser muss natürlich auch zum Datentyp passen, ihr könnt keiner numerischen Variablen eine Zeichenkette zuweisen.
In den beiden Events weisen wir der Variablen Nachricht jeweils einen anderen Text zu und geben ihn aus. Achtet bitte auf die richtige Schreibweise, LSL will korrekte Groß/Kleinschreibung. Also Kanal ist nicht gleich kanal, und llsay wird sicher beim Speichern bemängelt. Speichert und testet das Script.
Es stehen noch mehr Datentypen zur Verfügung. Der Typ float speichert gebrochene Zahlen, also solche mit Komma. In einem key können wir sogenannte UUIDs speichern, das sind 32-stellige Schlüssel für Avatare und Objekte, die es nur einmal gibt. In einer list lassen sich Werte verschiedenen Typs speichern, allerdings ist die Bearbeitung von Listen etwas gewöhnungsbedürftig. Die Listenelemente stehen in eckigen Klammern ([]). Außerdem gib t es noch vector und rotation, die aus drei bzw. vier floats bestehen und als Einheit betrachtet werden. Diese werden in spitze Klammern gesetzt (<>). Zu beachten ist die englische Darstellung gebrochener Zahlen, also das Komma wird hier ein Punkt.
Die verschiedenen Datentypen kann man zum Teil konvertieren, d.h. ineinander umwandeln. Schreibt folgende Zeilen in das Event touch_start;
integer Summe = 100;
llSay(Kanal, Summe);
Dieses Script wird einen Fehler erzeugen, weil mit llSay ein Text ausgegeben werden soll, Summe aber eine Zahl ist. Ändert diese Zeile daher in:
llSay(Kanal, (string)Summe);
Damit haben wir dem Editor mitgeteilt, dass er unsere Variable Summe als String behandeln soll. Die Typumwandlung zur Darstellung der Zahl findet im Hintergrund statt, davon könnt ihr euch überzeugen, wenn ihr das Script testet.
Konstanten gibt es in LSL nicht, dazu werden initialisierte Variablen verwendet. Zur Kennzeichnung werden sie komplett in Grossbuchstaben dargestellt, das ist aber nicht zwingend vorgeschrieben. Ihr solltet euch trotzdem daran halten, ändert also überall die Variable Kanal in KANAL. Diese "konstanten" Variablen dürfen dann auch im Programm nicht mehr verändert werden.
Ein neues Script
Wir wollen nun ein kleines Script schreiben, was die Farbe unseres Objektes bei jeder Berührung ändert. Farben werden durch Vektoren durch ihre Rot-, Grün- und Blauanteile dargestellt. Ein vector ist definiert als
<x,y,z>
Dies kann man für Farben auch als Vektor
<Rot, Grün, Blau>
auffassen. Die Farbwerte können im Bereich von 0 bis 1 liegen. Für die Erzeugung von zufälligen Farbwerten steht uns die Funktion
float llFrand(float max);
zur Verfügung, die einen zufälligen Wert zwischen 0 und max zurückgibt. Wir definieren nun am Anfang des Scriptes drei Variablen für die Teilfarben:
float Rot;
float Gruen;
float Blau;
Innerhalb des Events touch_start löschen wir die Zeile mit "llSay und schreiben stattdessen:
Rot = llFrand(1);
Gruen = llFrand(1);
Blau = llFrand(1);
llSetColor(<Rot, Gruen, Blau>, ALL_SIDES);
Mit llSetColor wird die Oberflächenfarbe eines Objektes eingestellt. Wir machen aus unseren drei Farbwerten kurzerhand einen Vektor, indem wir sie in spitze Klammern setzen. Der zweite Parameter des Befehls bestimmt, welche der Flächen gefärbt wird, in unserem Fall verwenden wir eine vordefinierte Konstante für alle Flächen.
Speichert das Skript und testet es.
Über Funktionen
Wie schon erwähnt, gibt es mehr als 200 Funktionen und Anweisungen. Diese sind alle im Format
Datentyp Funktionsname(Datentyp Parameter)
definiert. Der Datentyp der Funktion bestimmt den Rückgabewert, also ob es eine Zahl, eine Zeichenkette o.ä. ist. Gibt eine Funktion nichts zurück, spricht man auch
von einer Anweisung oder Prozedur. Der oder die Parameter müssen auch bestimmte Datentypen besitzen, können aber auch fehlen.
Beispiele:
llSay hat zwei Parameter (Ganzzahl und String) und gibt keinen Wert zurück
llFrand hat einen Parameter (float) und gibt einen float-Wert zurück
Solche Funktionen können wir auch selbst schreiben. Für unser Script könnten wir eine Funktion gebrauchen, die keinen Parameter benötigt und eine zufällige Farbe zurückgibt.
Löscht die drei float-Variablen am Anfang des Scriptes und schreibt oberhalb von "default":
vector Zufallsfarbe()
{
float Rot = llFrand(1);
float Gruen = llFrand(1);
float Blau = llFrand(1);
return <Rot, Gruen, Blau>;
}
Wir haben hier also wieder unsere drei Variablen, die wir gleich mit Zufallszahlen initialisieren und mit der Anweisung return als Vektor zurückgeben. Testen wir es, indem wir im on_touch Event die Zeile
llSetColor(Zufallsfarbe(), ALL_SIDES);
hinzufügen. Mit llSetColor färben wir unser Objekt ein, die Parameter sind die Farbe, die wir hier zufällig erzeugen, und die Oberfläche. Die vorgefertigte Konstante ALL_SIDES gibt an, dass alles gefärbt werden soll.
Meine Größe
Wir wollen nun ein kleines Script schreiben, was unsere Größe anzeigt. Erstellt ein neues Standard-Script und löscht die Zeile im Event touch_start. Die Funktion zur Ermittlung der Größe lautet
vector llGetAgentSize(key id)
wobei der Parameter id die UUID des Avatars ist, der das Objekt. Diese müssen wir natürlich erst einmal heraus bekommen. Dafür gibt es auch eine Funktion, die da lautet
key llDetectedKey (integer number)
Der Parameter number muss für unsere Zwecke Null sein.
Wir benötigen eine Variable für die Größe und eine für die UUID, schreibt also in das Event "touch_start"
vector Groesse;
key id;
Eine vector-Variable brauchen wir deswegen, weil es bereits eine Funktion gibt, die uns die Größenausdehnungen von Avataren zurück gibt, und zwar dreidimensional (Länge, Breite, Höhe). Ein vector ist definiert als
<x,y,z>
und auf die einzelnen Komponenten kann man über Groesse.x, Groesse.y und Groesse.z zugreifen. Letzteres wäre also unsere Höhe bzw. Größe.
Wir schreiben nun in das Event touch_start die Zeilen
id = llDetectedKey(0);
Groesse = llGetAgentSize(id);
Die Größe soll natürlich auch ausgegeben werden, wir schreiben daher
llSay(0, "Du bist " + (string)Groesse.z + " m groß.");
Wir wandeln hier wieder die Zahl Groesse.x (eine gebrochene Zahl diesmal) in eine Zeichenkette um und verknüpfen sie durch ein einfaches Plus mit den anderen Strings. Man kann damit beliebige Strings aneinander hängen, andere mathematische Operationen sind mit Zeichenketten aber nicht möglich.
Zum Schluss ändern wir noch den Text im Event state_entry:
llSay(0, "Berühre mich, um deine Größe zu erfahren.");
Beim Testen merken wir, dass jede Menge Nachkommastellen vorhanden sind. Das liegt am Datentyp float der Variable Groesse.z. Um eine ordentliche Anzeige zu bekommen, müssen wir die Zahl formatieren. Wir benötigen dazu eine neue string-Variable, nennen wir sie Meter, also
string Meter;
In dieser Variable speichern wir die ersten vier Stellen von Groesse.z, also die Vorkommastelle, den Punkt und zwei Nachkommastellen. Das können wir mit der Funktion llGetSubString machen:
Meter = llGetSubString((string)Groesse.z, 0, 3);
Diese Funktion liefert einen Teil einer Zeichenkette zurück, die erste Zahl ist die Startposition, die zweite die Endposition, wobei die Zählung bei 0 beginnt. Die Textausgabe müssen wir auch noch ändern:
llSay(0, "Du bist " + Meter + " m groß.");
Das sollte jetzt besser aussehen.
Vielen wird es aufgefallen sein, dass der Text "Hello, Avatar" nur nach dem Erstellen des Scriptes zu sehen war. Wenn er auch erscheinen soll, sobald das Objekt aus dem Inventar gezogen wurde, muss das Event on_rez behandelt werden. Der Code sieht so aus:
on_rez(integer start_param)
{
llResetScript(); // setzt das Skript zurück
}
Damit wird jedes Mal, wenn das Objekt erzeugt wird (also aus dem Inventar in die Welt gebracht wurde), das Script zurück gesetzt. Das führt dazu, dass das Event state_entry aufgerufen wird.
Außerdem habe ich hier einen Kommentar eingefügt. Kommentare beginnen immer mit zwei Schrägstrichen. Alles, was dahinter steht, wird während des Programmablaufes übergangen. Kommentare sollten immer dann verwendet werden, wenn etwas unklar ist bzw. zu Missverständnissen führen könnte, z.B. Zweck der verwendeten Variablen o.ä.
Stack und Heap
Ich will an dieser Stelle einmal kurz auf die Programmverwaltung eingehen. Jedem Script stehen in einem Simulator 16 kB zur Verfügung. Im unteren Speicherbereich befinden sich der Programmcode und die globalen Daten (alles was vor dem "default" steht, gefolgt von den Daten, die während des Programmablaufs angelegt werden (neue Zeichenketten, Listen u.ä.). Im oberen Speicherbereich werden Daten und Programminformationen abgelegt, die innerhalb von Funktionen verwendet werden, der sogenannte Stack. Während das Programm läuft, wächst der Heap nach oben und der Stack nach unten, und wenn sich beide begegnen, erscheint die Fehlermeldung "Stack-Heap Collison" und das Script wird angehalten, das heißt es erfolgt keine Programmabarbeitung mehr. Meistens passiert das, wenn zuviel Listen und Zeichenketten verwendet werden.
Ein Dialog
Wir wollen uns jetzt einen Dialog anzeigen lassen. Das sind die kleinen blauen Fenster, die ab und zu rechts oben auf dem Bildschirm erscheinen und eine oder mehrere Tasten besitzen. Die Schaltfläche "ignorieren" ist immer vorhanden, dazu können noch bis zu 12 weitere Tasten erscheinen. Zum Anzeigen dieses Dialogs dient die Anweisung llDialog:
llDialog(key id, string text, list buttons, integer channel));
id ist dabei der Schlüssel des Avatars, für den der Dialog angezeigt werden soll, text ist der Anzeigetext, buttons ist eine Liste der Beschriftungen für die Tasten und channel ist der Chatkanal. Bei Betätigung einer Taste wird die Beschriftung in diesem Chatkanal ausgegeben.
Erzeugt ein neues Skript, löscht die Zeile im Event touch_start und schreibt folgende Zeilen hinein:
key id = llDetectedKey(0);
llDialog(id, "Drücke eine Taste", ["Taste 1", "Taste 2", "Taste 3"], 20);
Dies bringt bei Berührung einen Dialog mit drei Tasten auf den Bildschirm; leider tut sich nichts, wenn wir eine Taste betätigen. Niemand hört nämlich auf den Kanal 20, also müssen wir selber dafür sorgen. Dies geschieht in zwei Schritten: Zuerst müssen wir dafür sorgen, dass das Script den Kanal abhört, und dann die Tasten auswerten. Zum Abhören installieren wir einen sogenannten Listener, der kommt in den Event state_entry.
llListen(integer channel, string name, key id, string msg);
Alles, was in der Klammer steht, sind Filter, die man nicht unbedingt setzen muss. Der Kanal muss dabei angegeben werden. Mit key und/oder id kann man auf Nachrichten auf einen bestimmten Avatar oder ein bestimmtes Objekt warten, und mit msg können nur bestimmte Nachrichten ausgewertet werden. Wir wollen nur den Kanal 20 überwachen, also schreiben wir
llListen(20, "", NULL_KEY, "");
Die Auswertung erfolgt in einem Extra-Event, welches "listen" heißt und die gleichen Parameter besitzt wie der Listener. Schreibt also zwischen die vorletzte und letzte Klammer des Scriptes
listen(integer channel, string name, key id, string msg)
{
llSay(0, "Eine Taste wurde gedrückt.");
}
Nun reagiert das Script zwar, wenn wir eine Taste drücken, aber sinnvoll ist das auch nicht. Wir müssen die Nachrichten auswerten, um zu erkennen, welche Taste gedrückt wurde.
Kontrollstrukturen
Zur Steuerung des Programmablaufes gibt es sogenannte Kontrollstrukturen. Die am meisten verwendete ist die if-Anweisung. Sie hat das Format
if(Bedingung)
{
Programmblock 1
}
else
{
Programmblock 2
}
Beachte, dass kein Semikolon am Zeilenende gesetzt werden darf. Wenn die Bedingung in der if-Zeile erfüllt ist, dann wird der Programmblock 1 ausgeführt, ansonsten der Programmblock 2, niemals aber beide gleichzeitig. Den else-Teil kann man auch weglassen. Die Bedingung besteht meist aus Vergleichen, wie wir sie gleich in unserem Script anwenden werden. Auf Gleichheit wird mit dem Operator "==" geprüft, aus Ungleichheit mit "!=". Wir prüfen, welche Taste eine Nachricht gesendet hat.
Löscht die Zeile im Event listen und schreibt folgende Zeilen hinein:
if(msg == "Taste 1")
{
llSay(0, "Taste 1 wurde gedrückt");
}
if(msg == "Taste 2")
{
llSay(0, "Taste 2 wurde gedrückt");
}
if(msg == "Taste 3")
{
llSay(0, "Taste 3 wurde gedrückt");
}
Es sollte nun bei Tastendruck der entsprechende Text erscheinen. Stören tut aber, das jeder die Antwort hören kann. Wenn Texte nur für den Besitzer des Scriptes ausgegeben werden sollen, verwenden wir die Anweisung llOwnerSay(Text). Hier ist keine Kanalnummer notwendig. Ändert nun die drei Zeilen mit llSay in llOwnerSay in der Form
llOwnerSay("Taste 1 wurde gedrückt");
Wenn wir nur eine Anweisung in einem Programmblock haben, können die geschweiften Klammern auch weggelassen werden.
if(msg == "Taste 1") llOwnerSay("Taste 1 wurde gedrückt");
if(msg == "Taste 2") llOwnerSay("Taste 2 wurde gedrückt");
if(msg == "Taste 3") llOwnerSay("Taste 3 wurde gedrückt");
Wir wollen nun auch mal etwas praktisches tun, nämlich unseren Würfel auf Kopfdruck umfärben. Ändert die Dialogzeile im Event state_entry
llDialog(id, "Drücke eine Taste", ["Rot", "Grün", "Blau"], 20);
und die Zeilen im Event touch_start.
if(msg == "Rot") llSetColor(<1,0,0>, ALL_SIDES);
if(msg == "Grün") llSetColor(<0,1,0>, ALL_SIDES);
if(msg == "Blau") llSetColor(<0,0,1>, ALL_SIDES);
Man kann übrigens die Kanäle auch direkt aufrufen. Gebt mal in die Chatzeile
/20 Rot
ein. Der Würfel sollte sich färben.
An- und Ausziehen
Wir wollen jetzt einmal ein Objekt anziehen bzw. anlegen und sehen, was passiert.
Das An- und Ausziehen löst das Event
attach(key attached)
aus, wobei der Parameter attached der Schlüssel desjenigen ist, der das Objekt anzieht. Beim Ausziehen wird ein leerer Schlüssel namens NULL_KEY übergeben. Das Event touch_start brauchen wir hier nicht und löschen es, statt dessen fügen wir das Event attach ein:
attach(key attached)
{
if (attached == NULL_KEY) llSay(0, "Ausgezogen.");
else llSay(0, "Angezogen.");
}
Beim An- und Ausziehen sollte nun ein entsprechender Text erscheinen.
Es stört natürlich, dass uns jetzt die Kiste am Arm hängt. Daher machen wir sie beim Anziehen einfach unsichtbar und beim Ausziehen wieder sichtbar, damit wir sie auch wiederfinden. Dazu gibt es die Anweisung llSetAlpha, die eine Wert zwischen 0 und 1 erwartet und wieder die Oberfläche wie bei llSetColor. Der Wert 0 steht für durchsichtig, der Wert 1 für völlig undurchsichtig. Wir schreiben also
if (attached == NULL_KEY)
{
llSay(0, "Ausgezogen.");
llSetAlpha(1, ALL_SIDES);
}
else
{
llSay(0, "Angezogen.");
llSetAlpha(0, ALL_SIDES);
}
Der Würfel dürfte nun nicht mehr zu sehen sein.
States
Zur Erläuterung der Verwendung von States oder Zuständen schreiben wir nun ein Programm, welches eine Lampe simulieren soll. Diese hat zwei Zustände: ein und aus. Wir erstellen ein neues Skript, löschen das Event touch_start und die Zeile in state_entry und schreiben dort hinein:
llSetColor(<1,1,1>, ALL_SIDES);
state aus;
Dies setzt die Farbe auf weiß und springt dann in den Zustand aus. Die beiden Zustände müssen wir natürlich erst programmieren, schreibt also folgenden Code ans Ende des Skriptes:
state ein
{
state_entry()
{
llSetPrimitiveParams([PRIM_FULLBRIGHT, ALL_SIDES, TRUE, PRIM_GLOW, ALL_SIDES, 1.0]);
}
touch_start(integer total_number)
{
state aus;
}
}
state aus
{
state_entry()
{
llSetPrimitiveParams([PRIM_FULLBRIGHT, ALL_SIDES, FALSE, PRIM_GLOW, ALL_SIDES, 0.0]);
}
touch_start(integer total_number)
{
state ein;
}
}
In beiden States sind jeweils die Ereignisse state_entry und touch_start vorhanden. Da aber immer nur ein State aktiv sein kann, werden auch immer nur die Ereignisse des aktiven States abgearbeitet. In unserem Fall wird beim Aktivieren des Aus-Zustandes die Helligkeit und das Leuchten abgeschaltet (mit llSetPrimitiveParams). Anschließend wird aus eine Berührung gewartet, die den Übergang zum Ein-Zustand zur Folge hat, wo die Helligkeit auf Maximum gesetzt wird und das Leuchten eingeschaltet wird. Beim nächsten Tastendruck erfolgt wieder der Übergang in den Aus-Zustand usw. Für nähere Einzelheiten zur Anweisung llSetPrimitiveParams sei auf die Wiki verwiesen, es gibt einfach zu viele Parameter, um diese hier zu besprechen.
Wir wollen uns nun ein Blinklicht basteln. Löscht die beiden States touch_start, es soll ja automatisch gehen. Die Umschaltung zwischen den States erfolgt durch einen Timer, der im Ereignis state_entry des default-Zustandes eingerichtet wird.
llSetTimerEvent(0.5);
Mit dieser Anweisung wird das Ereignis timer jede halbe Sekunde ausgelöst, das wir dann auch bearbeiten. Im State "ein" schreiben wir:
timer()
{
state aus;
}
und im State "aus" das gleiche, nur dass dort der State "ein" aufgerufen wird. Unsere Lampe sollte nun lustig vor sich hin blinken.
Ein Scanner
Das nächste Script soll die Umgebung nach anderen Avataren abtasten. Dafür werden zwei Anweisungen bereitgestellt, llSensor und llSensorRepeat. Sie unterscheiden sich nur dadurch, dass llSensor nur einmal aufgerufen wird und llSensorRepeat zyklisch. Wir verwenden die letztere Anweisung.
llSensorRepeat(string name, key id, integer type, float range, float arc, float rate);
Gleich 6 Parameter sind zu beachten, die da wären:
name - scannt nach Objekten oder Avataren mit bestimmtem Namen. Bei einem Leerstring wird nach allem gescannt.
id - Wie name, nur dass ein Schlüssel vorgegeben werden kann.
type - gibt an, wonach gesucht werden soll (Avatare, Objekte, aktiv/passiv usw.)
range - die Entfernung in Metern, bis zu der gescannt wird (maximal 96 m)
arc - der Umkreis im Bogenmaß. Pi würde eine Halbkugel über dem Objekt bedeuten, 2*Pi eine Kugel.
rate - die Abtastrate in Sekunden (entfällt bei llSensor)
Wir schreiben in das Ereignis state_entry
llSensorRepeat("", "", AGENT, 40, PI, 5);
Das bedeutet, wir suchen alle 5 Sekunden nach allen Avataren im Umkreis von 40m.
Das dazugehörige Ereignis müssen wir auch noch eintragen:
sensor(integer total_number)
{
llOwnerSay((string)total_number + " Avatare gefunden.");
}
Das Ereignis sensor übergibt uns netterweise gleich die Anzahl der gefundenen Avatare bzw. Objekte. Wenn das Script gespeichert wurde, müsste der Scanner nach 5 Sekunden anfangen, die Umgebung abzutasten.
Wir wollen aber wissen, wer in der Nähe ist. Dazu müssen wir die einzelnen Avatare einzeln durchgehen und ihre Namen ermitteln. Dazu gibt es die Programmanweisung for. Wir schreiben in das Event sensor:
integer i;
for(i = 0; i < total_number; i++)
{
llOwnerSay(llDetectedName(i));
}
Das for ist eine Schleife und bewirkt Folgendes: Zuerst wird einer sogenannten Laufvariable ein Startwert zugewiesen, hier 0. Dann wird angegeben, wie lange die Schleife durchlaufen werden soll, also solange die Variable i kleiner als die Gesamtzahl ist. Und zum Schluss wird angegeben, was mit der Variablen gemacht werden soll. I++ bedeutet Erhöhung um 1.
Die Variable i hat also beim ersten Durchlauf den Wert 0, dann 1, dann 2 usw. bis sie den Wert total_number erreicht hat. Und mit dem jeweiligen Wert wird die Funktion llDetectedName aufgerufen, die den dazugehörigen Namen ermittelt. Zu beachten ist, dass am Ende der for-Zeile kein Semikolon stehen darf.
Wir können auch noch die Entfernung des jeweiligen Avatars ermitteln. Dazu benötigen wir die Position der Kiste, die von der Funktion llGetPos als vector zurückgegeben wird.
vector Pos = llGetPos();
Die Distanz zwischen zwei Vektoren wird mit der Funktion llVecDist(vector1, vector2) ermittelt und gibt eine gebrochene Zahl zurück. Wir müssen also wieder die Typen für die Distanz umwandeln, erst in ein integer, damit die Nachkommastellen wegfallen, dann in einen string zur Ausgabe.
sensor(integer total_number)
{
llOwnerSay((string)total_number + " Avatare gefunden.");
integer i;
vector Pos = llGetPos();
integer Distanz;
for(i = 0; i < total_number; i++)
{
Distanz = (integer)llVecDist(Pos, llDetectedPos(i));
llOwnerSay(llDetectedName(i) + " " + (string)Distanz + "m");
}
}
Geschenkt bekommen oder bezahlen
Im nächsten Skript beschäftigen wir uns mit Objekten, die etwas verschenken bzw. verkaufen. Jeder kennt doch diese Objekte, die uns Landmarken und ähnliches geben, wenn wir einen Laden betreten oder etwas anklicken. So etwas machen wir jetzt auch.
Dazu müssen wir uns mit dem Inventar befassen. Inventare von Avataren und Objekten funktionieren auf die gleiche Art und Weise, nur dass man in Objekten keine neuen Ordner erstellen kann. Mit
llGetInventoryNumber(integer type);
ermittelt man die Anzahl der Gegenstände eines bestimmten Typs. INVENTORY_ALL würde die Gesamtzahl aller Gegenstände liefern und INVENTORY_LANDMARK die aller Landmarken. Die Funktion
llGetInventoryName(integer type, integer number);
gibt den Namen des Gegenstands mit der Nummer number zurück, wieder beginnend bei 0. Und letztlich können wir mit
llGiveInventory(key destination, string name);
den Gegenstand mit Namen name an das Objekt oder den Avatar mit dem Schlüssel destination geben.
Stellt euch einen Bierstand vor, bei dem ihr beim Anklicken eine Flasche bekommt. (Stellt einen Bierstand hin). Nehmt euch jeder eine Flasche, die brauchen wir jetzt. Erzeugt ein neues Objekt, beginnt ein neues Skript und schreibt in das Ereignis touch_start
if (llGetInventoryNumber(INVENTORY_ALL) == 1 ) llSay(0, "Schade, alles alle.");
else
{
string name = llGetInventoryName(INVENTORY_ALL, 1);
key id = llDetectedKey(0);
llGiveInventory(id, name);
}
In der ersten Zeile wird geprüft, ob nur ein Gegenstand im Inventar ist (das wäre unser Skript). Ansonsten wird der nächste Gegenstand an denjenigen gegeben, der das Objekt berührt hat. Der Vollständigkeit halber schreiben wir noch in das Ereignis state_entry
llSay(0, "Berühre mich für ein Bier");
Legt nun die Flasche ins Objekt; beim Anklicken solltet ihr nun jedes Mal eine erhalten.
Wenn alle Inventargegenstände ausgegeben werden sollen (außer dem Skript natürlich), müssen wir eine for-Schleife verwenden. Ändert das Skript wie folgt:
if (llGetInventoryNumber(INVENTORY_ALL) == 1 ) llSay(0, "Schade, alles alle.");
else
{
string name;
key id = llDetectedKey(0);
integer i;
for (i = 0; i < llGetInventoryNumber(INVENTORY_ALL); i++)
{
name = llGetInventoryName(INVENTORY_ALL, i);
if (name != llGetScriptName()) llGiveInventory(id, name);
}
}
Hier gehen wir das gesamte Inventar durch und geben alles außer dem Skript aus. Mit der Funktion llGetScriptName kann man das den Namen des aktiven Skriptes ermitteln.
Das Verkaufen funktioniert etwas anders. Ein Objekt kann zwar von jedem Geld verlangen, es aber immer nur seinem Eigentümer überweisen. Bei Bezahlung wird das Ereignis money aufgerufen, welches den Schlüssel des Zahlenden sowie den Betrag übergibt. Löscht das Ereignis touch_start und schreibt statt dessen
money(key giver, integer amount)
{
llSay(0, llKey2Name( giver) + " hat soeben " + (string)amount + " L$ bezahlt.");
}
Bezahlt jetzt dem Objekt mal ein Summe, keine Angst, ihr bekommt das Geld gleich wieder, da ihr ja der Besitzer seid.
Mit llSetPayPrice kann man die Voreinstellungen des Bezahl-Fensters ändern. Das erste Argument gibt den voreingestellten Betrag an, gefolgt von einer Liste der vier Schaltflächen. Wenn dort PAY_HIDE steht, erscheint die Schaltfläche gar nicht erst. Ändert nun das Ereignis state_entry.
llSay(0, "Für 10 $ bekommst du ein Bier");
llSetPayPrice(10, [10,20,30,40]);
Nun sollten die neuen Beträge beim Bezahlen auf den Schaltflächen stehen. Wir wollen aber nur den Betrag von 10 L$ fordern, deshalb entfernen wir alle Schaltflächen bis auf eine.
llSetPayPrice(PAY_HIDE, [10, PAY_HIDE, PAY_HIDE, PAY_HIDE]);
Für das Geld sollte man natürlich auch etwas bekommen, also ändert das Ereignis money.
llGiveInventory(giver, llGetInventoryName(INVENTORY_ALL, 1));
Legt wieder die Flasche ins Objekt und testet das Skript.
Der LSLEditor von Alphons van der Heijden
Da ich immer wieder nach dem Offline-LSLEditor gefragt werde, will ich mal an dieser Stelle das Tool vorstellen und ein wenig den Umgang erläutern.
Zuerst müsst ihr es erst mal haben. Downloaden könnt ihr es hier. Ladet euch bei der Gelegenheit gleich noch die Datei LSLEditorHelp.chm von der Downloadseite herunter. Es handelt sich dabei um die englische Wiki, schön gegliedert und mit Beispielen und anderen Infos. Diese Datei kommt ins gleiche Verzeichnis wie der LSLEditor selbst.
Leider wird der LSLEditor von Alphons nicht mehr weiterentwickelt. Dafür hat sich ein Entwicklerteam entschlossen, den Code auf Sourceforge auf den neueseten Stand zu bringen. Der Editor mit dem neuesten Befehlssatz ist nun hier herunterzuladen. Nur die CHM-Datei verbleibt auf dem alten Stand.
Der Editor selbst muss nicht installiert werden, ein Doppelklick auf die EXE startet das Programm. Lediglich das .Net Framework 2.0 muss installiert sein. Nach dem Start seht ihr das bekannte Standardskript. Geht zuerst auf Tools -> Options
und dann auf Help. Hier muss "Offline Wiki" markiert werden, dann ist die CHM-Datei als Standard-Hilfe aktiviert. Mit OK kehrt ihr wieder zum Skript zurück.
Nun könnt ihr euch dem eigentlichen Skript zuwenden. Als erstes fällt auf, dass der Editor über eine Codevervollständigung verfügt, d.h. bei Schlüsselwörtern bietet er euch die möglichen Wörter an, je nachdem was ihr tippt.
Ihr könnt das entsprechende Wort aus der Liste wählen - falsche Schreibweisen sind damit kaum noch möglich. Auch die Klammern werden gleich mit erzeugt und bei Bedarf eingerückt. Bei den Funktionen werden auch die entsprechenden Parameter angezeigt.
Wenn ihr den Cursor über ein Schlüsselwort bewegt, wird eine Kurzinfo eingeblendet. Klickt ihr es an und drückt F1, wird die dazugehörige Hilfe aus der Wiki angezeigt, wenn ihr die Hilfedatei wie oben beschrieben installiert habt, ansonsten wird die Hilfeseite aus dem Internet geladen.
Unter Debug -> Syntax Checker bzw. Druck auf F6 wird das Skript auf Fehler überprüft. Sind welche vorhanden, erscheinen im unteren Bereich Meldungen oder Warnungen.
Mittels Doppelklick auf einen der Fehler könnt ihr direkt zu der Zeile springen, wo der Fehler auftrat. Warnungen können übergangen werden; sie entstehen, wenn z.B. einer Variablen im gesamten Skript kein Wert zugewiesen wurde. Um das Skript zu testen, geht ihr auf Debug -> Start oder drückt F5. Dann öffnet sich ein solches Fenster:
1 - hier wird der Ablauf des Skriptes dargestellt, also die ausgeführten Funktionen usw.
2 - das aktive State, kann ausgewählt werden
3 - die Events. Diese müssen von Hand ausgelöst werden (außer Timern und
state_entry/state_exit), einfach nur auf die entsprechenden Tasten drücken (so könnt ihr z.B. auch Timeranwendungen beschleunigen). Sind viele Events vorhanden, müsst ihr eventuell scrollen (der Scroll-Balken erscheint dann automatisch). Es sind immer die Events des gerade aktiven States zu sehen.
4 - der Ausgabebereich. Alles, was durch llSay oder llOwnwerSay ausgegeben wird, erscheint hier.
5 - die Eingabezeile. Hier könnt ihr den Chat simulieren
Im obigen Beispiel hatte ich das Event touch_start bereits ausgelöst. Beendet wird der Testmodus mit Debug -> Stop oder Shift + F5.
Es besteht auch die Möglichkeit, Plugins einzusetzen. Diese kommen in den gleichnamigen Ordner im LSLEditor-Verzeichnis. Aktivieren kann man sie unter Tools -> Options -> Plugins, aufgerufen werden sie bei den Tools. Ihr solltet auch gleich bei Debugging - Internal einen Namen und einen Key eintragen, die werden dann beim Testen verwendet.
Es sollte auch ein Häkchen für die Registrierung von LSL-Dateien gesetzt werden, damit ihr sie per Doppelklick öffnen könnt. Zu finden ist dies unter Text Editor - General.
So, das war mal ein Überblick über den Editor. Viel Spaß.
Arbeiten mit Solutions
Solutions sind komplexe Projekte, mit denen ihr das inworld-Verhalten simulieren könnt. Z.B. das Auslesen einer Notecard oder ein LM-Giver, was mit einem einzelnen Skript nicht getestet werden kann. Geht auf File -> New -> Solution, um eine neue Solution anzulegen.
Bei den Eigenschaften solltet ihr den Namen und den Solutionnamen ändern sowie das Häkchen bei "Create directory..." drinlassen bzw. reinmachen.
Bei Klick auf OK öffnet sich dieses Fenster:
1 ist das gewohnte Bearbeitungsfenster für Skripte u.ä.,
2 ist das Objektefenster, wo alles erscheint was in der Solution vorhanden ist,
und 3 das Informationsfenster, dort steht z.B. die GUID eines Objektes.
WICHTIG: Erzeugt immer als allererstes ein Objekt, sonst stürzt das Programm ab, wenn ihr etwas anderes erstellen wollt. Dazu klickt ihr im Objektefenster mit der rechten Maustaste auf den Solutionnamen und wählt "Add new Object".
So, jetzt habt ihr ein Objekt ohne Skript erzeugt, wie in SL. Dieses Objekt könnt ihr auch umbenennen. Klickt dann mit der rechten Maustaste drauf und wählt "Add new Item" -> Script.
Nun befindet sich auch ein Skript in eurer Kiste, das ihr mit Doppelklick öffnen und wie gewohnt bearbeiten könnt. Fügt noch andere Sachen hinzu, die ihr für euer Skript benötigt, und benennt sie so, wie ihr wollt.
Alles was sich in dem Objekt befindet, kann in eurem Skript genutzt werden, wie im richtigen (SL)Leben eben. Notecards könnt ihr übrigens genauso bearbeiten wie Skripte, alles andere geht noch nicht.
Ihr könnt auch mehrere Objekte erzeugen und sie per Skript kommunizieren lassen.
Das Rezzen und Linken geht per Skript leider nur theoretisch, d.h. es werden keine neuen Objekte erzeugt.
Zum Testen müssen alle Skripte in den Editor geladen werden, die gestartet werden sollen, ansonsten könnt ihr so wie bei einem einzelnen Skript vorgehen.
WICHTIG: Beim Speichern von Solutions immer "Save All" wählen, sonst wird nur das aktive Skript gespeichert und nicht die Solution selbst.
Zur Vereinfachung sollten auch die Solutions mit der Endung .SOL mit dem Editor registriert werden, damit ihr sie auch mit Doppelklick öffnen könnt. Das geht wieder über Tools -> Options:
Das war in Kürze ein Überblick über die Solutions. Viel Spaß.