JASS für Einsteiger


JASS
für Einsteiger

zum
Anfang
Einleitung

Dieser Kurs wendet sich komplett an Einsteiger. Es sind KEINE
GUI-Kenntnisse nötig, da ich komplett bei 0 anfange.

Noch etwas ganz wichtiges, bevor ihr anfangt, den Kurs
durchzumachen:

Versucht AUF KEINEN FALL, GUI mit Jass zu vergleichen. Am
besten ihr vergesst alles, was ihr über das GUI wisst,
solange ihr JASS lernt.

zum
Anfang
Tools

zum
Anfang
1.1 Funktionen

Jass besteht aus
Funktionen ( engl.: function ). Eine Funktion ist ein kleiner
Teil des ganzen Scripts und enthält eine Reihe von
Anweisungen, die ausgeführt werden sollen.

Allgemein lassen sich Funktionen ziemlich gut mit
Glühbirnen vergleichen:

  • Eine Funktion hat einen Namen (z.B. Glühbirne)
  • Eine Funktion hat eine Aufgabe ( Leuchten )
  • Eine Funktion tut, solange sie nur „rumsteht“, nichts
    (wie eine Glühbirne ohne Strom^^) So kann man z.B.
    beliebig viele Funktionen in eine Map einbauen, solange man
    ihnen nicht sagt, dass sie etwas machen sollen, wird sich
    nichts ändern.
  • Man braucht eigentlich nicht zu wissen, wie eine Funktion
    arbeitet, wenn man sie nur benutzen möchte, genauso wie
    man nicht wissen muss, wie eine Glühbirne funktioniert,
    um sie in eine Lampe reinzubauen.

zum
Anfang
1.2 Der Funktionsrumpf

Eine Funktion besteht immer aus mindestens 2 Zeilen,
nämlich der Anfang und das Ende. Diesen Teil der Funktion
nennt man Funktionsrumpf.

Diese zwei Zeilen sind immer gleich aufgebaut:

function FUNKTIONSNAME takes IRGENDWAS returns IRGENDWAS
    //Hier steht der Inhalt der Funktion
endfunction

Die großgeschriebenden Teile ändern sich, die
kleingeschriebenen bleiben immer gleich. Euch wird vermutlich auch
das

    //Hier steht der Inhalt der Funktion

auffallen. Dabei handelt es sich um einen Kommentar, er wird
vom Editor einfach ignoriert. Ein Kommentar macht ihr einfach mit
// , der Rest der Zeile wird dadurch als Kommentar gewertet.

Das Wort „FUNKTIONSNAME“ muss durch einen gültigen
Funktionsnamen ersetzt werden. Funktionsnamen dürfen nicht
mit Zahlen beginnen und keine Leerzeichen enthalten.
Außerdem dürft ihr keine 2 Funktionen mit dem selben
Namen erstellen.

Die anderen beiden Teile nach „takes“ und „returns“
interessieren uns vorerst nicht. Man kann sie auch beide durch
„nothing“ ersetzen, was wir jetzt auch machen um eine leere
Funktion zu erstellen:

function MacheNichts takes nothing returns nothing
endfunction

Der Funktionsname kann frei gewählt werden. Allerdings
sollte man Funktionen nach dem benennen, was sie tun, und alle
Funktionsnamen einheitlich deutsch oder englisch machen.

zum
Anfang
1.3 Lokale Variablen

Sehr sehr sehr oft möchte man innerhalb einer Funktion
Werte temporär abspeichern. Dazu verwendet man Lokale
Variablen (Globale Variablen werden in Kurs 2 behandelt). Eine
lokale Variable existiert nur in der Funktion, in der sie
erstellt wurde, und nirgendswo sonst. Andere Funktionen wissen
nicht, was für Lokale Variablen die anderen Funktionen
haben und sie können auch nicht auf sie zugreifen.

Das Erstellen einer Lokalen Variable nennt man auch
„Deklarieren der Variable“, und die Zeile, in der die Variable
erstellt wird, ist somit die Deklaration. Eine Lokale Variable
deklariert man folgendermaßen:

local VARIABLENTYP VARIABLENNAME

Eine Liste mit allen Variablentypen findet man unter
http://www.mappedia.de/wiki/Variablentyp,
wir werden aber zu Beginn nur die so genannten nativen
Variablentypen benutzen, die da wären:

boolean Wahrheitswert, entweder true oder false
integer Ganzzahl, z.B. -7166
real Kommazahl, z.B. 3.147
string Zeichenkette z.B. „Hallo 1337!!!“

Hier ein paar Beispiele für Variablendeklarationen:

function Ex1 takes nothing returns nothing
    local integer i
    local real RealWert
    local integer a_b_c
    local string s
endfunction
 
function Ex2 takes nothing returns nothing
    local integer i = 10
    local integer j = i + 12
    local string helloWorld
endfunction

In Funktion Ex2 wurde den Variablen i und j ein Startwert
verpasst. Dazu erweitert man die Deklaration einfach etwas:

local VARIABLENTYP VARIABLENNAME = STARTWERT

In Zeile 2 ist als Startwert: i + 12 angegeben. Dadurch wird
der Wert, der in i gespeichert ist, um 12 erhöht und
anschließend in j gespeichert. j hat hier also den Startwert
10 + 12 = 22.

Setzen von Variablen

Ähnlich wie
das Deklarieren funktioniert auch das Setzen der Variablen:

set VARIABLENNAME = WERT

Auch hier wieder ein paar Beispiele:

function Ex3 takes nothing returns nothing
    local string text1 = "Hallo"
    local string text2 = " Welt"
    local integer i
 
    set i = 4
    set i = i + 1
 
    set text1 = text1 + text2 + "!"
endfunction

Zu Beginn werden 3 Variablen deklariert: text1, text2 und i.
Die nächsten 2 Zeilen setzen die Variable i zuerst auf 4 und
inkrementieren (erhöhen) den Wert dann um 1. Die letzte Zeile
hängt text1, text2 und ein „!“ zusammen und speichert das
Ergebnis in text1.

zum
Anfang
1.4 Funktionsaufrufe

Am Anfang des Tutorials habe ich geschrieben, dass eine
Funktion erst etwas tut, wenn man ihr sagt, dass sie es tun
soll. Dieses „der Funktion sagen“ nennt man in Jass (und auch
in anderen Programmiersprachen) Funktionsaufruf.

Es gibt 2 Möglichkeiten, eine Funktion aufzurufen: Mit und
ohne „call“. Wir werden uns zuerst die Methode mit call
ansehen.

function FuncB takes nothing returns nothing
    local integer i = 10
endfunction
 
function ZZ takes nothing returns nothing
    call FuncB()
endfunction
 
function FuncA takes nothing returns nothing
    local real var
 
    call FuncB()
    set var = 10
    call ZZ()
endfunction

Gut, was passiert hier?

Wir starten bei „FuncA“. Es wird eine lokale Variale var vom
Typ real erzeugt. Anschließend wird FuncB aufgerufen.
Wenn eine Funktion aufgerufen wird, so springt der „Zeiger“ auf
die Zeile, die gerade ausgeführt wird, in die aufgerufene
Funktion. Die Funktion, von der aus aufgerufen wurde, ist so
lange pausiert.

Es wird Zeile für Zeile durchgearbeitet (hier ist es nur
eine) und sobald das Ende der Funktion erreicht ist, springt
der Zeiger wieder zurück in die Ursprüngliche
Fuktion, FuncA. Nun wird die Zeile nach dem Funktionsaufruf
ausgeführt, set var = 10.

Danach wird ZZ aufgerufen. In ZZ wird wieder FuncB aufgerufen,
die beiden anderen Funktion sind so lange pausiert. FuncB wird
wieder ausgeführt, der Zeiger springt zuerst wieder
zurück zu ZZ und dann zu FuncA. Nun ist auch dort die
Funktion zu Ende und der Script fertig ausgeführt.

Auffällig sind hier die () nach dem Namen der Funktion.
Dort werden später die Parameter stehen, aber dazu mehr in
1.6.

Einige von euch werden sich jetzt vermutlich auch wundern:
„Aber eine Funktion tut doch nichts so lange man es ihr nicht
sagt. Woher weiß denn der Editor, dass FuncA zuerst
ausgeführt werden soll?!“. Die Frage ist auf jeden Fall
berechtigt, allerdings müsst ihr euch vorerst einfach
damit abfinden, dass wir einfach einen „Startpunkt“ bei einer
Funktion setzen.

