Guten Abend,
da ich momentan relativ wenig zu tun habe, dachte ich mir, dass ich mal ein Tutorial zu den Basics von PAWN schreibe. ( Zu der SAMP-API wohlgemerkt ).
Ich weiß, dass es bereits einige Tutorials über dieses Thema gibt, allerdings sind viele davon nicht mehr up-to-date. Kann ja nicht schaden, ein weiteres zu schreiben.
Zuallererst ein paar Informationen über PAWN.
PAWN ist eine Script-Sprache, welche nicht nur in SA:MP verwendet wird. Einige Spiele, die auf der Source-Engine( Half-Life 1 & 2, L4D, CSS..) basieren, können auch mit dieser Sprache verwendet werden.
Am populärsten ist PAWN natürlich in der SA:MP-Szene. Dafür gibt es die SAMP-API. Wenn du also das nächste mal von PAWN hörst, kann es sich genauso gut um Counter-Strike-Source handeln.
In diesem Tutorial werde ich euch die Grundlagen der SAMP-API beibringen.
Fangen wir also an.
Um richtig loslegen zu können, brauchen wir das neueste Serverpaket. ( Zu finden auf samp.com/download.php -> SAMP 0.3z Windows Server)
Das Paket beinhaltet 7 Ordner und 6 Dateien. Fangen wir bei dem ersten Ordner an
Filterscripts. siehe n/a ( Wird unten erklärt )
Gamemodes. Hier werden die Scripts ( = Gamemodes ) gespeichert.
Includes. Hieraus werden Includes geladen. ( Siehe "Includes" )
NPCModes. Brauchen wir für sog. Bots ( Siehe "Bots" )
Pawno. Ordner mit der IDE und dem Compiler
Plugins. Ordner für Plugins
Zu den Dateien:
Announce & npc-exe. Sind für uns uninteressant. Siehe Topic.
Server.cfg. Diese Datei ist die einzige, die für uns relevant ist.
Die IDE ( engl. integrated development environment ; z.dt: Intigrierte Entwicklerumgebung ) ist der Editor, mit dem wir die Gamemodes erstellen. Die IDE in dem Serverpaket heißt "PAWNO". Es gibt noch viele weitere, allerdings reicht dieser vollkommen aus.
Da wir nun ein neues Script erstellen wollen, öffnen wir die IDE. Das ganze sieht dann folgendermaßen aus:
Das war's allerdings noch nicht, wir müssen noch "STRG+N" drücken, um die Vorlage der SAMP-API zu öffnen. Das sollte dann so aussehen:
Schon haben wir einen neuen, leeren Gamemode erstellt.
Kommen wir nun zu
2. Die SAMP-API Grundlagen
Halt. Bevor jetzt die "Pro's" fragen, warum ich nicht mit "Control Structures & Variablentypen" anfange, ganz einfach: Braucht man momentan nicht.
Funktionen (auch "natives" genannt) bilden das Grundgerüst des Gamemodes. Es sind - vereinfacht gesagt - Befehle, die der Server ausführt. Die meisten davon kann man sich selbst sehr gut herleiten, sofern man ein bisschen Englisch kann.
Es gibt verschiedene Arten von Funktionen.
-Vehiclespezifische
-Playerspezifische
-Objektspezifische
Ein Beispiel für jede Kategorie:
GetVehiclePos
GetPlayerPos
GetObjectPos
Callback heißt auf Deutsch "Rückruf". Das heißt, ein Event ( = Ereignis ) ruft ein Callback auf, damit man mit diesem weiterarbeiten kann.
Ein Beispiel wäre "OnPlayerSpawn".
Dieser Callback wird aufgerufen, wenn der Spieler spawnt.
Ein weiteres Beispiel ist "OnPlayerConnect". Dies wird aufgerufen, wenn der Spieler auf den Server connectet.
...
Wie man sieht, kann man sich die Callbacks auch sehr gut herleiten.
2.3 "Control Structures & Variablen"
Ein paar Zeilen drüber habe ich über Funktionen geredet. Ein Beispiel dazu war "GetVehiclePos". Leider ist das nur das halbe vom Ei. Es fehlen die sog. "Parameter". Diese geben an, was die Funktion wissen muss bzw. was die Funktion ausgibt.
Das ganze würde bei "GetVehiclePos" so aussehen:
GetVehiclePos(vehicleid,FLoat:x,Float:y,Float:z)
Parameter sind innherhalb der beiden Klammern zu finden & mit einem "," getrennt.
Somit ist "vehicleid" ein Parameter," Float:x",Float:y","Float:z" auch.
Diese Parameter sind verschiedene Variablentypen. Hier hätten wir z. b einen "Integer" sowie 3 "Float".
2.3.1.1 Integer
Ein Integer ist eine einfache Zahl. 1, 3, 100000 wären dafür Beispiele. Zu beachten ist, dass das Limit der Variable bei ((2^31) - 1) liegt. ( Allerdings braucht ihr nie so einen hohen Wert. Ist also nur Hintergrundwissen )
2.3.1.2 Float
Diesen Variablentypen verwendet man beispielsweise bei Positionsabfragen, HP-Abfragen etc. Eben da, wo man eine Gleitkommerzahl braucht. Ein Beispiel hierfür wäre 0.1, 135.1256 oder -1241.124
2.3.1.3 String
Ein String ist eine Zeichenkette und wohl mit dem Integer der am häufigsten auftretende Variablentyp. Ein Beispiel hierfür ist "Hans", "Ik bhin tohl" oder "Uwe".
2.3.1.4 Boolean
Booleansche Variablen haben nur 2 mögliche Werte. "true" für wahr, "false" für falsch.
Es gibt noch ein paar weitere Variablentypen ( char.. ), allerdings muss ein Anfänger nicht davon wissen.
2.3.2.1 Operatoren
Operatoren braucht man, um Bedingungen auszudrücken. Hier eine Liste mit den Opperatoren, welche mit der SA:MP API verwendet werden können.
"==" Vergleicht, ob links das gleiche wie rechts steht ( " 1 == 1 " -> wahr; " 2 == 1 " -> falsch )
"!=" Überprüft, ob links nicht das gleiche wir recht steht ( " 1 != 2 " -> wahr; " 1 != 1 " -> falsch )
">" Überprüft, ob links größer als rechts ist ( " 1 > 0 " -> wahr; " 1 > 2 " -> falsch )
"<" Überprüft, ob links kleiner als rechts ist ( " 1 < 0 " -> falsch; " 1 < 2 " -> wahr )
"<=" Überprüft, ob links kleiner gleich rechts ist ( " 1 <= 1 " -> wahr; " 1 <= 0 " -> falsch )
">=" Überprüft, ob links größer gleich rechts ist ( " 1 >= 0 " -> wahr; " 1 >= 20 " -> falsch)
2.3.2.2 "if-Statement"
Mit Hilfe eines "if-Statements" überprüft man eine Variable nach einer bestimmten Bedingung. Wenn wir Beispielsweise die Variable "A" mit dem Wert "10" haben & wir folgendes schreiben
if(A == 10)
würde das ganze eine "wahre Aussage" sein.
2.3.2.3 "else-Statement"
Was ist aber, wenn die Aussage ^falsch ist? Dafür gibt es "else", welches immer das Gegenteil der Bedingung ausdrückt.
Beispiel:
if(A == 10) {
...
}
else
{
// Wenn A alles andere als 10 ist. 10000 z.b
}
2.3.2.4 "else if - Statement"
Ist vom Prinzip her das selbe wie oben, nur dass eine weitere Bedingung an dem "else" geknüpft ist.
if(A == 10){
// Wenn A den Wert 10 hat
}
else if(A == 9 )
{
// Wenn A den Wert 9 hat
}
3. Das Erstellen erster Commands
Wir haben nun die wichtigsten Teile der Theorie durch. Natürlich gibt es noch viele viele weitere Dinge, die man wissen sollte, allerdings ist das zu viel für einen Anfänger.
Nun wird also unser erster Command erstellt. Er soll den Spieler heilen.
Um einen Befehl zu erstellen, braucht man einen "Command Prozessor". Es gibt viele verschiedene. (OCMD, ZCMD, X-BeliebigerBuchstabeAusDemAlphabet-CMD). Ich verwende meistens "ocmd".
Dieses Include ist nicht von Haus auf in dem Serverpaket enthalten. Deswegen müssen wir es downloaden und einfügen. ( DL )
Nachdem wir es nun heruntergeladen haben, öffnen wir den Includes-Ordner. ( "GTA-Ordner\pawno\include").
Ist die Datei nun erfolgreich eingefügt, müssen wir diese noch "includieren", damit wir damit arbeiten können. Dafür schreiben wir unter #include <a_samp>#include <ocmd>
Nun können wir auf die Funktionen dieses Includes zugreifen.
3.1 Der Aufbau & der erste, statische Befehl
Der Aufbau eines OCMD-Commands sieht immer wie folgt aus:
ocmd:Command(playerid,params[])
{
// Stuff
}
Der Befehl soll bewirken, dass der Spieler volle Leben bekommt. Dafür brauchen wir die richtige "native" ( = Funktion ).
In diesem Fall ist es "SetPlayerHealth".
( Kleiner Tipp: Alle Funktionen, die in "a_samp" enthalten sind, sind in der WIKI nachzulesen. )
ocmd:command(playerid,params[])
{
SetPlayerHealth
}
Das ist aber noch nicht alles. Der Server braucht noch Informationen, welchen Spieler diese Funktion betrifft & auf wie viel HP er gesetzt werden soll. ( = Parameter )
Diese sind "playerid" und "Float:health".
Wir wissen bereits, dass Parameter mit einem Komma getrennt werden.
ocmd:command(playerid,params[])
{
SetPlayerHealth(playerid,100.0)
}
Allerdings sind wie immer noch nicht fertig. Der Server muss noch wissen, wo die Funktion zu Ende ist. Das geschieht mit einem ";".
ocmd:command(playerid,params[])
{
SetPlayerHealth(playerid,100.0);
}
Fertig? Noch nicht ganz.
Es fehlt noch ein "return".
ocmd:command(playerid,params[])
{
SetPlayerHealth(playerid,100.0);
return 1;
}
Ein return stoppt das Ausführen des Scriptes bzw des Codeblocks. Es wird - in den meisten Fällen - am Ende eines Codeblocks hinzugefügt. ( Genauere Erklärungen zu "return" gibt es in diesem Thread <Link fehlt>)
Um diesen Befehl nun zu testen, müssen wir das Script compilen. ( Ihn in ein für den Server lesbares Format bringen ). Das geschieht, wenn man "F5" drückt. Da das Script noch keinen Namen hat, werden wir nun gefragt, wie wir es nennen wollen.
Ich nenne es "GTA".
Wenn das geschehen ist sollte folgendes Fenster erscheinen:
( Header Size etc wird bei euch nicht stehen. Also nicht wundern, wenn es bei euch fehlt. )
Nun müssen wir noch in der "server.cfg" den Gamemode ändern, damit das richtige Script startet.
So sieht das standardmäßig aus.
Zuallererst ändern wir das "RCON-Passwort". Dieses brauchen wir, um uns als Serverside-Admin einzuloggen.
Der Hostname ist der Name, der in der Serverliste angezeigt wird.
Uns interessiert "gamemode". "grandlarc" ändern wir zu dem Namen, unter dem wir das Scirpt abgespeichert haben. In meinem Fall "GTA".
Das ganze sollte dann so aussehen:
Wenn wir nun die Server-exe starten, sollte der Server hochgefahren werden. Um darauf spielen zu können, müssen wir ihn noch zu unserer Serverliste hinzufügen. Es reicht in den meisten Fällen, ":7777" als IP anzugeben.
Ist der Server nun hinzugefügt und hochgefahren, können wir joinen.
Ingame können wir mit /command unseren Befehl testen.
3.2 Dynamische Befehle
Wir haben gerade einen statischen Befehl erstellt. Statisch bedeutet, dass immer der selbe Wert verwendet wird, da wir ihn statisch angegeben haben.
Wenn wir nun individuelle Werte haben wollen, müssen wir das ganze dynamisch gestalten. Dafür verwenden wir Variablen.
Außerdem benutze ich noch das Include "sscanf2 ".
Nachdem wir das Include eingefügt und includiert haben, können wir starten.
Der Aufbau eines Befehls bleibt gleich.
ocmd:command(playerid,params[])
{
// Stuff
return 1;
}
Wir wollen nun einen Befehl erstellen, der dynamisch ist. D. H: Die Werte sollen individuell im Chat veränderbar sein. Es soll ein Befehl werden, mit dem wir uns ein Auto spawnen können.
Dafür müssen wir - endlich - Variablen verwenden.
Variablen werden wir folgt deklariert ( = erstellt new Variable;
Dies funktioniert allerdings nur für Integer. Um einen String zu erstellen, müssen wir noch die Länge des Strings angeben.
new String[128];
Diese String hat nun die maximale Länge von 128 Zeichen. ( = Maximale Zeichenkette, die man im Chat sehen kann )
Ein Boolean deklariert man folgendermaßen:
new bool:NAME;
Zuletzt gibt es noch "Float". Diesen Variablentyp deklariert man so:
new Float:NAME;
Als nächstest überlegen wir uns, welche Parameter wir brauchen, um ein Fahrzeug zu erstellen.
Diese wären "Vehicleid" und "Farbe1" sowie "Farbe2".
Diese Paramter sind dynamisch, das heißt sie sind individuell änderbar.
ocmd:command(playerid,params[])
{
new vID,f1,f2;
}
Tipp: Man muss nicht alle Variablen untereinander schreiben, um sie zu deklarieren. Er reicht, sie mit einem Komma zu trennen. Nach der letzten Variable darf das Semikolon allerdings nicht fehlen!
Nun kommt sscanf zum Einsatz.
Dieses Include überprüft, ob alle Parameter angegeben wurden. ( sscanf2 ist natürlich zu mehr fähig; Interessiert uns aber momentan nicht )
Dies macht man folgendermaßen:
if(sscanf(params,"iii",vID,f1,f2))return SendClientMessage(playerid,-1," Verwende: /command <ModelID> <Farbe1> <Farbe2>);
Was bedeutet das nun im Detail?
SSCANF vergleicht die vorgegebenen Parameter mit den im Chat eingegebene Paramteren( = params ).
Die 3 "i"s stehen für "integer".
(u = SpielerID /Name; s[länge] = string; f = float; i = integer -> sind die Wichtigsten. Liste )
Aber warum brauchen wir 3 Parameter mit dem Variablentypen Integer? Ganz einfach. Vehicleid ist ein integer, Farbe1 & Farbe 2 auch.
Hätten wir einen Integer, danach einen string und zuletzt ein Float, würde das ganze folgendermaßen aussehen:
if(sscanf(params,"is[20]f",NameFürInteger,NameFürString,NameFürFloat))..
Die Zahl hinter "20" ist dabei die Länge des Strings, welcher als Parameter angegeben wurde.
Kommen wir nun zu den 3 Wörtern hinter den 3 "i"'s. Das sind die Vorgaben, nach welchen "sscanf" die Eingabe überprüft.
Da wir die 3 Variablen als Integer deklariert haben, müssen wir auch 3x "i" verwenden, wie oben schon beschrieben. Zu beachten ist, dass sie die selbe Reihenfolge wie die Specifer, also die "Abkürzungen" davor haben. Sollte dort "is[20]i" stehen, muss danach auch "IntegerName,StringName,IntegerName" stehen.
Das "return SendClientMessage" wird aufgerufen, wenn ein Parameter fehlt.
Soviel zu den Paramteren.
Nun müssen wir das Fahrzeug erstellen.
Damit es an der Position des Spielers spawnt, brauchen wir noch die Koordinaten des Spielers.
Dafür deklarieren wir neue Variablen.
Diese können wir ganz einfach an den vorhandenen Variablen dranhängen.
new vID,f1,f2,Float:x,Float:y,Float:z;
Da die Position des Spielers eine Gleitkommazahl ist, brauchen wir den Typ "Float".
Die Position des Spielers bekommen wir mit "GetPlayerPos". Diese Funktion fordert eine Variable, in der die X, Y - und Z- Koordinate gespeichert werden soll.
Da wir diese erstellt haben, können wir diese ganze einfach einsetzen.
GetPlayerPos(playerid,x,y,z);
Nun können wir das Auto estellen. Die native dafür ist "CreateVehicle".
Diese Funktion fordert einige Parameter. (modelid, Float:x, Float:y, Float:z, Float:angle, color1, color2, respawn_delay)
Da die modelid dynamisch sein soll, setzen wir hier nicht einen Wert ein, sondern die Variable "vID". Diese setzt automatisch den Wert ein, den wir im Chat an erster Stelle eingegeben haben.
X,Y & Z Koordinate sind ebenfalls dynamisch, da wir diese nicht statisch deklariert haben.
Der nächste Parameter ist die Rotation ( = angle ) des Fahrzeuges. Diese bestimmt, in welche Richtung das Fahrzeug schauen soll. Ich setze einfach mal 0 ein.
Als nächstes kommen die Farben. Color1 & Color2 sind ebenfalls dynamisch, da wir sie im Chat angeben.
Zuletzt haben wir "Respawn_delay". Dieser Wert bestimmt, wie lange ein Fahrzeug ohne Fahrer an Ort und Stelle bleibt, bis es respawnt wird.
In Sekunden! Wenn es nicht respawnt werden soll, geben wir einfach "-1" an.
Wenn wir nun die Variablen einsetzen, sieht das ganze so aus:
CreateVehicle(vID,x,y,z,0,f1,f2,-1);
Der ganze Code sollte nun so aussehen:
ocmd:command(playerid,params[])
{
new vID,f1,f2,Float:x,Float:y,Float:z;
if(sscanf(params,"iii",vID,f1,f2))return SendClientMessage(playerid,-1," Verwende: /command <ModelID> <Farbe1> <Farbe2>);
CreateVehicle(vID,x,y,z,0,f1,f2,-1);
return 1;
}
Nun kann man noch einige Abfragen einbauen. Beispielsweise, ob die VehicleID überhaupt zugelassen ist. ( IDS gehen von 400 - 611 )
Dafür verwenden wir "IF" & verschiedene Operator.
if(vID < 400 || vID > 611)return SendClientMessage(playerid,-1," Falsche ID!");
Die Erklärung dafür steht weiter oben.
Nun können wir den Befehl testen. Compilen -> Server starten/neustarten -> Befehl + Paramter eingeben -> Freuen.
3.3 Weiter Verwendung von Variablen
Natürlich kann man Variablen noch für andere Zwecke verwenden.
Dazu schauen wir uns 2 weitere Bereiche der Variablen an.
3.3.1 Lokale Variablen
Lokale Variablen sind nur innerhalb eines bestimmten Bereiches gültig ( = lokal ). Dieser Bereich wird zum Beispiel durch geschweifte Klammern gekennzeichnet.
ocmd:command(playerid,params[])
{
new var,var2,Float:var3,var4[MAX_PLAYER_NAME];
if(var != var2)
{
new var5;
var 5 = var/var2;
}
else if(var >= var2)
{
new var7 = var;
}
else if(var == var 2)
{
new Float:var6;
var6 = var3;
}
return
}
Wie man sehen kann, habe ich verschiedene Variablen deklariert. Jede einzelne davon ist eine lokale, nur in diesem Command benutzbare Variable.
Var, var2,var3 und var4 kann man in dem gesamten Commandblock verwenden.
Var5,var6 & var7 sind nur innerhalb ihrer geschweiften Klammern verwendbar.
Wenn man also die ID eines Spielers speichert, um diese in einem anderen Callback zu verwenden, muss man zu "Globalen Variablen" greifen.
3.3.2 Globale Variablen
Will man nun Variablen verwenden, die man im gesamten Script aufrufen und verändern kann, muss man diese unter den Includes deklarieren.
Die Art und Weise der Deklarierung bleibt die selbe. Einzig allein der "Wirkungsradius" ist nicht mehr lokal, sondern global auf das gesamte Script erweitert.
Beispielsweise möchte ich die ID eines Spielers speichern, um diese später in einem anderen Codeblock zu verwenden.
Dafür erstellen wir eine Globale Variable.
new GlobaleVariable;
Unser Command:
ocmd:command(playerid,params[])
{
new id;
if(sscanf(params,"u",id))return SendClientMessage(playerid,-1," /command <ID>");
if(!IsConnected(id))return SendClientMessage(playerid,-1," Der Spieler ist nicht auf dem Server!");
GlobaleVariable = id;
return 1;
}
ocmd:showid(playerid,params[])
{
printf("%d",GlobaleVariable);
return 1;
}
Das würde in der Server-exe den Wert der Variable ausgeben, die wir vorher definiert haben.
( Was es mit dem "printf" aufsich hat, werde ich weiter unten erklären )
Kommen wir nun zu einem ziemlich wichtigen Themengebiet. Arrays.
Ich versuche, das ganze möglichst leicht verständlich zu erklären.
Nachdem man sich jetzt einige Zeit mit Scripten beschäftigt hat, wird man merken, dass es auf Dauer sehr unübersichtlich ist, für Alles neue Variablen zu erstellen.
Dafür gibt es verschiedene Typen von Arrays.
4.1 Eindimensionale Arrays
Das ist der häufigste auftretende Fall von Arrays.
Ein Eindimensionales Array kann man sich als Box mit vielen Schubladen vorstellen. In diese Schubladen werden Werte von Variablen gespeichert.
Die Deklarierung sieht wie folgt aus:
new ArrayName[Größe];
Mit der Vereinfachten Darstellung würde das so aussehen:
new Box[AnzahlAnSchubladen];
Um nun einen Wert in eine der Schubladen zu "legen", muss man wie folgt vorgehen:
Box[Schublade] = variable;
Wenn wir also ein Array mit dem Namen "Schrank" haben, welches mit 10 "Slots" ( = Schubladen ) gefüllt ist, und wir in die einzelnen Schubladen verschiedene Werte legen, sieht das Ganze so aus.
Schrank[0] = 10;
Schrank[1] = 20;
Schrank[3] = var;
Schrank[4] =" Hello ";
...
Schrank[10] = " World";
Ein Array kann auch so aussehen:
new Array[5] = "Raven";
oder'
new Array[5] = {'R','a','v','e','n'};
Ist exakt das selbe.
Würden wir das jetzt compilen, würde ein Error erscheinen.
array index out of bounds..
Warum kommt dieser Fehler? Wir haben den Index des Arrays gesprengt. Wir haben mehr Schubladen verwendet, als wir erstellt haben. Aber halt. Wir haben doch 10 erstellt und 10 verwendet?
Das ist richtig, allerdings brauchen wir eine Schublade für den "Null-Terminator".
Null-Terminator? Ein neuer Film von Arnold Schwarzenegger?
Als Anfänger musst du nur wissen, dass er existiert.
Um es trotzdem mal zu erklären.
Einzelne Elemente eines Arrays können nicht "leer" sein. Sollten sie keine Werte haben, sind die trotzdem mit "NULL" gefüllt. Das ist eigentlich nur für den Compiler, damit er das Ende eines Strings erkennen kann. Warum Strings? In PAWN gibt es eigentlich keine strings. Eine genauere dafür Erklärung gibt es
hier. Das Ende der Strings werden mit \0 dargestellt, da der Compiler wissen muss, wann der String zu Ende ist. Hinter dem letzten, "sichtbaren" Element ist also ein \0, der Null-Terminator. -Danke an IPrototypeI, der mir bei dieser Erklärung etwas geholfen hat.
new Array[6] = "Raven";
sieht also für den Compiler so aus:
[R][a][v][e][n][\0]
Die eckigen Klammern symbolisieren dabei die einzelnen Elemente des Arrays.
Wer sich den Expander durchgelesen hat, weiß nun, dass wir für jeden String, für jedes Array einen Null-Terminator brauchen.
Das heißt, wir erhöhen das Ganze um 1.
new Array[6]
funktioniert also ohne Errors. Man muss also immer +1 für den Null-Terminator rechnen.
Die wohl häufigste Verwendung von eindimensionalen Arrays ist wohl die Verwendung mit MAX_PLAYERS und MAX_VEHICLES.
Wozu braucht man das? Das wird dazu verwendet, um die Variablen Spielerspezifisch zu machen. Das bedeutet, dass jeder Spieler, der die Variable benutzt, einen eigenen, unabhängigen Wert hat.
Unter dem Kapitel "Verwendung von Variablen" haben wir gelernt, wie wir Integer, Floats und Strings erstellen. Diese Variablen sind alle "einmalig". Das heißt, sie werden immer wieder überschrieben. Dafür müssen wir wissen, dass der Server seine Aufgaben nicht für jeden Spieler einzeln ausführt, sondern für alle Spieler, was eigentlich klar ist, da es sonst sehr ressourcenlastig wäre. Wenn wir nun eine Globale Variable deklarieren und diese in dem Command /getID verwenden, wird diese Variable immer verändert, sobald ein Spieler auf dem Server den Befehl eingibt. Dabei kann es passieren, dass 2 Spieler den Befehl gleichzeitig eingeben. Somit ändert sich der Wert der Variable zuerst zu den Parametern, die der erste Spieler angegeben hat und danach zu den Parametern, die der 2. Spieler eingegeben hat.
-> Spieler A gibt /getID ein
-> Er bekommt den Wert "5", dieser wird in der Variable "ID" gespeichert
-> ID hat nun den Wert 5
-> Spieler B gibt /getID ein
-> Er bekommt nun den Wert "7" zurück, dieser überschreibt den Wert der Variable
-> ID hat nun den Wert 7
-> Spieler A wundert sich, warum sich der Wert der Variable geändert hat.
Dafür gibt es eben die "Spielervariablen".
Dieser werden so deklariert:
new Variable[MAX_PLAYERS];
Wie wir sehen, ist das ein eindimensionaler Array. MAX_PLAYERS spiegelt den Wert der, in der Server.cfg angegebenen Maximalen Spieleranzahl, wider. Das heißt, es wird eine Box mit dem Namen "Variable" erstellt. Diese wird mit Schubladen gefüllt. Die Anzahl dieser Schubladen ( in diesem Fall "MAX_PLAYERS" ) findet man, wie schon gesagt, in der Server.cfg. Es sind also genug Schubladen für jeden Spieler vorhanden.
Um diese nun zu verwenden, muss man folgenden Code benutzen
Array[playerid] = var;
Somit wird dem Spieler eine eigene, nur von ihm veränderbare Variable zugewiesen.
Um das oben genannte Problem zu umgehen, muss man also die Globale Variable durch ein Array ersetzen.
new ID[MAX_PLAYERS];
und in dem Command die Zuweisung folgendermaßen ändern:
ID[playerid] = pID;
Nun gibt es keine Überschneidungen mehr, da die Variablen für jeden einzelnen Spieler individuell sind.
-> Spieler A gibt /getID an
-> Bekommt als Wert "5", wird in seiner Variable gespeichert
-> Spieler B gibt /getID ein
-> Bekommt als Wert "7", wird in seiner Variable gespeichert
-> Wert von Spieler A wird nich überschrieben, da der Wert in einer anderen Schublade gespeichert wird.
Achtung! Um allen Elementen von Anfang an den selben Wert zuzuweisen, darfst du nicht den Fehler machen & folgendes schreiben:
new Array1[6] = -1;
Dies belegt nur das erste Element, die erste Schublade mit den Wert -1. Die anderen Elemente haben den Wert 0.
new Array[6] = {-1,...};
wäre richtig.
printf("%d | %d | %d ",Array1[0],Array1[1],Array1[3]);
printf("%d | %d | %d ",Array2[0],Array2[1],Array2[3]);
Gibt -1 | 0 | 0 für das erste Array und
-1 | -1 | -1 für das 2. Array aus.
4.2 "Zwei Dimensionale Arrays"
Nun gibt es noch 2 Dimensionale Arrays. Diese sind - um an unserer Metapher festzuhalten - Schränke, die mit Regalen und Schubladen bestückt sind.
Der Aufbau sieht wie folgt aus:
new Array[Zeilen][Spalten];
mit unserem Beispiel also:
new Schrank[Regal][Schublade]
Um das Ganze mal bildlich festzuhalten:
Um dieses Array zu füllen, müssen wir folgende Methode anwenden:
new Array[3][4] = {
{Wert1,Wert2,Wert3},// Zeile 0
{Wert1,Wert2,Wert3},// Zeile 1
{Wert1,Wert2,Wert3}// Zeile 2
};
In Verbindung mit unserem Bild wäre das so:
new Schrank[AnzahlRegale][AnzahlMaximaleSchubladen] = {
{5,1,6,2},//Regal 0
{5,11,135.2,4},// Regal 1
{1,1},//Regal 2
{11,135}//Regal 3
// Prinzip:
// {WertDerSchublade,WertFuerDieAndereSchublade}, Regal 0
// {WertFürSchublade,WertFürSchublade} Regal 1
};
Natürlich kann man beispielsweise auch Namen speichern:
new Array[3][5] = {
"Hans", // Zeile 0
"Otto", // Zeile 1
"Uwe" // Zeile 2
}
Wenn man keinen festen Wert hat, kann man die Zahlen auch weglassen:
new Array[][] = {
...
};
4.3 "3-Dimensionale Arrays"
Falls die ersten beiden Versionen der Arrays nicht ausreichen sollten, gibt es noch die letze Version.
Diese wird relativ häuft verwendet. Beispielsweise für ein Autohaus oder für Objekte.
Sie werden wie folgt erstellt:
new Array [Zeile][Spalten][Elemente] = {
};
Mit einem Beispiel sieht dass dann so aus:
new Name[3][3][12] = {
{"Uwe","Hans","Kai"},
{"Petra","Klaus","Manfred"},
{"Izzi","Raven","Mane"}
};
/* [0] [1] [2]
> [0]
> [1]
> [2]
*/
Hier kann man ebenfalls die Werte in den Klammern weglassen:
new Array[][][] = {
{1,"Hans",-1234,"Kai"},
{5235,"Peter",124412,"Petra"},
{0,"Niemand",-1,"Urs"}
};
So wäre der allgemeine Aufbau.
Das war's zu den Arrays. Im nachfolgendem Kapitel werde ich erklären, wie das häufig angewendet wird.
5. Enumeration ( = Enum )
Wenn man nun das eben gelernte durchsieht, wird man merken, dass es schnell unübersichtlich werden kann. Hunderte von Variablen, deren Sinn und Zweck teilweise schwer einzusehen ist. Um das Alles übersichtlicher zu gestalten kann man "Enums" verwenden.
Diese sind - vereinfacht gesagt - große Kühlschränke, in welchen man Variablen verstauen kann.
Ein Enumeration wird folgendermaßen erstellt:
enum Name
{
}
Viele verwenden dies, um Spielerdaten zwischenzuspeichern. Das werden wir auch machen. Dafür erstellen wir ein Enum mit einem beliebigen Namen. Ich nenne es p_data
enum p_data{
}
Dieser ist nun noch relativ leer. Deswegen fülle ich ihn.
enum p_data{
pName[MAX_PLAYER_NAMe],
pMoney,
pAge
}
Nun ist der "Kühlschrank" mit "Zwischenspeichern" gefüllt. Auf diese müssen wir noch irgendwie Zugriff bekommen.
Dafür erstellen wir ein 2-Dimensionales Array.
new Player[MAX_PLAYERS][p_data];
Wie man sieht, verwende ich als Zeilenangabe "MAX_PLAYERS" & als Spalte "p_data". MAX_PLAYERS wurde bereits erklärt, p_data ebenfalls.
Wenn wir nun eine Variable in diesen Kühlschrank stecken wollen, müssen wir das folgendermaßen machen:
Player[playerid][pMoney] = 100;
Der Wert ist nun in diesem "Zwischenspeicher" gespeichert.
Einige werden sich wundern, warum ich die ganze Zeit "Zwsichenspeicher" sage & nicht einfach Variable. Das liegt daran, dass das keine Variablen sind!
Wären es Variablen, könnte ich folgendes machen:
enum p_data{
pName[MAX_PLAYER_NAMe],
pMoney = 100,
pAge = 20
}
Wenn ich diese nun printen würde, müsste ja 100 & 20 als Wert rauskommen, richtig?
Der Output ist allerdings " 0 | 0 | 0 ". Damit ist bewiesen, dass "pMoney" keine Variable ist, da ich ihr sonst einen Wert zuweisen könnte. Aber was bringt das " = XYZ " dann? Damit können wir einen neuen Index zuweisen.
pMoney ist nur ein anderer Name für den Index, der in diesem Fall 1 wäre. Bei pAge wäre es 2, da es an 3. Stelle steht. Wenn wir nun " = 9" hinter einen Index schreiben, erhöhen wir ihn um den angegeben Wert. Mehr dazu weiter unten.
Eine weitere Verwendung wäre die Verbindung mit einem Multidimensionalen Array wäre folgendes Beispiel eines Haussystems.
enum h_data{
owner[MAX_PLAYER_NAME],
Float:px,
Float:py,
Float:pz,
price,
bool:owned
}
new Data[MAX_HOUSES][h_data] = {
{"Keiner",123.23,2351.12,23451.2,10000,false},
{"Raven",1251.124,12412.12,1241,2,true},
{"Kevin",135.112,12312.12,12.2,false}
};
Eine kurze Erklärung.
In dem Enum stehen die Namen der Zwischenspeicher. Diese werden in dem Array mit Werten gefüllt.
Als Zeilenangabe haben wir diesmal das vorher definierte MAX_HOUSES. Ist im Prinzip das selbe wie mit "MAX_PLAYERS". Jedes Haus die Variablen mit individuellen Werten.
Diese werden in dem Array angegeben.
Des Weiteren kann ein Enum verwendet werden, um beispielsweise Dialogen automatisch ID's zuzuweisen.
enum
{
DIALOG_1, // ID 0
DIALOG_HAS,// ID 1
DIALOG_MONEY// ID 2
...
}
Wie wir bereits wissen, können wir die ID's auch bestimmte Zahlen zuweisen. Wenn die IDS beispielsweise von 500 starten sollen, schreiben wir folgendes
enum
{
DIALOG_BUY = 500, // 500
DIALOG_SELL,// 501
DIALOG_LEVIN,//502
DIALOG_XDFS//503
}
6. Weitere Kontrollstrukturen
6.1 Switch & case
Es gibt anstelle der IF-Abfragen noch eine weitere Möglichkeit, Bedingungen auszudrücken. Eine davon ist "switch & case".
Diese ersetzen die "if- und else-if" Abfragen, sodass ein kürzere Quellcode möglich ist. Sie sind auch in bestimmten Situationen ressourcenschonender, als normale IF-Abfragen.
Sie sind wie folgt aufgebaut:
switch(Variable/Funktion)
{
case 0:
{
}
case 1:
{
}
case 2:
{
}
...
}
Als Vergleich nun der selbe Code - nur mit IF-Abfragen:
if(Variable == ZuVergleichen)// = switch
{
}
else if(Variable == zuVergleichen)// = case 0
{
}
else if(Variable == ZuVergleichen)// = case 1
{
}
else if(Variable == ZuVergleichen)// = case 2
{
}
Wie man sieht ist die vorherige Version viel kürzer.
Nun zu einem praktischen Beispiel:
switch(dialogid)
{
case 0:
{
}
case 1:
{
}
case 2:
}
Ist eine weit verbreitete Möglichkeit, IF-Abfragen bei Dialogen zu umgehen.
Anstatt den Zahlen hinter dem "case" kann man auch den Dialognamen schreiben.
case DIALOG_UNO:
...
Das ":" hinter dem "case" darf man jedoch nicht vergessen.
Wie man sieht, ersetzt jedes "case" eine IF-Abfrage. Der Wert hinter dem case bestimmt die Bedingung.
switch(myVariable)// Wert == 3
{
case 0:
{
// wird nicht aufgerufen, da der Wert 3 ist & nicht 0
}
case 1:
{
// wird nicht aufgerufen, da der Wert 3 ist & nicht 1
}
case 2:
{
// wird nicht aufgerufen, da der Wert 3 ist & nicht 2
}
case 3:
{
// wird aufgerufen, da der Wert 3 ist.
}
}
Das ganze kann man auch kürzer schreiben:
switch(myVariable)// Wert == 3
{
case 0,1,2:
{
// wird nicht aufgerufen
}
default:
{
//Wird aufgerufe
}
}
Die Werte der Bedingungen werden also mit einem "," getrennt.
Was bedeutet "default" ?
Default wird aufgerufen, wenn die Bedingungen, die hinter dem case stehen, nicht erfüllt werden.
Das benutzt man, um den Code wesentlich zu kürzen, wenn man eine riesige Masse an Zahlen hat, die geprüft werden müssen.
Ein Beispiel:
new vehicle = GetVehicleModel(carid);
switch (vehicle){
case 406,417,425,430,432,435,441,447,449,450,452,453,454,469,472,473,476,481,484,487,488,493,497,509,510,511,512,513,519,520,532,537,538,539: { return 0; }
default: { return 1; }
}
Diese Code überprüft die ModelID des Fahrzeuges und geht dabei die Zahlen durch, die hinter dem Case stehen. Sollte die Zahl der ModelID dort erscheinen, wird der Code in der Klammer dahinter ausgeführt. In diesem Fall "return 0;"
Sollte die Zahl nicht auftauchen, wird "default:" aufgerufen. Hier wird ebenfalls der Code in den Klammern ausgeführt.
Wie schon angesprochen sind switch & case ressourcenschonender als "IF-Statements". Das kann man hier gut erkennen:
if(GetValueFromSth(Sth) == Wert)
{
}
else if(GetValueFromSth(Sth) == Wert)
{
}
else if(GetValueFromSth(Sth) == Wert)
{
}
else if(GetValueFromSth(Sth) == Wert)
{
}
"GetValueFromSth" wird durch diesen Code 4x aufgerufen.
Mit switch & case sieht das ganz anders aus:
switch(GetValueFromSth(Sth))
{
case 0,1,2,4,5,6,7,8,9:
{
}
default:
{
}
}
Hier wird die Funktion nur 1x aufgerufen. Es werden also weniger Ressourcen dafür benötigt.
7.1 Format
Dem ein oder anderen mag schon aufgefallen sein, dass man mit "SendClientMessage(-ToAll)" lediglich statische Nachrichten senden kann. ( Nachrichten haben immer den selben Text )
Es gibt jedoch auch die Möglichkeit, Variablen in den Text einzubringen. Dafür müssen wir den Text formatieren. Das geschieht mit der Funktion "format"
Wenn wir nun den Spielernamen in einer Willkommens-Nachricht senden möchten,
Zuallererst erstellen wir das Grundgerüst.
public OnPlayerConnect(playerid)
{
new name[MAX_PLAYER_NAME],str[128]; // Variable, in der wir den Spielernamen speichern + string, um den Text zu formatieren
GetPlayerName(playerid,name,sizeof(name)); // Namen in der Variable "name" speichern
return 1;
}
Nun kommen wir zu "format".
Zunächst schauen wir uns die Parameter an, die da wären:
(output[], len, const format[], {Float,_}:...)
output[] = Die Variable ( String ), die formatiert werden soll. In unserem Fall "str".
len = Länge. ( Entweder direkt einen Wert angeben oder "sizeof" verwenden. )
format[] = Der formatierte Text
{Float,_}:... = "Argumente jeder Art" -> Variablen, welche wir einsetzen wollen
Unsere fertige Formatierung sieht dementsprechend so aus:
format(str,sizeof(str)," Herzlich Willkommen auf Localhost-DM, %s!", name);
format(/*stringname */, /*länge*/,/*Text + Platzhalter (%s)*/, /*Variable, die für den Platzhalter eingesetzt werden soll*/);
FORTSETZUNG
____________________________________________________________________________________________
Es ist schon relativ spät - ich hoffe, ich habe keine Flüchtigkeitsfehler gemacht^^
Rest folgt
Freue mich über Kritik/Vorschläge.
-Raven.
Edits:
1. Kapitel 3.3 hinzugefügt // 15.11.2014
[size=10]2. Kapitel 4 hinzugefügt
[size=10]3. -7. Rechtschreibfehler und sonstige Fehler ausgebessert
[size=10]8. Kapitel 5 hinzugefügt
[size=10]9. Fehler ausgebessert
[size=10]10. Formatierung geändert
[size=10]11. Edits hinzugefügt
[size=10]12. 6. & 6.1 hinzugefügt // 18.11.2014
13. Fehler ausgebessert // 18.11.2014