Wie dieser Startpunkt bestimmt wird, erfahrt ihr noch, aber
dafür wäre es jetzt zu früh.

zum
Anfang
1.5 Rückgabewerte

Wie gesagt gibt es zwei Möglichkeiten, eine Funktion
aufzurufen: Mit und ohne call. Der Unterschied ist, dass beim
Aufruf mit call der Rückgabewert verloren geht. Aber
zuerst: Was ist überhaupt der Rückgabewert?

Funktionen können, grob gesagt, 3 verschiedene Aufgaben
übernehmen:

  • Etwas berechnen (Zum Beispiel die Summe zweier
    Zahlen)
  • Etwas tun (Zum Beispiel eine Einheit erstellen)
  • Etwas tun und etwas berechnen (Zum Beispiel eine Einheit
    Physikalisch korrekt fliegen lassen und nebenbei die neue
    Position berechnen)

Der Rückgabewert ist bei Möglichkeit 1 und 3
wichtig. Er ist das Ergebnis der Berechnung, die die Funktion
vornimmt. Dazu muss man zunächst festlegen, von welchem
Variablentyp denn das Ergebnis ist, was man im Funktionsrumpf
tut.

Wir erinnern uns:

function FUNKTIONSNAME takes IRGENDWAS returns IRGENDWAS

Na, wo könnte wohl der Rückgabewert
hingehören? Er ist das zweite „IRGENDWAS“. Als
Rückgabewert kann man jeden beliebigen Variablentyp, z.B.
boolean verwenden. Man ersetzt dann einfach das nothing von bisher
mit dem Variablentyp.

function Ex5 takes nothing returns boolean
endfunction

Mit dem Festlegen des Typs ist es aber nicht getan, denn
bisher ist ja nur klar, welchen Typ das Ergebnis hat. Welcher Wert
es aber nun genau ist, gibt man mit dem Schlüsselwort „return“
(ohne s) an:

function Ex6 takes nothing returns real
    local real x = 5.00
    return x
endfunction
 
function Startpunkt takes nothing returns nothing
    local real y
 
    set y = Ex6()
endfunction

Gut, jetzt im Detail was passiert:

Wir Starten bei Funktion Startpunkt, deklarieren eine lokale
Real-Variable namens y. Anschließend wird y auf den
Rückgabewert von Funktion Ex6 gesetzt, heißt also im
Detail:

-> Springe zu Funktion Ex6

-> Führe Funktion aus, bis ein „return“ kommt

-> nehme den Wert nach dem „return“ und gehe damit
zurück zur aufrufenden Funktion Startpunkt

-> Setze den Wert für y ein.

Nach ausführung hätte also y den Wert 5.00.

Bis jetzt bringt uns all dies allerdings noch recht wenig, da
die Werte ja immer gleich sind. Mit Parametern kommt ein wenig
Variation ins Spiel

zum
Anfang
1.6 Parameter

Jetzt wird geklärt was es mit dem zweiten IRGENDWAS in
unserem Funktionsrumpf auf sich hat. Das ist die Stelle, an die
die Parameter der Funktion kommen.

Was Parameter sind? Nun, der Satzbau der Funktionsdeklaration
sagt es eigentlich schon:

Funktion XY nimmt irgendwas und gibt irgendwas zurück

Parameter sind Werte, die eine Funktion nimmt. Diese kann man
bei jedem Funktionsaufruf ändern und die Funktion kann
dadurch komplett unterschiedliche Dinge tun, je nach dem,
welche Werte als Parameter übergeben werden.

Ein Parameter selbst ist fast das selbe wie eine Lokale
Variable, er hat einen Typ und einen Namen und kann auch
genauso wie eine Lokale Variable gesetzt und verwendet werden.
Die Parameterdeklaration sieht somit genauso wie die einer
Lokalen Variable aus, nur ohne das „local“- Schlüsselwort,
und dass alle in den Funktionsrumpf nach das „takes“, durch
Kommas getrennt, gequetscht werden:

function Ex7 takes integer i returns nothing
endfunction
 
function Ex8 takes boolean hallo, integer i, integer other_int, real z, boolean b, string s returns real
    local integer local_int = i + other_int
    return z
endfunction

So werden also Funktionen mit Parametern deklariert, aber wie
übergibt man nun die Parameter?

Bisher sahen unsere Funktionsaufrufe so aus:

call FUNKTIONSNAME()

oder

set VARIABLE = FUNKTIONSNAME()

so sehen sie auch aus, wenn die aufzurufende Funktion keine
Parameter, also nothing, hat. Mit Parameter sieht sie so aus:

call FUNKTIONSNAME( WERT DEN PARAMETER 1 HABEN SOLL, WERT DEN PARAMETER 2 HABEN SOLL, ... )

Wir setzen also, durch Kommata getrennt, alle Werte ein, die
für die Parameter verwendet werden sollen. Und zwar in der
Reihenfolge, in der sie deklariert wurden.

Hier ein Beispiel.

function Ex9 takes integer i1, integer i2 returns integer
    return i1 + i2
endfunction
 
function Startpunkt takes nothing returns nothing
    local integer result = Ex9(7,12)
    set result = result + Ex9(1,1)
endfunction

Wir starten bei Startpunkt. Von dort wird Ex9 aufgerufen, mit
den Werten 7 und 12. Diese werden für die Parameter von Ex9
eingesetzt, i1 und i2. Es folgt die Zeile „return i1 + 12“, es wird
also 7 und 12 zusammengezählt und zurückgegeben. Somit
ist in Variable result aus Startpunkt jetzt 19 gespeichert. Die
Nächste Zeile ist eigentlich genau das gleiche, das Ergebnis
von vorher wird mit 1 + 1 zusammengezählt und wieder
abgespeichert. Am Ende ist in result der Wert 21 gespeichert.

zum
Anfang
1.7 If-Abfragen

Bis jetzt wurden die Anweisungen in unseren Funktionen immer
knallhart Zeile für Zeile durchgearbeitet. Manchmal
möchte man aber auch einige Anweisungen nur ausführen
lassen, wenn eine Bedingung erfüllt bzw. nicht
erfüllt ist.

Dazu gibt es die If-Abfragen.

Eine If-Abfrage sieht so aus:

if ( Bedingung ) then
    //Aktionen
else
    //Aktionen
endif

Ist die Bedingung nach dem if erfüllt, so wird der Teil
nach dem then ausgeführt, wenn nicht, wird der Teil nach dem
else bis zum endif ausgeführt. Den else-Teil kann man auch
weglassen, dann wird, falls die Bedinungen nicht erfüllt ist,
einfach nichts gemacht.

Die Abfrage sieht dann so aus:

if ( Bedingung ) then
    //Aktionen
endif

Als Bedingung ist alles gültig, was einen Boolean-Wert
(true oder false) ergibt. Das können:

  • a) Ein Vergleich z.B. if (
    a > b ) then
  • b) Eine Variable vom Typ
    Boolean z.B. if ( var ) then
  • c) Eine Funktion mit
    Rückgabewert Boolean z.B. if ( MyFunc() ) then

sein.

Meistens benutzt man in einer Bedingung einen Vergleich. Um
sowas bauen zu können, muss man aber erstmal die 6 so
genannten Vergleichsoperatoren kennen:

== „Ist Gleich“
!= „Ist Ungleich“
> „Ist Größer als“
< „Ist kleiner als“
>= „Ist größer oder gleich“
<= „Ist kleiner oder gleich“

Die letzten 4 Vergleichsoperatoren können
logischerweise nur beim Vergleichen von 2 Integern oder Reals
verwendet werden, denn man kann ja schlecht vergleichen lassen
ob denn nun z.B. ein Wahrheitswert größer als ein
anderer ist. Man beachte, dass das Gleichheitszeichen NICHT „=“
sondern „==“ lautet. Dabei handelt es sich um einen der
beliebtesten Fehler, der auch erfahrenen Jass’ern
desöfteren passiert.

if ( 5 = 5 ) then
endif

ist also falsch, es müsste heißen:

if ( 5 == 5 ) then
endif

zum
Anfang
1.8. Loops

Loops (deutsch: Schleife) sind nützlich, wenn man
ähnliche Anweisungen mehrmals ausführen lassen will.
Die Funktionsweise eines Loops ist eigentlich ganz simpel:
Alles, was sich zwischen den beiden Zeilen

loop

und

endloop

befindet, wird UNENDLICH oft wiederholt. Hier ein Beispiel.

function Ex10 takes nothing returns nothing
 
loop
endloop
 
endfunction

Wird die Funktion ausgeführt, so wird dine so genannte
Endlosschleife gestartet, denn sie hört ja nie auf. Hätte
Warcraft keinen eingebauten Schutzmechanismus, würde es einen
Absturz verursachen.

Ihr merkt wahrscheinlich schon, dass das nicht der
gewünschte Effekt eines Loops ist. Deswegen gibt es das
Sprachelement „exitwhen BEDINGUNG“ Wird die Zeile exitwhen
BEDINGUNG ausgeführt, dann wird geschaut ob die Bedingung
true ergibt. Falls ja, wird die Schleife beendet und es wird
die Zeile nach dem endloop ausgeführt. Die Bedingung nach
dem exitwhen ist so eine, wie man sie auch in einem If
verwendet.

Hier ein Beispiel zum sinnvollen Benutzen einer Schleife:

function Ex11 takes nothing returns nothing
    local integer i = 1
    loop
        //Aktionen
        set i = i + 1
        exitwhen i > 10
    endloop
endfunction

Wird die Funktion ausgeführt, so wiederholt sich die
Schleife 10 mal. Jedes mal wird Integer i um 1 erhöht und ist
irgendwann bei 11. Dann ist die Bedingung nach dem exitwhen true,
denn 11 > 10 und die Schleife wird verlassen.

zum
Anfang
1.9 Übungen

Übung 1

Erstelle eine Funktion
„IstKleinerAls“, die als Parameter 2 Integer übernimmt und
Rückgabewert boolean hat. Ist der erste Parameter kleiner
als der Zweite, so wird true zurückgegeben, ansonsten
false.

Übung 2

Baue die Funktion so um,
dass sie das genau Gegenteil bewirkt. Wenn Parameter 1 also
größer ist als Parameter 2, soll true rauskommen,
ansonsten false. Was wäre ein sinnvoller Name für die
Funktion?

Übung 3

Erstelle eine Funktion
„Minimum“, die wieder 2 Integer als Parameter hat. Diesmal soll
allerdings der Rückgabewert Integer sein und den kleineren
der beiden Parameter zurückgeben.

Übung 4

Erstelle die Funktion
„MinMax“. Sie hat 2 Parameter vom Typ Integer und einen vom Typ
boolean. Ist der Boolean-Parameter true, soll das Minimum (wie
in Übung 2) ermittelt werden, anderenfalls das Maximum
(der größere der beiden Werte)

Übung 5

Erstelle die Funktion
„Betrag“, die den Betrag eines Reals zurückgibt, der
Parameter ist. Wer es nicht weiß: Der Betrag einer Zahl
ist der Abstand der Zahl von 0, d.h. der Betrag von -5 ist 5,
der von 18 ist 18.


Musterlösungen zu den Übungen

zum
Anfang
Kapitel II

Dieses zweite Kapitel baut komplett auf das Erste auf. Wir
lernen, wie man Warcraft-Spezifische Sachen macht, z.B. Units
erstellt, wie man Globale Variablen erstellt und was für
Variablentypen es in JASS gibt. Außerdem gehe ich sehr
ausführlich auf Memory Leaks ein, und wir erfahren etwas
über die besondere Funktion GetLocalPlayer(), die sehr
nützlich sein kann.

Dazu gibt es noch ein ganzes Paket Übungen, mit denen ihr
das Wissen verinnerlichen könnt.

zum
Anfang
2.1 Native Funktionen

Bis jetzt konnten wir mit unseren Funktionen nur eigene
Funktionen aufrufen und somit eigentlich auch nur einfache
Sachen berechnen. Jetzt wollen wir aber Sachen machen, die
direkt mit Warcraft zu tun haben.

Um das zu ermöglichen, hat Blizzard uns JASSern eine ganze
Menge an „Nativen Funktionen“ zur Verfügung gestellt.
Diese Funktionen befinden sich in den beiden Dateien „common.j“
und „blizzard.j“ im Warcraft Ordner.

Die Funktionen dort kann man genauso aufrufen, wie die eigenen.
Unterschied ist nur, dass man nicht direkt sehen kann, was
GENAU die aufgerufene Funktion tut, man kann aber anhand von
Name, Parameter und Rückgabewert erkennen, welchen Effekt
der Aufruf haben wird.

Wie wir aus Kapitel 1 wissen:

„Man muss nicht genau wissen, was eine Funktion tut, man muss
nur wissen, wie man sie benutzt“

„Genau wie man nicht wissen muss, wie eine Glühbirne
funktioniert, um sie in eine Lampe reinzubauen.“

Genug gelabert, nun zur Praxis. Schaut euch diese Funktion an:

native AttachSoundToUnit takes sound soundHandle, unit whichUnit returns nothing

Auffallend ist, dass das „function“ durch „native“ ersetzt
wurde und dass „endfunction“. Das soll euch aber nicht stören,
es bedeutet einfach nur dass es sich hierbei um eine native
Funktion handelt.

Außerdem fehlt der ganze Funktionsinhalt. Den kann man
sich auch nicht ansehen, da der Effekt der Funktion von
Warcraft selber mit C++ gemacht wurde.

Was die Funktion tut, liest man aus dem Funktionsnamen. Der
lautet „AttachSoundToUnit“, also ein Sound wird einer Einheit
angehängt. Was für Parameter man übergeben muss,
steht dort auch:

sound soundHandle
unit  whichUnit

sound und unit sind die Variablentypen, soundHandle und
whichUnit sind die Parameternamen. Jeder der halbwegs logisch
denken kann, wird davon jetzt erkennen können, was das ganze
wohl zu bedeuten hat^^

Die Funktion ruft man nun genauso auf wie eigene Funktionen:

function Startpunkt takes nothing returns nothing
    local sound EinSound
    local unit DieEinheit
    call AttachSoundToUnit( EinSound, DieEinheit)
endfunction

Alles klar? Tja, dann ist nur noch die Frage, wo der
pöhse Ersteller dieses Tutorial denn nun diese Funktion her
hat! Darauf gibt es jetzt eine Antwort:

Benutzung der Native List

Der
pöhse Ersteller hat die Native List von JassCraft benutzt!
Dort sind alle Funktionen und Variablentypen aufgelistet und es
gibt eine schöne Suchfunktion. Möchte man zum
Beispiel ein Multiboard erstellen, gibt man bei „Search“ ein:

multiboard

Nun kann man sich die richtige Funktion raussuchen und
benutzen. Das ganze mag anfangs etwas mühselig erscheinen,
aber mit der Zeit behält man immer mehr Funktionen im Kopf und
muss nur noch selten etwas nachschlagen. Übung macht den
Meister!

zum
Anfang
2.2 Globale Variablen

In Tutorial 1 haben wir lokale Variablen kennen gelernt. Diese
sind in einer Funktion gültig und können
außerhalb nicht verwendet werden.

Was ist aber nun, wenn man etwas Mapübergreifend speichern
möchte?

Dafür gibt es Globale Variablen. Sie können in allen
Funktionen und überall verwendet werden. Ansonsten
funktionieren sie eigentlich genauso wie lokale Variablen.

Die Deklaration einer globalen Variable ist im normalen WE nur
über das GUI möglich. Man geht dazu im Trigger Editor
oben auf das X und erstellt die Variable.

Aber Achtung: Um Überschneidungen zu vermeiden, bekommen
diese Variablen automatisch ein Präfix, das „udg_“. Wenn
man also eine Variable namens „Hallo“ erstellt, so heißt
sie in Wirklichkeit:

udg_Hallo

Die andere Möglichkeit, globale Variablen zu erstellen,
ist deutlich eleganter. Wenn man den JNGP-Editor benutzt, herrscht
so genannte „Globale Declaration Freedom“.

Das bedeutet, man kann überall im ganzen Script Globale
Variablen deklarieren.

Man benutzt dazu einen „Globals-Block“, der sieht so aus:

globals
    integer HALLO
    boolean Global_Bool = true
    real z
endglobals

Der Block beginnt immer mit „globals“ und endet mit
„endglobals“. Danach kann man so viele Globale Variablen
deklarieren, wie man möchte. Man kann auch beliebig viele
dieser Blöcke erstellen.

zum
Anfang
2.3 Arrays

Mal angenommen, man möchte für jeden Spieler global
(in einer 12-Spieler-Map) einen Wert speichern, z.B. die Kills.
Mit unseren bisherigen Methoden wäre das nur so
möglich:

globals
    integer Kills_1
    integer Kills_2
    integer Kills_3
    ...
endglobals

Man bräuchte also 12 Variablen, und wenn man etwas
für einen Spieler speichern möchte immer 12 If-Abfragen,
um die richtige Variable auszuwählen. Ziemlich
umständlich, was?

Dafür gibt es Arrays. Man kann jede Variable zu einem
Array machen. Ein Array sind immer ganz viele Variablen des
gewünschten Typs, die unter einem Namen zusammengefasst
sind. Um auf die einzelnen Variablen zuzugreifen, benutzt man
den Array-Index. Am einfachsten erklärt sich das an einem
Beispiel.

globals
    integer array Kills
endglobals
 
function EineFunktion takes nothing returns integer
    return 17
endfunction
 
function Startpunkt takes nothing returns nothing
    local integer i = 10
    set Kills[1] = 5
    set Kills[2+1] = 19
    set Kills[i] = i
    set Kills[EineFunktion()-10] = 7
endfunction

Erklärung:

Im Globals-Block ganz oben wird ein globales Integer Array
namens „Kills“ erstellt. Das sind also jede Menge (genau
genommen 8192) Integer-Variablen, die unter dem Namen „Kills“
zusammengefasst sind. In Funktion Startpunkt wird dann
zunächst eine lokale Integervariable erstellt, und danach
wird Kills[1] auf 5 gesetzt. Das heißt also, wir setzen
die erste (das ist die Zahl in den eckigen Klammern) der 8192
Integer-Variablen auf den Wert 5. Das gleiche passiert in den
Zeilen danach, sie zeigen, dass man als Index auch eine
Variable oder eine Funktion mit Rückgabewert Integer
verwenden kann. Wir hätten nach der Ausführung also
folgende Werte in Kills gespeichert:

        [1] = 5
        [2] = 0
        [3] = 19
        [4] = 0
Kills   [5] = 0
        [6] = 0
        [7] = 7
        [8] = 0
        [9] = 0
        [10] = 10
        ...
        ...
        ...
        usw

Wenn man jetzt also z.B. auf die Kills von Spieler 1
zugreifen will, greift man auf Kills[1] zu, bei Spieler 7 auf
Kills[7] usw.

zum
Anfang
2.4 Variablentypen

Bisher haben wir uns (offiziell^^) nur mit nativen
Variablentypen beschäftigt. Bei Nativen Funktionen wurden
aber auch schon die Variablentypen unit und sound
angeschnitten, und es gibt noch vieeeel mehr.

Diese Variablentypen sind in verschiedene Ober- und Untertypen
unterteilt. Neben den nativen Variablentypen, die wir bisher
kennen gelernt haben, gibt es noch einen nativen typ, namens

handle (deutsch: Objekt)

Alle anderen Variablentypen z.B. sound sind Untertypen dieses
Typs, denn ein Sound ist ja auch ein Objekt. Manche Untertypen von
Handle haben noch weitere Untertypen, z.B. der Typ widget. Widgets
sind alle Objekte, die man im Spiel sehen kann und die Lebenspunkte
haben. Dazu gehören:

  • Einheiten
  • Zerstörbare Objekte (z.B. Bäume)
  • Items

Den Obertyp eines Variablentyps kann man sich in der
Native List von JassCraft ansehen. Klickt man rechts unten auf
„Show Options“ und lässt sich nur noch „Types“ anzeigen,
so kann man sich alle Typen ansehen.

unit (Einheit) sieht zum Beispiel so aus:

type unit               extends     widget  // a single unit reference

„type“ leitet eine Typendeklaration ein Darauf folgt der Name
des Typs (unit) Anschließend folgt „extends TYP“, also
„erweitert TYP“, und dieser TYP ist der Obertyp des deklarierten
Variablentyps.

Man kann übrigens keine eigenen Variablentypen deklarieren
(nur in vJass). Das Nützliche an Ober und Untertypen ist,
dass man, falls eine Funktion z.B. einen Parameter

... takes handle h ...

hat, man JEDEN BELIEBIGEN Untertyp von Handle angeben kann.
Also eine Unit, einen Punkt, einen Timer… Nun noch ein paar
Besonderheiten der Variablentypen in Jass:

  • Die Typen ability, unittype, itemtype sind nicht das was
    man glaubt. Einheiten-,Fähigkeiten und Itemtypen werden
    im JASS als Integer angegeben. Diese Integer nennt man
    „Rawcodes“ und man kann sie sich im Objekt Editor anzeigen
    lassen, wenn man auf Ansicht->Werte als Rohdaten anzeigen
    klickt Beispielsweise muss man bei CreateUnit() als zweiten
    Parameter einen integer unitid übergeben. Möchte
    man einen Menschen Footman erstellen, muss man hier ‚hfoo‘
    angeben. (In ‚ ‚ )
  • Für GUI-ler: unitgroup und playergroup gibt es
    nicht. Die unitgroup heißt einfach group, Die
    playergroup heißt force.
  • Es gibt einen Variablentyp, von dem man kein Array
    erstellen kann: code (dazu mehr später)

zum
Anfang
2.5 Konstanten

Mal angenommen, ihr wollt ein Sytem machen, dass einheiten in
einer Wurfparabel-Bahn durch die Luft fliegen lässt. Dazu
werdet ihr ein paar mal die Zahl 9.81 brauchen, die
Erdanziehungskraft g.

Jetzt könnte man diese Zahl natürlich einfach immer
benutzen, wenn man sie braucht. Was ist jetzt aber, wenn man
das System veröffentlicht, und der Benutzer möchte
eine Mondmap machen? Dann muss er das ganze System nach jedem
Vorkommen von 9.81 durchsuchen und die Zahl ändern. Recht
mühsam. Besser ist, das ganze in eine globale Variable zu
speichern:

globals
    real Gravity = 9.81
endglobals
 
//Hier ist das System

Möchte der Benutzer des Systems jetzt die Gravitation
ändern, muss er nur einmal das am Anfang machen. Allerdings
könnte es ja jetzt sein, dass der Benutzer mal versehentlich
den Wert von Gravity ändert, und so das ganze System kaputt
macht. (Nun gut, ist eher unwahrscheinlich in diesem Fall, aber mir
fällt leider kein gutes Beispiel ein) Dafür gibt es das
Schlüsselwort constant (deutsch.: konstant, gleichbleibend)
Man kann es vor Funktionen oder Variablendeklaration setzen, um die
Variable oder Funktion als immer gleichbleibend zu kennzeichnen.
Ein Beispiel:

globals
    constant real Gravity = 9.81
endglobals
 
function Startpunkt takes nothing returns nothing
    local real r = Gravity //Geht
    set Gravity = 10.00    //Error!
endfunction

Die Kommentare sagen eigentlich alles, sobald man versucht,
den Wert einer Konstanten zu ändern, bekommt man einen Error
hingeklatscht. Merken: Wenn man einen Werte niemals während
der Laufzeit ändern will, deklariert man ihn als constant.

zum
Anfang
2.6 Memory Leaks

Memory Leaks (detusch.: Speicherlecks) – die meisten Mapper
wissen wie man sie behebt aber nur die wenigsten wissen, was
sie eigentlich WIRKLICH GENAU sind. Deswegen werde ich auf
dieses Thema sehr genau und ausführlich eingehen, da
Speicherlecks sehr schädlich sein können und man
immer im Einzelfall unterscheiden muss, was denn nun einen Leak
erzeugt – und was nicht.

Zur Theorie: Wenn man ein Objekt erzeugt, werden die Daten in
den RAM gespeichert. Man kann über Variablen auf die
Objekte zugreifen und die Daten abfragen und verändern.
Wenn man ein Objekt nicht mehr braucht und nicht löscht,
so bleibt es RAM drinnen, und gammelt da, solange bis man
Warcraft beendet. Diese Daten verbrauchen den Speicherplatz im
RAM. Wenn man zu viele Objekte erstellt und nicht wieder
löscht, so wird der RAM immer voller und voller… und
irgendwann führt das zu ruckeligem Spiel und Lags.

Ich rede hier immer von „Objekten“. Genauer gesagt sind es aber
nur bestimmte Objekte, die leaken.

Merken:

Und zwar leaken alle Variablentypen, die Untertyp von Handle
sind, aber nicht player und nicht die Widgets. Wenn man ein
Leakendes Objekt erstellt und nicht per Jass-Befehl entfernen
lässt, so entsteht ein Memory Leak.

FAQ:

Frage: Warum leaken die nativen Typen, also integer, real,
string und boolean nicht?

Antwort: Warcraft löscht Werte dieser Typen automatisch.

Frage: Warum leaken player, unit, destructable und item nicht?

Antwort: Indirekt leaken diese Typen schon. Wenn man eine
Einheit erstellt, dann braucht sie ja auch Platz im RAM, und
wenn man zu viele erstellt, ruckelt es. Allerdings kann man sie
ja auf der Karte sehen und wenn man sie beseitigen würde,
müssten man ja auch die Einheit auf der Karte beseitigen.
Und Einheiten auf der Karte sind normalerweise gewollt, daher
bezeichnet man sie nicht als Memory Leaks.

Nun, genug Theorie, hier eine Funktion die einen Memory Leak
erzeugt:

function ExampleLeak takes nothing returns nothing
    local group g = CreateGroup()
endfunction

Jedes Mal, wenn man diese Funktion aufruft, wird eine Gruppe
erstellt und in die Variable g gespeichert. Naja, wird nicht
gespeichert, um das zu verstehen muss man etwas tiefer einsteigen:
Wenn man macht

local group g = CreateGroup()

Wird eine Gruppe erstellt und im Ram gespeichert. In die
Variable wird nicht die Gruppe selber, sondern ein Zeiger auf die
Gruppe gespeichert. Der Zeiger zeigt da hin, wo die Gruppe
gespeichert ist. Nehmen wir einfach mal an, die Gruppe wird im RAM
in das Feld 1 gespeichert. Dann steht in Feld 1 die Gruppe, also
das Objekt selber, und in der Variable g steht 1. Benutzt man nun
die Variable g, so schaut Warcraft in Feld 1, und findet die
Gruppe. Macht man jetzt zum Beispiel folgendes:

set g = null

Ist dann die Gruppe weg? Nein, ist sie nicht. Denn jetzt ist
noch folgendes da:

-In Feld 1 steht weiterhin die Gruppe

-In Variable g steht nicht mehr die Adresse der Gruppe sondern
0. Und somit haben wir einen Memory Leak. Wir haben eine Gruppe
irgendwo im Speicher, aber wissen nicht wo. Es ist, als
würde man sich die Adresse eines Hauses auf einen Zettel
schreiben, und dann den Zettel wegwerfen.

Der Zettel ist dann weg, aber das Haus steht noch! Und wir
wissen nicht wo. Um „das Haus abzureißen“ benutzt man die
Remove oder Destroy Funktionen. Die gibt es für fast jeden
Variablentyp, z.B. für group: DestroyGroup()

Um unsere Funktion leakfrei zu machen, tun wir nun also
folgendes:

function ExampleLeak takes nothing returns nothing
    local group g = CreateGroup()
    call DestroyGroup(g)
endfunction

Wir nehmen also die Gruppe, auf die die Variable g zeigt, und
zerstören sie. Dadurch ist jetzt noch ein kleiner Leak
übrig… der Zettel mit der Adresse ist ja noch da!

Also setzen wird die Variable noch gleich null:

function ExampleLeak takes nothing returns nothing
    local group g = CreateGroup()
    call DestroyGroup(g)
    set g = null
endfunction

Jetzt ist die Gruppe zerstört und die Adresse
vernichtet, und es kein Leak übrig.

Versucht jetzt aber nicht, knallhart jedes Objekt immer und
überall zu vernichten. Seht Memory Leak-Beseitigung als
dass was es ist – Speicherverwaltung. Denn ob ein Memory Leak
ein Memory Leak ist, ergibt sich daraus, wie man es verwendet.
So kann man Beispielsweise Gruppen das Ganze Spiel lang im
Speicher behalten, um z.B. alle Einheiten eines Spielers darin
zu speichern. Die verbrauchen auch Speicher, zählen aber
nicht als Memory Leaks, da sie ja erwünscht sind.

zum
Anfang
2.7 Die Funktion
GetLocalPlayer()

constant native GetLocalPlayer      takes nothing returns player

So einfach sieht sie aus – die Funktion, der hier ein ganzes
Kapitel gewidmet wird. Tatsächlich hat GetLocalPlayer aber
sehr viel Power – und auch viel Zerstörungskraft. ^^ Die
Besonderheit von GetLocalPlayer ist, dass sie asynchron
ausgeführt wird, also im Multiplayer bei jedem Spieler anders.
Und zwar ist der Rückgabewert von GetLocalPlayer an dem PC
jedes Spielers man selber.

Am PC von Spieler 1 ist GetLocalPlayer also Spieler 1, bei
Spieler 7 ist der Rückgabewert Spieler 7 usw.

Das mag anfangs etwas verwirrend klingen, ist aber eigentlich
nicht so komplex.

Gut – was kann man damit machen?

Die häufigste Anwendung ist es, Sachen wie zum Beispiel
Multiboards nur bei einem Spieler anzeigen zu lassen. Denn bei

native MultiboardDisplay                takes multiboard lb, boolean show returns nothing

kann man keinen Spieler angeben, es wird immer entweder
für alle Spieler gezeigt oder verborgen. Mit GetLocalPlayer
kann man diese Funktion bauen:

function MultiboardDisplayForPlayer takes multiboard mb, boolean show, player p returns nothing
    if ( GetLocalPlayer() == p ) then
        call MultiboardDisplay(mb,show)
    endif
endfunction

Gut, was wird hier gemacht? Essenziell ist die Bedingung

GetLocalPlayer() == p

Nehmen wir mal an p ist Spieler 3, dann ergibt sich
folgendes:

Am PC von Spieler 1 kommt raus: Spieler 1 == Spieler 3 (false)
=> Multiboard wird nicht gezeigt

Am PC von Spieler 2 kommt raus: Spieler 2 == Spieler 3 (false)
=> Multiboard wird nicht gezeigt

Am PC von Spieler 3 kommt raus: Spieler 3 == Spieler 3 (true)
=> Multiboard wird gezeigt

Am PC von Spieler 4 kommt raus: Spieler 4 == Spieler 3 (false)
=> Multiboard wird nicht gezeigt

usw.

Diese Methode funktioniert bei allen Sachen, die sich aufs
Anzeigen von irgendwas beschränken.

Denn was würden denn hierbei passieren?

function Desync takes player p returns nothing
    if (GetLocalPlayer() == p) then
        call CreateUnit(p,'hfoo',0,0,0)
    endif
endfunction

Diese Funktion hätte zur Folge, dass nur am PC von
Spieler p eine Einheit da wäre, bei den anderen nicht. Der
könnte dann etwas mit der Einheit machen, was dann nur bei ihm
so wären, und irgendwann wäre bei allen Spielern etwas
komplett anderes aufm Bildschirm. Diesen Status nennt man
Asynchrones (=ungleiches) Spiel. Damit so etwas nicht passiert, hat
Warcraft einen automatischen Anti-Async-Mechanismus, und zwar
beendet es beim Aufruf der Funktion oben sofort das Spiel.

Deswegen hab ich auch gesagt: „GetLocalPlayer hat hohe
Zerstörungskraft“

Fazit: GetLocalPlayer kann sehr nützlich sein, aber sehr
schnell auch viel kaputt machen, wenn man nicht aufpasst.

zum
Anfang
2.8 Übungen

Übung 1

Erstelle eine einfache
Funktion, die als Parameter einen player und einen string
nimmt. Der Text soll allen Spielern außer dem angegebenen
angezeigt werden.

Übung 2

Erstelle eine Funktion,
die überprüft, ob ein String in einem anderen
enthalten ist (Rückgabewert ist boolean)

Übung 3

Erstelle eine Funktion,
die die durchschnittlichen Prozentualen Leben aller Einheiten
in einer Einheitengruppe berechnet. Die Gruppe, die hier als
Parameter übergeben wird, soll NICHT zerstört werden,
aber es dürfen keine anderen Memory Leaks entstehen.

Übung 4

Ziel ist es, folgende
Funktion zu erstellen: Die Funktion setzt einen String auf eine
Bestimmte Länge, indem es bestimmte Zeichen hinten
anhängt. Beispiel: SetStringLength(„Mr X“,10,“X“) ergibt
„Mr XXXXXXX“. Parameter sind also 2 strings (Der String der
verlängert werden soll und das Zeichen das angehängt
wird) und ein integer der die gewünschte Stringlänge
angibt.


Musterlösungen zu den Übungen

zum
Anfang
Kapitel III

In Teil 3 des Kurses beginnen wir endlich mit den altbekannten
Triggern. Somit könnt ihr das Wissen auch praktisch
einsetzen. Anschließend gehe ich noch kurz auf (doch
recht wichtige) Pick-Schleifen ein und dann kommt einer der
wichtigsten Punkte in JASS – nämlich GCRB, sowohl Theorie
als auch praktische Anwendung.

Die restlichen Teile sind eher fortgeschrittene Dinge, die aber
trotzdem recht wichtig sind.

Und es wird natürlich wieder etliche Übungen geben,
die unbedingt gemacht werden sollten.

zum
Anfang
3.1 Trigger & Kartenscript

Den Meisten wird der Begriff Trigger bekannt sein – den
GUI-lern natürlich sowieso. Allerdings werden wir in
diesem Abschnitt sehen, dass Trigger gar nicht so feststehend
und auch nicht so wichtig sind, wie uns das GUI das immer
vorgaukelt, man auch per Trigger neue Trigger erstellen kann
usw. Dazu werden ich mal wieder etwas tiefer in die Materie
eindringen und den groben Aufbau der war3map.j-Datei
ansprechen.

Aber zunächst: Was ist ein Trigger? Ein Trigger ist
ein Objekt, dem man 0 bis unendlich Ereignisse, Bedingungen und
Aktionen zuteilen kann. Wenn eines der Ereignisse eintritt und
alle Bedingungen wahr sind, werden alle Aktionen eines Triggers
ausgeführt.

Zunächst müssen wir einen Trigger erstellen, um ihm
Aktionen zuweisen zu können. Das geht mit der Funktion
CreateTrigger,
z.B.

local trigger t = CreateTrigger()

Eine Aktion erteilt man mit TriggerAddAction.
Der erste Parameter ist der gewünschte Trigger, der zweite
Parameter wird euch etwas seltsam vorkommen: code actionFunc. Hier
müsst ihr eine Funktion angeben, die aufgerufen werden soll,
wenn der Trigger ausgeführt wird. Angegeben wird das in der
Form:

function FUNKTIONSNAME

Also ein Beispiel:

function test2 takes nothing returns nothing
    call DoNothing()
endfunction
 
function Test takes nothing returns nothing
    local trigger t = CreateTrigger()
    call TriggerAddAction(t, function test2)
endfunction

Wichtig ist, dass unsere Funktion test2 keine Parameter und
Rückgabewerte hat. Es ist üblich, einen Trigger nur eine
Aktions-Funktion zu geben. Bedingungen werden ähnlich wie
Aktionen hinzugefügt. Hierzu erstellt man eine Funktion mit
Rückgabewert boolean und gibt diese dann folgendermaßen
an:

call TriggerAddCondition(t, Condition( function FUNKTIONSNAME ) )

Ist der Rückgabewert dieser Funktion dann true, so wird
der Trigger ausgeführt.

Ereignisse fügt man über die
„TriggerRegisterXXXEvent“-Funktionen hinzu. Von denen gibt es
jede Menge, wobei XXX alles mögliche sein kann. Um die
ganzen Events zu finden, am besten in die Suchleiste von
JassCraft „TriggerRegister“ eingeben – ein Parameter ist immer
der gewünschte Trigger und der Rest ergibt sich aus dem
Zusammenhang. Jetzt wird euch aber wahrscheinlich auffallen,
dass wir einen ewigen Teufelskreis haben. Denn durch die Events
der Trigger können wir die ausführung von Funktionen
starten. Aber um einem Trigger ein Ereignis zu erteilen,
brauchen wir ja wieder Aktionen – und von wo werden die
gestartet?

Wir brauchen – wie in Kurs 1 bereits erwähnt – einen
Startpunkt. Um diesen zu finden werden wir einen kurzen
Einblick in das Kartenscript der Warcraft 3 Karten wagen.

In jedem w3x-Archiv (Warcraft 3 Maps sind sowas wie zip
Dateien) befindet sich eine Datei war3map.j. Dort sind alle
Trigger und auch noch anderes Zeug drinnen. Denn
zusätzlich zu unseren eigenen Funktionen findet dort noch
die Erstellung von Zerstörbaren Objekten, Einheiten und
Gebieten, die man im Editor erstellt hat, statt.
Wettereinstellungen und sonstiges Zeug wird gemacht.
Anschließend erfolgt die Initialisierung der mit GUI
erstellten Trigger und der Globalen Variablen und dann erst
kommt das Zeug das man selber gescriptet hat.

Um unseren Startpunkt festzulegen, wäre es jetzt
natürlich gut, eine unserer Funktionen direkt in diesem
Initialisierungsteil aufzurufen, Problem ist nur, dass man
diesen nicht verändern kann!

Aber Moment – „Initialisierung der mit GUI erstellten Trigger“
– na das wär doch ne Möglichkeit. Wandelt man einen
GUI-Trigger in Jass um, so erkennt man das diese immer aus
mindestens 2 Funktionen bestehen, einer InitTrig_TRIGGERNAME
und Trig_TRIGGERNAME_Actions. Die Init-Trig-Funktion ist die,
die automatisch beim Mapstart aufgerufen wird. Außerdem
wird eine Globale Trigger-Variable namens gg_trg_TRIGGERNAME
erstellt ( [g]enerated [g]lobal – [tr]i[g]ger ). Das
heißt also, wir müssen, auch wenn wir komplett mit
Jass arbeiten wollen, immer mindestens einen Trigger mit dem
GUI erstellen. In der Init-Aktion können wir dann den
„Kreislauf“ starten. Ob wir die vom GUI erstellten Trigger
nutzen oder eigene benutzen, bleibt uns selber überlassen.

Zusammenfassung:

  • Trigger mit set VARIABLE = CreateTrigger() erstellen
  • Aktion mit TriggerAddAction(TRIGGER,function
    FUNKTIONSNAME) hinzufügen
  • Bedingung mit TriggerAddCondition(TRIGGER,
    Condition(function FUNCTIONSNAME)) hinzufügen
  • Ereignisse mit TriggerRegisterXXXEvent
    hinzufügen
  • Trigger über das GUI erstellen und eigene Funktionen
    in Init-Aktion aufrufen

zum
Anfang
3.2 Pick-Schleifen

In Kurs 1 haben wir Schleifen kennen gelernt. Diese mögen
sehr nützlich sein, wenn man z.B. etwas 10 mal
auführen will – aber was ist wenn man zum Beispiel alle
Einheiten in einer Einheitengruppe durchgehen will?

Dafür gibt es die Pick-Schleifen. Wir werden 2
Möglichkeiten kennen lernen, eine solche Schleife zu
benutzen:

-Die Nativen Funktionen ForGroup() und ForForce()

-Die Schleife selber bauen (Nur Gruppen)

Die Funktion ForGroup sieht so so aus:

native ForGroup                 takes group whichGroup, code callback returns nothing

Die group ist logischerweise unsere Gruppe, und den Typ Code
kennen wir ja schon. Hier müssen wir eine Funktion angeben,
die für jede Einheiten in der Gruppe ausgeführt werden
soll. Auf die aktuell „gepickte“ Einheit können wir dann mit
GetEnumUnit() zugreifen.

Allerdings ist das mit der zusätzlichen Funktion ziemlich
nervig. Außerdem kann man keine Parameter und
Rückgabewerte verwenden. Daher bauen sich viele JASS’ler
ihre Pick-Schleifen selber.

Ein Beispiel ist 1000 Worte wert:

function PickTest takes group g returns nothing
    local unit u = null
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g)
        //Hier werden die Aktionen durchgeführt
    endloop
    call DestroyGroup(g)
    set g = null
endfunction

Die Gruppe wird hier via Parameter übergeben. Wir
starten einen Loop, der die erste Einheit in der Gruppe nimmt. Wir
prüfen, ob die Einheit gleich null ist, sprich ob die Gruppe
leer ist. Danach entfernen wir die Einheit aus der Gruppe und
führen die Aktionen aus, die wir mit der Einheit halt
anstellen wollen.

Die Schleife beginnt von vorne, und da wir die Einheit zuvor
aus der Gruppe entfernt haben, ist die FirstOfGroup jetzt eine
andere Einheit. Irgendwann haben wir alle Einheiten in der
Gruppe durch und u == null ergibt true. Dann wird die Schleife
verlassen und der Memory Leak beseitigt. Beachten muss man
hierbei, dass die Gruppe bei dieser Pick-Methode zerstört
wird. Möchte man die ursprüngliche Gruppe behalten,
legt man zuvor eine temporäre Kopie an:

function PickTest takes group g returns nothing
    local unit u = null
    local group g1 = CreateGroup()
    call GroupAddGroup(g,g1)
    loop
        set u = FirstOfGroup(g1)
        exitwhen u == null
        call GroupRemoveUnit(g1)
        //Hier werden die Aktionen durchgeführt
    endloop
    call DestroyGroup(g1)
    set g1 = null
endfunction

zum
Anfang
3.3 Gamecache & Return-Bug

GCRB steht für Game Cache and Return Bug. Diese Technik
wird sehr oft benutzt und daher behandeln 4 Teile dieses
Tutorials nur das Thema.

zum
Anfang
3.3.1 Return-Bug

Sehen wir uns mal folgende Funktion an:

function h2i takes handle h returns integer
    return h
endfunction

Wir werden recht schnell feststellen: Das kann nicht gehen,
es wird einen Syntax Error geben. Schauen wir uns doch mal diese
Funktion an:

function h2i takes handle h returns integer
    return h
    return 0
endfunction

Nun werdet ihr euch fragen „warum fragt der Depp jetzt
nochmal das gleiche – das geht doch genauso wenig!“. Die Antwort
wäre eigentlicht total richtig denn die Funktion IST falsch,
jedoch kann man sie Funktion problemlos verwenden, ohne
irgendwelche Fehler oder Spielabstürze zu bekommen. Und eben
deshalb ist es der Return-BUG – Ein Fehler den Blizzard beim
Syntax-Checker gemacht hat. Solange der Typ des letzten returns
richtig ist (hier die 0, ein Integer) können die Typen von den
returns davor beliebig sein.

Nun – was passiert wenn man diese Funktion aufruft?

Wir haben zum Beispiel eine Einheit auf der Map stehen und
übergeben diese als Parameter an die Funktion. Die
Funktion führt jetzt „return h“ aus und soll einen Integer
zurückgeben – die Einheit soll jetzt also ein Integer
sein?! Man könnte einen Absturz vermuten, aber es klappt
und raus kommt eine Zahl irgendwo bei 120000000.

Die Zahl selber ist auch eher unwichtig, wichtig ist nur ihre
Bedeutung, sie ist nämlich die Id der Einheit. Blizzard
hat wohl jedem Objekt (handle) das auf der Map rumsteht eine
EINDEUTIGE Nummer gegeben. Wenn man also die h2i-Funktion mit
jeder Gruppe, Einheit, Gebiet, Punkt, Sound, Spezialeffekt usw.
auf der Map aufrufen würde und sich die Zahl als Text
ausgeben lässt, so wird jedes Mal was anderes rauskommen.

Allerdings wird jedes mal das selbe rauskommen, wenn man die
Funktion mehrmals mit dem gleichen Parameter aufruft.

Ebenso wie die h2i-Funktion könnte man auch eine
Umkehrfunktion schreiben – z.B. i2u:

function h2i takes handle h returns integer
    return h
    return 0
endfunction
 
function i2u takes integer i returns unit
    return i
    return null
endfunction
 
function test takes  unit u returns nothing
    local integer i = h2i(u)
    local unit u2 = i2u(i)
    if ( u == u2) then
        call BJDebugMsg("Es hat geklappt!")
    endif
endfunction

In Funktion Test lassen wir uns die Id von einer Einheit in
eine Variable speichern. Danach benutzen wir i2u. Wenn der
Parameter davon eine von h2i erzeugte Zahl ist wird hier wieder die
Einheit von vorher rauskommen. Somit ist die Bedingung u2 == u wahr
und die Nachricht würde auf dem Bildschirm ausgegeben werden.

zum
Anfang
3.3.2 Gamecache

Der Gamecache stellt eine Alternative zu Variablen dar. Man
kann darin Werte speichern, indem man zuerst einen Gamecache
initialisiert und man kann dann Werte (Integer, Real, String)
unter einer Kategorie und einem Label abspeichern.

Diese Werte kann man entweder als Alternative zu Variablen
verwenden, oder zum Mapübergreifenden Speichern von Daten
wie z.B. in einer Kampagne den Helden, was aber nur im
Singleplayer geht.

Funktionen die man für den Gamecache braucht:

InitGameCache
StoreInteger
StoreReal
StoreString
StoreBoolean
GetStoredInteger
GetStoredReal
GetStoredString
GetStoredBoolean

Alles eigentlich recht simpel und sollte keine
größeren Probleme darstellen. Jetzt werden wir uns
anschauen, wie wir den Gamecache mit dem Return Bug
verknüpfen.

zum
Anfang
3.3.3 Praktische Anwendung

Man kombiniert den Return Bug mit dem Gamecache, um Werte an
Objekte „anzutackern“. Das heißt man kann jedem Objekt
beliebig viele verschieden Werte anhängen. So könnte
man zum Beispiel das Alter jeder Einheit über GCRB
speichern, indem man einen Integer an die Einheit „antackert“.

Wir wissen, dass jedes Objekt eine eindeutige ID besitzt.
Diesen Integer-Wert kann man auch in einen String umwandeln.
Wir speichern nun einen Wert in den Gamecache und benutzen als
Label die ID der Einheit. Die Kategorie können wir
beliebig wählen.

Hier ein Beispiel.

globals
    gamecache Cache = InitGameCache("GCRB.w3v")
endglobals
 
function h2i takes handle h returns integer
    return h
    return 0
endfunction
 
function ChangeAge takes unit u, integer value returns nothing
    local integer id = h2i(u)
    call StoreInteger(Cache,"Alter",I2S(id),value)
endfunction
 
function GetAge takes unit u returns integer
    local integer id = h2i(u)
    return GetStoredInteger(Cache,"Alter",I2S(id))
endfunction

Über diese 4 Funktionen könnten wir ohne Probleme
mit ein paar einfachen Funktionsaufrufen das Alter jeder Einheit
verändern, z.B. in einem periodischen Trigger (den ich aus
Platzgründen weggelassen habe). Man kann aber nicht nur
Integer, Reals und Strings anhängen, sondern auch andere
Objekte, zum Beispiel kann man eine Unit an eine Unit „antackern“.

Dazu benutzt man einfach nochmal den Return Bug und erhält
die Id von der Einheit die man antackern möchte. Diesen
Integer kann man mit GCRB attachen und wenn man die Einheit
wieder abfragen möchte kann man diesen Integer mit i2u
wieder in eine Unit umformen. Ich benutze hier als Beispiele
meistens Units (weil man sich die einfach am besten vorstellen
kann), aber wie gesagt klappt das mit allen Untertypen von
Handle.

Da diese Schreibweise mit dem I2S(h2i(u)) recht
unübersichtlich ist, benutzt man meistens ein GCRB-System.
So ein System sind einfach ein Paar Funktionen, die das
Attachen vereinfachen. Das meistbenutzte und wohl auch
komfortabelste sind Kattana’s Local Handle Vars, hier das
System:

library LHV
 
globals
    gamecache LOCAL_HANDLE_CACHE = InitGameCache("localhandles.w3v")
endglobals
 
// ===========================
private function H2I takes handle h returns integer
    return h
    return 0
endfunction
 
// ===========================
function LocalVars takes nothing returns gamecache
    return LOCAL_HANDLE_CACHE
endfunction
 
function SetHandleHandle takes handle subject, string name, handle value returns nothing
    if value==null then
        call FlushStoredInteger(LocalVars(),I2S(H2I(subject)),name)
    else
        call StoreInteger(LocalVars(), I2S(H2I(subject)), name, H2I(value))
    endif
endfunction
 
function SetHandleInt takes handle subject, string name, integer value returns nothing
    if value==0 then
        call FlushStoredInteger(LocalVars(),I2S(H2I(subject)),name)
    else
        call StoreInteger(LocalVars(), I2S(H2I(subject)), name, value)
    endif
endfunction
 
function SetHandleBoolean takes handle subject, string name, boolean value returns nothing
    if value==false then
        call FlushStoredBoolean(LocalVars(),I2S(H2I(subject)),name)
    else
        call StoreBoolean(LocalVars(), I2S(H2I(subject)), name, value)
    endif
endfunction
 
function SetHandleReal takes handle subject, string name, real value returns nothing
    if value==0 then
        call FlushStoredReal(LocalVars(), I2S(H2I(subject)), name)
    else
        call StoreReal(LocalVars(), I2S(H2I(subject)), name, value)
    endif
endfunction
 
function SetHandleString takes handle subject, string name, string value returns nothing
    if value==null then
        call FlushStoredString(LocalVars(), I2S(H2I(subject)), name)
    else
        call StoreString(LocalVars(), I2S(H2I(subject)), name, value)
    endif
endfunction
 
function GetHandleHandle takes handle subject, string name returns handle
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleInt takes handle subject, string name returns integer
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
endfunction
function GetHandleBoolean takes handle subject, string name returns boolean
    return GetStoredBoolean(LocalVars(), I2S(H2I(subject)), name)
endfunction
function GetHandleReal takes handle subject, string name returns real
    return GetStoredReal(LocalVars(), I2S(H2I(subject)), name)
endfunction
function GetHandleString takes handle subject, string name returns string
    return GetStoredString(LocalVars(), I2S(H2I(subject)), name)
endfunction
function GetHandleUnit takes handle subject, string name returns unit
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleTimer takes handle subject, string name returns timer
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleTrigger takes handle subject, string name returns trigger
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleEffect takes handle subject, string name returns effect
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleGroup takes handle subject, string name returns group
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleLightning takes handle subject, string name returns lightning
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleLocation takes handle subject, string name returns location
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleTimerdialog takes handle subject, string name returns timerdialog
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function GetHandleWidget takes handle subject, string name returns widget
    return GetStoredInteger(LocalVars(), I2S(H2I(subject)), name)
    return null
endfunction
function FlushHandleLocals takes handle subject returns nothing
    call FlushStoredMission(LocalVars(), I2S(H2I(subject)) )
endfunction
 
endlibrary

Um mit dem System einen Werte zu attachen, ruft man einfach
SetHandleInt bzw. SetHandleReal/String/Boolean/usw. auf und
übergibt als Parameter das Handle an das attacht werden soll,
das Label und dem der Wert gespeichert werden soll und welcher Wert
gespeichert werden soll. Die Werte zurück zu erhalten geht
dann mit GetHandleInt/Real/String/Boolean usw.

Nachtrag:

Dieses System gilt mittlerweile als veraltet, da beim umwandeln
eines Integer-Typs in einen Handle Typ Fehler aufreten
können. Daher sollte dieses System höchstens für
die einfachen Datentypen Integer, Real, Boolean und String
verwendet werden.

Wenn ihr dieses System verstanden habt, ist es aber auch nicht
mehr so schwer ein anderes System zu benutzen. In folgendem
Tutorial lernt ihr eine moderne Methode kennen: http://wc3campaigns.net/showthread.php?t=91491

zum
Anfang
3.4 Waits, Threads & Op
Limit

Folgende einfache Funktion:

function test takes nothing returns nothing
    loop
        call DoNothing()
    endloop
endfunction

Ruft man diese Funktion auf und testet das ganze mit dem
JNGP, so wird man ingame den Error „Hit Op Limit“ angezeigt
bekommen. Das Op(eration) Limit, auf deutsch fälschlicherweise
Maximale Ausführungszeit genannt, ist ein Schutzmechanismus
des Editors. So gibt es eine Grenze von 300 000 Micro Operations,
die maximal in einem Thread (dazu gleich mehr) ausgeführt
werden dürfen, anschließend wird die Ausführung
einfach gestoppt.

Jede „Aktion“ in einer Funktion verbraucht verschieden viele
dieser Micro Ops. Eine komplette Liste kann auf
http://www.wc3campaigns.net eingesehen werden. Diese Grenze von
300 000 Micro Ops liegt ziemlich hoch, allerdings braucht man
manchmal Trigger, die noch mehr verbrauchen. Um die Grenze dann
zu umgehen, gibt es mehrere Möglichkeiten:

  • Wait
  • TriggerSyncStart oder TriggerSyncReady
  • Nulltimer
  • ExecuteFunc

Wait

Ein Wait ist in JASS die Funktion
TriggerSleepAction. Man kann die Dauer angeben, für die
der Trigger pausiert wird. Durch ein beliebig langes Wait wird
die Anzahl der verbrauchten Micro Ops wieder auf 0 gesetzt,
allerdings ist ein Wait auch recht lange und so oft nervig in
Triggern die sehr schnell ausgeführt werden sollen.

TriggerSyncStart bzw.
TriggerSyncReady

Diese beiden Natives ohne Parameter und
Rückgabewert sind so ähnlich wie das Wait, allerdings
ist die Wartezeit kürzer. Am schnellsten ist
TriggerSyncStart mit einer Pausezeit von ca. 0.1 Sekunden.

Nulltimer

Ein Nulltimer ist ein Timer,
dessen Countdown genau 0.00 Sekunden dauert, das heißt er
läuft sofort aus. Bei der Funktion TimerStart kann man als
Parameter eine Funktion übergeben, die beim Ablauf des
Timers ausgeführt wird, also bei einem Nulltimer sofort.
Diese Funktion wird in einem neuen Thread gestartet, d.h. die
Anzahl der verbrauchten Micro Ops ist wieder bei 0.
Problematisch bei dieser Methode ist jedoch, dass keine
Parameter übergeben und keine Rückgabewerte genommen
werden können, alternativ kann man aber mit GCRB die
nötigen Werte an den Timer attachen.

ExecuteFunc

Dabei handelt es sich um
eine Native Funktion, mit der man eine Funktion in einem neuen
Thread ausführen lassen kann. Man gibt dazu einfach den
Namen der Funktion als Parameter an. Problematisch ist
allerdings wieder, dass kein Parameter und Rückgabewert
möglich ist, im Gegensatz zum Nulltimer sind jedoch
Ereignisreaktionen wie GetTriggerUnit() noch verfügbar.

Nun wie angekündigt eine Erklärung zu Threads:
Aktionen werden immer in einem Thread ausgeführt. Der
lässt sich eigentlich auch recht gut mit einem Thread im
Forum vergleichen. Jedes Mal, wenn ein Trigger ausgeführt
wird, wird ein neuer Thread gestartet und die Trigger-Aktionen
in diesem Thread ausgeführt. Das Op Limit ließe sich
mit einer Maximalzahl an Posts in einem Forenthread vergleichen
(gibt ja zum Glück keine^^). Mit Nulltimern und
ExecuteFunc startet man einen neuen Thread und führt die
Aktionen dann dort aus.

zum
Anfang
3.5 Übungen

Übung 1

Baue das Alter-System
fertig. Jede Einheit auf der Landkarte soll einen Wert „Alter“
attached bekommen, der sich in der Sekunde um 1 erhöht.

Übung 2

Erstelle ein System,
mithilfe dessen man eine Einheit immer neben einer anderen
herlaufen lassen kann, wie in einer 2er-Formation. Das ganze
soll man mit beliebig vielen Einheiten auf der Map machen
können.

Übung 3

Erstelle ein System, mit
dem man ein ITEM in einer graden Linie fliegen lassen kann. Das
ganze soll bei beliebiger Itemzahl und mit einstellbarer
Richtung, Geschwindigkeit und Reichweite funktionieren.


Musterlösungen zu den Übungen

zum Anfang

  • 08.10.2008 um 15:04
JASS für Einsteiger Programmierer und Templater gesucht!