Dracularks Jass Einsteiger Tutorial

[nocontentad]

Dracularks Jass Einsteiger Tutorial


zum Anfang


1. Einleitung

Was wird dieses Turoial erklären?

Dieses Tutorial ist für Einsteiger, die noch nie (bzw. nur wenig) mit
Jass gearbeitet haben. Es wird erklären, wie man einen GUI-Trigger
in einen JASS-Trigger umwandelt und wie dieser neu entstandene
Trigger zu verstehen/zu lesen ist.
Weiters wird es noch eine kurze Einführung in lokale Variablen
geben.


zum Anfang


2. Der GUI-Trigger

Was ist GUI?

GUI (Graphical User Interface) ist, wie der Name schon sagt, eine
graphische Oberfläche für den Benutzer, um ihm das Arbeiten zu
vereinfachen. Im Falle des Trigger Editors sieht das wie folgt aus:

Bild:Gui-Trigger

Man sieht also in Form eines Interfaces, die verschiedenen Events,
Bedingungen und Aktionen eines Triggers, was das Lesen für Leute,
die nur wenig von Programmieren verstehen, vereinfacht.

Was ist ein Trigger?
Ein Trigger (auch Auslöser genannt) ist etwas, das auf gewisse
Sachen reagiert (Events) und dementsprechende Anweisungen
(Aktionen) ausführt, wenn die Bedingungen gelten.
Im Falle dieses Beispiels: Wenn eine Einheit den Effekt einer
Fähigkeit startet (z.B. einen Stormbolt auf eine Einheit zaubert) und
dieser Zauber Gebrüll ist, dann wird ein Footman für den Besitzer der
zaubernden Einheit an der Position des Ziels kreiert.
Dieser Einheit wird ein 2 Sekunden Timer geadded (nach 2 Sekunden
stirbt die Einheit), der Zauber Flammenschlag hinzugefügt und sie
wird beordert Flammenschlag auf die Position des Ziels zu zaubern.
Bei einem Trigger müssen also IMMER Events, (Conditions) und
Aktionen ausgeführt werden.
Der Nachteil, den dieser GUI-Trigger besitzt, sind die globalen
Variablen (Casting unit, Target unit of ability being cast, usw.).
Was sind globale Variablen und warum sind sie schlecht?
Das sind Variablen, die außerhalb von Funktionen definiert sind und
auf die man jederzeit und überall zugreifen kann. Es ist nicht schwer
zu erkennen, warum sie nicht das Ideale sind für komplexere Trigger
mit waits u.ä.
Man hat keine Garantie darauf, ob die globale Variable vor einem
wait, den gleichen Wert besitzt wie nach dem wait, da ja überall auf
sie zugegriffen werden kann und eventuell ihr Inhalt geändert
werden könnte.
Die Abhilfe? – Lokale Variablen und dafür benötigen wir Jass.


zum Anfang


3. Der JASS-Trigger

Zu allererst muss man den GUI-Trigger in einen Jass Trigger
konvertieren. Dazu wählt man oben im Triggereditor in der Kategorie
Edit ´Convert to Custom Text´ aus. Eine Warnnachricht wird
auftauchen, die könnt ihr aber vernachlässigen (Sie weißt euch
lediglich auf die folgen hin, was ´Convert to Custom Text´ bewirkt).
Der neu entstandene Trigger sieht dann wie folgt aus:

Bild:Jass-Trigger

Das mag auf den ersten Blick recht verwirrend aussehen, aber gehen
wir es einmal Schritt für Schritt durch.


zum Anfang


4. Die Jass-Syntax

Zuerst einmal erkennt man 3 Funktionen (Condition, Action, Event).
Ganz unten sieht man die Funktion, die den Trigger kreiert (unterhalb
des Kommentars, der mit // gekennzeichnet wird).
Dort sieht man, dass eine globale Variable gesetzt wird, mit dem
Namen gg_trg_NAME. Variablen werden mittels ´set´ Statement
gesetzt. Diese gg_trg_NAME Variable wird automatisch kreiert und
verweist auf den kreierten Trigger.
Zuerst einmal wird die Funktion CreateTrigger() aufgerufen, wie
einen Wert zurückliefert, und gg_trg_NAME wird diesem Wert
zugewiesen.
Anschließend werden dem Trigger Conditions und Actions
hinzugefügt mit den Funktionsaufrufen von TriggerAddCondition und
TriggerAddAction.
Funktionen werden mittels ´call´ Statement aufgerufen (außer bei
der Variablenwertsetzung, wie man es bei set gg_trg_NAME =
CreateTrigger() sieht).
Eine Funktion wird also mit call FunctionName( PARAMETERS)
aufgerufen.
PARAMETERS können nun mehrere verschiedene Variablen sein, die
an die Funktion übergeben werden, oder einfach nichts, dann
schreibt man nur die
leere Klammer ().
z.B.

call DoNothing()

Nun haben wir also den Trigger kreiert und haben ihm Bedingungen
und Aktionen hinzugefügt (Bedingungen sind immer optional und
können auch weggelassen werden). Diese InitTrig_NAME Funktion
wird eigentlich fast nie verändert. Neulinge sollte also die Finger
davon lassen.

Als nächstes schauen wir nach ganz oben, zu der
Bedingungsfunktion.
function Trig_NAME_Conditions takes nothing returns boolean
Was bedeutet das?
Nach ´takes´ kommen immer die Parameter, die die Funktion
erwartet (in diesem Fall eben nothing, also nichts). ´Returns´ gibt an,
was die Funktion zurückliefert (in diesem Fall einen Boolean Wert, ob
die Bedingung erfüllt ist oder nicht).
Möchte man z.B. eine Funktion erstellen, die eine Einheit, einen
Ganzzahl Wert und einen Spieler erwartet, würde das in etwa so
aussehen:

function Test takes unit u, integer i, player p returns nothing
     // irgendwelche Aktionen
endfunction
 

Parameter müssen also einmal einen Typ besitzen (unit, integer,
player, usw.), einen Namen für die Variable, die übergeben wird
(damit man innerhalb der Funktion auch damit arbeiten kann) und sie
müssen mit Beistrichen von einander getrennt sein.
Weiters sieht man, dass jede Funktion mit einem ´endfunction´
abgeschlossen werden muss.

Die Bedingung selbst

if( not( GetSpellAbilityId() == 'ANbr')) then
     return false
endif
return true
 

wird leider etwas merkwürdig vom Editor umgeformt. Zu Lesen ist es
ungefähr so:
´Wenn die Zauberspruch ID nicht ´Anbr´ ist, dann ist die Bedingung
nicht erfüllt (gibe falsch zurück, return false), ansonsten ist sie erfüllt
(gib wahr zurück, return true).

In einer Zeile könnte man es einfacher so formulieren:

return GetSpellAbilityId() == 'Anbr'
 

Warum?
Die Funktion muss ja einen boolean (Wahrheit) Wert zurückgeben,
also true oder false. Der Vergleich GetSpellAbilityId() == ´Anbr´ ist
schon selbst ein Wahrheitswert und somit kann man diese 4 Zeilen
auf eine zusammenfassen.
Bei der Aktionsfunktion gibt es nun nicht mehr viel neues, man kann
es sich aus den obigen Erklärungen zusammenreimen, was dort
passiert.
Somit kommen wir also zu den


zum Anfang


5. Lokalen Variablen

Der Vorteil von lokalen Variablen gegenüber globalen Variablen ist,
dass sie nur innerhalb der Funktion, in der sie erstell werden,
verwendet werden können und sie somit von globalen Dingen
ausgenommen sind. Sie sind sozusagen ´sicherer´ als globale
Variablen.
Wie erstellt man eine lokale Variable?
Schauen wir uns Die Aktionsfunktion an:

function Trig_Unit_Spawn_Actions takes nothing returns nothing
     call CreateNUnitsAtLoc   ( 1, 'hfoo', GetOwningPlayer( GetSpellAbilityUnit()), GetUnitLoc( GetSpellTargetUnit()), bj_UNIT_FACING)
     call UnitApplyTimedLifeBJ( 2.00, 'BTLF', GetLastCreatedUnit())
     call UnitAddAbilityBJ    ( 'AHfs', GetLastCreatedUnit())
     call IssuePointOrderLocBJ( GetLastCreatedUnit(), "flamestrike", GetUnitLoc( GetSpellTargetUnit()))
endfunction
 

Dort sieht man die globalen Variablen GetSpellAbilityUnit(), GetSpellTargetUnit(), GetLastCreatedUnit(), usw. (eigentlich sind das
keine Variablen, sondern sogenannte Getter Methoden, die eben
den Wert einer Variable zurückliefern, in dem Fall den Wert der
globalen Variablen für CastingUnit, SpellTargetUnit, LastCreatedUnit,
usw.)
Wir möchten diese Variablen nun auf lokale Variablen zuordnen.
Dazu erst einmal der Code:

function Trig_Unit_Spawn_Actions takes nothing returns nothing
     local unit   caster      = GetSpellAbilityUnit()
     local unit   target      = GetSpellTargetUnit()
     local player CasterOwner = GetOwningPlayer( caster)
     local unit   dummy       = null
 
     call CreateNUnitsAtLoc( 1, 'hfoo', CasterOwner, GetUnitLoc( target), bj_UNIT_FACING)
     set dummy = GetLastCreatedUnit()
     call UnitApplyTimedLifeBJ( 2.00, 'BTLF', dummy)
     call UnitAddAbilityBJ    ( 'AHfs', dummy)
     call IssuePointOrderLocBJ( dummy, "flamestrike", GetUnitLoc( target))
endfunction
 

Wie man sieht, müssen (!) lokale Variablen gleich als Allererstes in
der Funktion definiert werden (man muss ihnen aber noch keinen
Wert zuweisen [local unit dummy = null] ). Diesen Variablen weist
man dann beispielsweise die Werte der globalen Variablen zu (local
unit caster = GetSpellAbilityUnit()). Nun hat man also lokale
Variablen, die den Wert der globalen Variablen zu diesem Zeitpunkt
besitzen jedoch nicht mehr von globalen Einflüssen manipuliert
werden können. Somit könnte man sie auch nach waits und anderen
Dingen verwenden, ihr Wert bleibt gleich (außer man ändert ihn
selbst).
Diese lokale Variablen übergibt man nun den unterschiedlichen
Funktionen anstatt der globalen Variablen (z.B. bei local player
CasterOwner = GetOwningPlayer( caster ), übergibt man der
GetOwningPlayer Funktion die lokale Variable caster, anstatt der
vorhin globalen Variable GetSpellAbilityUnit() ).
Einen Sonderfall sehen wir nach der Funktion CreateNUnitsAtLoc.
Dort setzen wir mittels dem ´set´ Statement die unit Variable mit
dem Namen dummy auf GetLastCreatedUnit() (also die Einheit, die
zuletzt kreiert wurde, In diesem Fall unser Footman). Die Variable
wurde also erst im Laufe der Funktion mit einem Wert belegt. Die
Variable GetLastCreatedUnit() wird erst innerhalb der Funktion
CreateNUnitsAtLoc mit einem Wert belegt und deswegen macht es
erst Sinn NACH dieser Funktion die dummy unit Variable zu setzen.


zum Anfang


6. Schlusswort zur Jass-Syntax

Das war nun also die Einführung in die JASS Syntax und in die lokalen
Variablen. Keine Sorge, wenn ihr das beim ersten Mal noch nicht
richtig verstandet habt. Es ist noch kein Meister vom Himmel gefallen.
Wenn man jedoch erst einmal eingeübt ist mit Jass, lassen sich damit
viele Dinge einfacher und besser lösen, als mit GUI-Triggern und auch
die Beseitigung der Memory Leaks ist damit um einiges einfacher,

Um den Locationleak im obigen Trigger zu entfernen, braucht es
beispielsweise nur 3 Zeilen mehr:

function Trig_Unit_Spawn_Actions takes nothing returns nothing
     local unit     caster      = GetSpellAbilityUnit()
     local unit     target      = GetSpellTargetUnit()
     local player   CasterOwner = GetOwningPlayer( caster)
     local unit     dummy       = null
     local location TempPoint   = GetUnitLoc( target)// Wir speichern die Location in eine Variable
 
     call CreateNUnitsAtLoc( 1, 'hfoo', CasterOwner, TempPoint, bj_UNIT_FACING)
     set dummy = GetLastCreatedUnit()
     call UnitApplyTimedLifeBJ( 2.00, 'BTLF', dummy)
     call UnitAddAbilityBJ    ( 'AHfs', dummy)
     call IssuePointOrderLocBJ( dummy, "flamestrike", TempPoint)
     call RemoveLocation      ( TempPoint)// Wir zerstören die Location
     set TempPoint = null// Wir nullen die Location den leak weiter zu minimieren
endfunction
 

Zusammenfassung

  • Ein Trigger besitzt IMMER Events, (Conditions), Actions
  • Funktionen werden wie folgt definiert

    function NAME takes PARAMETERES returns VALUE
         // Actions to do
    endfunction
     
    
  • Funktionen werden mittels „call“ Statement aufgerufen (call DoSomething() )
  • lokale Variablen werden wie folgt definiert

    local VARTYPE VARNAME = VARVALUE
    
  • Lokale Variablen müssen immer zu Beginn der Funktion definiert werden
  • Getter Methoden wie GetSpellAbilityUnit(), GetSpellTargetLoc(), u.ä. liefern den aktuellen Wert der entsprechenden globalen Variablen zurück

    local unit caster = GetSpellAbilityUnit()
    


zum Anfang


7. Einheitengruppen in JASS

Was ist eine Einheitengruppe?
Eine Einheitengruppe ist, wie der Name schon sagt, eine Gruppe von Einheiten, die in einer Variable abgespeichert
werden können. Eine Sortierung gibt es, soweit ich weiß, nicht, d.h. die Einheiten in der Gruppe sind zufällig sortiert.
Es können also beispielsweise zuerst 3 Helden in der Gruppe sein, dann 5 Einheiten, 1 Gebäude und dann wieder
Helden. Die Reihenfolge ist also unabhängig davon, in welcher Reihenfolge man Einheiten in die Gruppe hinzufügt
und vom Typ der Einheiten.

Im GUI kennen wir diese Einheitengruppen aus den Funktionen : Pick every unit in _ and do Actions. Es wird also jede
Einheit in einer bestimmten Gruppe ausgewählt und gewisse Aktionen durchgeführt. Die Aktionen können sich nun
auf die ausgewählte Einheit selbst richten (z.B. indem man ihr Schaden zufügt) oder aber man arbeitet mit der Anzahl von Durchläufen, die die Schleife besitzt (z.B. für jede Einheit in der Gruppe erhält der Held +15 Schaden).

Es ist jetzt schon öfters der Begriff Schleife gefallen, aber was ist das eigentlich?
Eine Schleife kann man sich wie einen Kreis vorstellen. Der Kreis beginnt irgendwo (die Schleife fängt irgendwo an)
,hat irgendwo ein Ende (ein Durchlauf einer Schleife endet, nachdem alle Aktionen abgehandelt wurden) und beginnt
dann wieder von neuem beim Anfang (ein erneuter Durchlauf der Schleife, bis eine Abbruchbedingung einsetzt). Dies
geschieht solange, bis eine Abbruchbedingung einsetzt, oder die Schleife vom Benutzer selbst abgebrochen wird.
Eine Abbruchbedingung kann z.B. sein, dass eine Ganzzahl einen gewissen Wert übersteigt.

Ein Beispiel für eine einfache Schleife in JASS:

loop
     exitwhen <irgendeine Zahl> >= 5
endloop
 

Eine Schleife wird also mit ´loop´ gestartet und mit einem ´endloop´ beendet. Das ´exitwhen´ Statement gibt die
Bedingung an, bei der die Schleife enden soll (im Beispiel: eine Zahlvariable erreicht den Wert größer oder gleich 5 ).

Wie die gezeigte Schleife arbeiten auch Einheitengruppen. Es wird für jede Einheit in der Gruppe eine Aktion
ausgeführt, d.h. die Schleife beginnt mit der ersten Einheit, geht dann jede Einheit durch und wenn jede Einheit
einmal ausgewählt wurde, endet die Schleife.

Was lässt sich nun mit Einheitengruppen in JASS alles anstellen?
Ich glaube diese Frage beantwortet sich selbst, denn es ist eurer Phantasie überlassen, was ihr damit alles anstellen
wollt. Ich werde im Folgendem 2 Beispiele erklären:

  • Das erste wird eine Poison Nova sein (auf jedem feindlichen Held im Umkreis wird ein Shadowstrike gezaubert)
  • Das zweite wird ein ßbungsbeispiel für euch sein. Es ist eine Erweiterung des ersten Beispiels (auf 3 zufällige, feindliche Einheiten im Umkreis wird ein Frostbolt gezaubert)
  • Beide Karten werden in einer .rar Datei angehängt

Beispiel 1

Als erstes Beispiel hab ich mir eine Poison Nova genommen. Sie wird feindliche Helden im Umkreis auswählen und
je ein Dummy wird dann einen Shadowstrike auf sie zaubern.

Im GUI würde dies in etwa so aussehen:

Bild:Jass-Trigger

Was sind die Nachteile dieser Variante?
Zum einen wieder das Problem mit den globalen Variablen. Wenn nur wenige Einheiten ausgewählt werden, wird es
wohl selten zu Problemen kommen, aber werden nun 100e Einheiten ausgewählt, kann man kaum garantieren, dass
z.B. Casting Unit noch immer die richtige Casting Unit ist.
Weiters gibt es ein Problem mit Memory Leaks (Speicher Lecks). Zum einen ist das die erstellte Einheitengruppe selbst
( Units within 1500.00 range of Position ) zum anderen ist es die Position of Casting unit, die einen weiteren Leak
verursacht. Somit also keine saube Lösung für unsren Zauberspruch.
Sehen wir uns also einen JASS-Code an, der dasselbe bewirkt, nur eben besser.

function Trig_Poison_Nova_Conditions takes nothing returns boolean
     return GetSpellAbilityId() == 'A000'
endfunction
 
function Trig_Poison_Nova_Actions takes nothing returns nothing
     local unit   caster     = GetTriggerUnit()
     local group  targets    = CreateGroup()
     local unit   group_unit = null
     local unit   dummy      = null
     local real   cx         = GetUnitX( caster)
     local real   cy         = GetUnitY( caster)
     local player p          = GetOwningPlayer( caster)
 
     // Get units within 1500 range of position of caster
     call GroupEnumUnitsInRange( targets, cx, cy, 1500.00, null)
 
     // Go through all units of group targets
     loop
          // Take the first unit of the group
          set group_unit = FirstOfGroup( targets)
          // Repeat the loop until no units left in group
          exitwhen group_unit == null
          // If unit is alive, an enemy of caster and a hero
          if( GetWidgetLife( group_unit) > 0.405 and IsUnitEnemy( group_unit, p) and IsUnitType( group_unit, UNIT_TYPE_HERO)) then
               // Then create dummy and order it to shadowstrike unit
               set dummy = CreateUnit( p, 'hfoo', cx, cy, 270.00)
               // Add the shadowstrike
               call UnitAddAbility( dummy, 'A001')
               // Kill dummy after 2 seconds
               call UnitApplyTimedLife( dummy, 'BTLF', 2.00)
               // Order dummy to cast shadowstrike
               call IssueTargetOrder( dummy, "shadowstrike", group_unit)
          endif
          // Remove unit from group to get next unit in group
          call GroupRemoveUnit( targets, group_unit)
     endloop
 
     // Remove the leaks by destroying the group
     call DestroyGroup( targets )
     // And setting the group to null
     set targets = null
endfunction
 
// ==================================================
function InitTrig_Poison_Nova takes nothing returns nothing
     set gg_trg_Poison_Nova = CreateTrigger()
     call TriggerRegisterAnyUnitEventBJ( gg_trg_Poison_Nova, EVENT_PLAYER_UNIT_SPELL_EFFECT)
     call TriggerAddCondition          ( gg_trg_Poison_Nova, Condition( function Trig_Poison_Nova_Conditions))
     call TriggerAddAction             ( gg_trg_Poison_Nova, function Trig_Poison_Nova_Actions)
endfunction
 

InitTrig_Posion_Nova und Trig_Poison_Nova_Conditions sollten bereits aus der Einführung bekannt sein und werden
deshalb nicht weiter behandelt.
Sehen wir uns also die Aktionen an. Für unsere Nova brauchen wir:

  • eine lokale Variable für die Casting Unit
  • eine lokale Gruppen Variable, die die Einheiten im Umkreis beinhaltet
  • eine lokale Einheiten Variable, die auf eine Einheit in der Gruppe weist (ähnlich der Picked Unit im GUI )
  • eine lokale Einheiten Variable, die wir für den Dummy brauchen, der ShadowStrike zaubert

Diese 3 Dinge sind das wichtigste für unseren Zauber und werden nun also gleich zu Beginn der Aktionsfunktion
initialisiert. Wie man erkennt, wird eine Gruppe mit CreateGroup() kreiert. CreateGroup() ist eine Funktion, die als
Rückgabewert eine leere Gruppe liefert. Diese leere Gruppe speichern wir in die local group targets Variable (group ist
der Variablentyp für Einheitengruppen). Nun wollen wir diese Gruppe mit Einheiten füllen. Dies funktioniert über die
Funktion

native GroupEnumUnitsInRange takes group whichGroup, real x, real y, real radius, boolexpr filter returns nothing
 

Der erste Paramter ist die Gruppe, die aufgefüllt werden soll (im Beispiel die Gruppe ´targets´).
Der zweite und dritte Parameter sind die X und Y Koordinate, von der aus ausgewählt werden soll (im Beispiel die X und Y Koordinate der Casting Unit).
Der dritte Parameter ist der Radius, in welchem Bereich um die X, Y Koordinate also Einheiten gewählt werden sollen
(im Beispiel beträgt der Radius 1500 ).
Der letzte Parameter ist eine boolexpr, die ich persönlich nie angebe. Diese Boolean Expression wäre im GUI die
MATCHING Condition. Ich frage diese Bedingungen später mit einem if innerhalb der Schleife ab.
Wie bekommt man die X und Y Koordinate?
X und Y Koordinate bekommt man über mehrere Funktionen. Für Einheiten gibt es die Funktionen,

constant native GetUnitX            takes unit whichUnit returns real
constant native GetUnitY            takes unit whichUnit returns real

die als Parameter die Einheit erwarten, von der die jeweilige Koordinate zurückgegeben werden soll.
Für Punkte (Locations) gibt es die Funktionen,

native GetLocationX             takes location whichLocation returns real
native GetLocationY             takes location whichLocation returns real

die als Parameter den Punkt erwarten, von dem die jeweilige Koordinate zurückgegeben werden soll.

In unsrem Beispiel werden diese Koordinate gleich zu Beginn in 2 real Variablen abgespeichert.

local real cx = GetUnitX( caster )
local real cy = GetUnitY( caster )

Nun haben wir nun also unsre Gruppe voll mit Einheiten. Gehen wir nun also endlich zum interessanten Teil und
erstellen uns unsere Schleife, über die die Dummies kreiert werden, die dann Shadow Strikes zaubern.
Das Grundgerüst für eine Einheitengruppen Schleife sieht wie folgt aus:

loop
    set  = FirstOfGroup( <Gruppen Variable> )
    exithwen  == null
    // Aktionen, die ausgeführt werden sollen
    call GroupRemoveUnit( <Gruppen Variable>, <Einheiten Variable> )
endloop
 

Es wird also eine Schleife mittels ´loop´, ´endloop´ kreiert und einer Einheitenvariable wird die erste Einheit der
Gruppe zugewiesen (FirstOfGroup( group WhichGroup ) ). Dann wird überprüft, ob der Wert der Variable gleich
NULL ist (sprich, es gab keine Einheit in der Gruppe, die Funktion FirstOfGroup lieferte also null zurück).
Wenn dies der Fall ist, wird die Schleife beendet. Ansonsten werden Aktionen ausgeführt und am Ende der Aktionen
MUSS die Einheit aus der Gruppe entfernt werden, denn ansonsten würde es eine unerwünschte Endlosschleife
geben, die immer und immer wieder die Aktionen mit der ersten Einheit der Gruppe ausführt.
Die Einheit wird mit dem Aufruf der Funktion GroupRemoveUnit von der Gruppe entfernt.

Nun haben wir also den schwierigsten Teil hinter uns und es fehlt nur mehr die if Abfrage und die Erstellung des
Dummy. Im Beispiel sieht dies wie folgt aus:

if( GetWidgetLife( group_unit) > 0.405 and IsUnitEnemy( group_unit, p) and IsUnitType( group_unit, UNIT_TYPE_HERO)) then
     // Then create dummy and order it to shadowstrike unit
     set dummy = CreateUnit( p, 'hfoo', cx, cy, 270.00)
     // Add the shadowstrike
     call UnitAddAbility( dummy, 'A001')
     // Kill dummy after 2 seconds
     call UnitApplyTimedLife( dummy, 'BTLF', 2.00)
     // Order dummy to cast shadowstrike
     call IssueTargetOrder( dummy, "shadowstrike", group_unit)
endif
 

Eine If Abfrage wird also mit ´if( CONDITIONS ) then … endif´ erstellt. Als Bedingung haben wir, dass unsere
Einheit noch lebt ( GetWidgetLife( widget whichWidget ) gibt die aktuellen Leben eines Widgets (Einheit, Doodad)
zurück. Wenn dieser Wert größer 0.405 ist, lebt das Widget, ansonsten ist es tot), ob sie ein Gegner vom Besitzer der
Casting Unit ist ( p = GetOwningPlayer( caster ) ) und ob sie ein Held ist.
Da diese 3 Bedingungen mit einem ´and´ verknüft sind, müssen ALLE 3 Bedingungen erfüllt sein, damit das Ergebnis
true ist. Wenn auch nur eine dieser Bedingungen verletzt ist, ist das Gesamtergebnis false und die Aktionen nach dem
´then´ werden nicht ausgeführt.
Ist das Ergebnis true, wird nun also unser Dummy kreiert.

native CreateUnit takes player id, integer unitid, real x, real y, real face returns unit
 

Diese Funktion liefert die neu erstellte Einheit zurück.
Der erste Parameter ist der Spieler, für den die Einheit kreiert werden soll.
Der zweite Parameter ist der Ganzzahlwert der Einheit, die kreiert werden soll (im Beispiel ´hfoo´ also human footman).
Der dritte und vierte Parameter sind wieder die X und Y Koordinate, an der die Einheit erstellt werden soll.
Der fünfte Parameter ist der Sichtwinkel, wohin die neu erstellte Einheit schauen soll.
Als Ergebnis wird dann die neue Einheit zurückgeliefert. Diese speichern wir sofort in unsre Einheitenvariable
´dummy´ ab.
Die nachfolgenden Aktionen sind dann nichts mehr neues. Ein Expirationtimer wird hinzugefügt, der Shadow Strike zauber wird hinzugefügt und der Dummy wird beordert Shadow Strike auf die ausgewählte Einheit zu zaubern.
Zu guter letzt wird das If Statement noch mit einem ´endif´ beendet.

Das wäre es gewesen

Ich habe oben erwähnt, dass der Ganzzahl Wert der Einheit bei CreateUnit übergeben wird. Doch was ist dieser
Ganzzahl Wert eigentlich?
Wenn wir den Objekteditor öffnen (F6) und anschließend Strg+D drücken, erscheinen for den Namen immer
4 Zeichen. Diese 4 Zeichen sind der Ganzzahl Wert dieser Einheit, dieses Zauberspruches o.ä.und sind EINZIGARTIG.
Jedes Objekt in Warcraft3 besitzt also einen Ganzzahl Wert (sozusagen die ID des Objekts). Dieser Wert ist meistens
ein Kürzel des Namens des Objekts. Beispielsweise ´hfoo´ steht für Human FOOtman, oder ´AHfs´ steht für Ability
Human FlameStrike.
Diese Werte werden oft an Funktionen übergeben:

native UnitAddItemById       takes unit whichUnit, integer itemId returns item
native UnitAddAbility         takes unit whichUnit, integer abilityId returns boolean
native UnitApplyTimedLife takes unit whichUnit, integer buffId, real duration returns nothing
 

Zusammenfassung

  • Schleifen werden mit ´loop´, ´endloop´ kreiert

  • Einheitengruppen Variablen haben den Typ ´group´
  • Eine leere Einheitengruppe wird mit „CreateGroup()“ kreiert
  • Das Einheitenschleifen Grundgerüst
    loop
        set <Einheit> = FirstOfGroup( <Gruppe> )
        exitwhen <Einheit> == null
        call GroupRemoveUnit( <Gruppe>, <Einheit> )
    endloop
    
  • Jedes Objekt in Warcraft3 besitzt einen einzigartigen Ganzzahl Wert (ID)

ßbungsbeispiel

Um zu überprüfen, ob ihr das obige auch verstanden habt, solltet ihr selbstständig versuchen, folgenden Zauberspruch
zu realisieren.
Wie bei der Poison Nova werden Einheiten um den Zauberer ausgewählt und auf 3 von ihnen wird ein Frost Bolt
gezaubert. Ihr könnt als Grundgerüst die Poison Nova hernehmen.
Tipp: Die Anzahl der Durchläufe in der Schleife solltet ihr in einem Zähler vom Typ integer abspeichern und diesen
bei jedem Durchlauf überprüfen.

Eine Musterlösung zur Aufgabe findet ihr im RAR-Ordner.

Sonstiges:
Zur Entwicklung von Zaubersprüchen ist es oft nützlich, wenn man Textausgaben verwendet, die beispielsweise Werte
von Variablen ausgeben o.ä.. Ich verwende diese Textausgaben in der Musterlösung des ßbungsbeispiels. Damit wird
oftmals die Fehlersuche vereinfacht, wenn man sieht, wo etwas nicht den gewünschten Wert besitzt.

function BJDebugMsg takes string msg returns nothing
 
// Bsp.
call BJDebugMsg( ´Trigger ist gestartet´ )

Ebenso empfehle ich euch in der Jass Programmierung das ´JassNewGenPack´, das ihr hier findet, zu verwenden:
http://www.wc3campaigns.net/showthread.php?t=90999

Vorteile sind:

  • bessere und farbige Darstellung von Jass Codes

  • Fehlermeldungen zur Laufzeit des Tests, wenn z.B. Funktionen mit ungültigen Werten aufgerufen werden
  • Weniger Abstürze (wenn man z.B. ein ´endif´ vergisst, stürzt der Editor nicht sofort ab)
  • usw.

Ebenso empfiehlt es sich JassCraft zuzulegen, das so ziemlich alle Funktionen rechts in einer geordneten Liste ausgibt und ihre Grunddefinitionen beinhaltet wie z.B. die erwarteten Parameter der Funktion, ihr Rückgabewert oder bei komplexeren Funktionen, was genau sie tun.

http://wc3campaigns.net/showthread.php?t=80051

Beispiel-Maps

Download starten

zum Anfang

  • 29.09.2008 um 16:28
30.09. 14:00 :cn: wNv 0:5 :eu: fnatic 01.10. 16:30 :eu: MYM 2:3 :de: mTw

Dracularks Jass Einsteiger Tutorial

[nocontentad]

Dracularks Jass Einsteiger Tutorial


zum Anfang


1. Einleitung

Was wird dieses Turoial erklären?

Dieses Tutorial ist für Einsteiger, die noch nie (bzw. nur wenig) mit
Jass gearbeitet haben. Es wird erklären, wie man einen GUI-Trigger
in einen JASS-Trigger umwandelt und wie dieser neu entstandene
Trigger zu verstehen/zu lesen ist.
Weiters wird es noch eine kurze Einführung in lokale Variablen
geben.


zum Anfang


2. Der GUI-Trigger

Was ist GUI?

GUI (Graphical User Interface) ist, wie der Name schon sagt, eine
graphische Oberfläche für den Benutzer, um ihm das Arbeiten zu
vereinfachen. Im Falle des Trigger Editors sieht das wie folgt aus:

Bild:Gui-Trigger

Man sieht also in Form eines Interfaces, die verschiedenen Events,
Bedingungen und Aktionen eines Triggers, was das Lesen für Leute,
die nur wenig von Programmieren verstehen, vereinfacht.

Was ist ein Trigger?
Ein Trigger (auch Auslöser genannt) ist etwas, das auf gewisse
Sachen reagiert (Events) und dementsprechende Anweisungen
(Aktionen) ausführt, wenn die Bedingungen gelten.
Im Falle dieses Beispiels: Wenn eine Einheit den Effekt einer
Fähigkeit startet (z.B. einen Stormbolt auf eine Einheit zaubert) und
dieser Zauber Gebrüll ist, dann wird ein Footman für den Besitzer der
zaubernden Einheit an der Position des Ziels kreiert.
Dieser Einheit wird ein 2 Sekunden Timer geadded (nach 2 Sekunden
stirbt die Einheit), der Zauber Flammenschlag hinzugefügt und sie
wird beordert Flammenschlag auf die Position des Ziels zu zaubern.
Bei einem Trigger müssen also IMMER Events, (Conditions) und
Aktionen ausgeführt werden.
Der Nachteil, den dieser GUI-Trigger besitzt, sind die globalen
Variablen (Casting unit, Target unit of ability being cast, usw.).
Was sind globale Variablen und warum sind sie schlecht?
Das sind Variablen, die außerhalb von Funktionen definiert sind und
auf die man jederzeit und überall zugreifen kann. Es ist nicht schwer
zu erkennen, warum sie nicht das Ideale sind für komplexere Trigger
mit waits u.ä.
Man hat keine Garantie darauf, ob die globale Variable vor einem
wait, den gleichen Wert besitzt wie nach dem wait, da ja überall auf
sie zugegriffen werden kann und eventuell ihr Inhalt geändert
werden könnte.
Die Abhilfe? – Lokale Variablen und dafür benötigen wir Jass.


zum Anfang


3. Der JASS-Trigger

Zu allererst muss man den GUI-Trigger in einen Jass Trigger
konvertieren. Dazu wählt man oben im Triggereditor in der Kategorie
Edit „Convert to Custom Text“ aus. Eine Warnnachricht wird
auftauchen, die könnt ihr aber vernachlässigen (Sie weißt euch
lediglich auf die folgen hin, was „Convert to Custom Text“ bewirkt).
Der neu entstandene Trigger sieht dann wie folgt aus:

Bild:Jass-Trigger

Das mag auf den ersten Blick recht verwirrend aussehen, aber gehen
wir es einmal Schritt für Schritt durch.


zum Anfang


4. Die Jass-Syntax

Zuerst einmal erkennt man 3 Funktionen (Condition, Action, Event).
Ganz unten sieht man die Funktion, die den Trigger kreiert (unterhalb
des Kommentars, der mit // gekennzeichnet wird).
Dort sieht man, dass eine globale Variable gesetzt wird, mit dem
Namen gg_trg_NAME. Variablen werden mittels „set“ Statement
gesetzt. Diese gg_trg_NAME Variable wird automatisch kreiert und
verweist auf den kreierten Trigger.
Zuerst einmal wird die Funktion CreateTrigger() aufgerufen, wie
einen Wert zurückliefert, und gg_trg_NAME wird diesem Wert
zugewiesen.
Anschließend werden dem Trigger Conditions und Actions
hinzugefügt mit den Funktionsaufrufen von TriggerAddCondition und
TriggerAddAction.
Funktionen werden mittels „call“ Statement aufgerufen (außer bei
der Variablenwertsetzung, wie man es bei set gg_trg_NAME =
CreateTrigger() sieht).
Eine Funktion wird also mit call FunctionName( PARAMETERS)
aufgerufen.
PARAMETERS können nun mehrere verschiedene Variablen sein, die
an die Funktion übergeben werden, oder einfach nichts, dann
schreibt man nur die
leere Klammer ().
z.B.

call DoNothing()

Nun haben wir also den Trigger kreiert und haben ihm Bedingungen
und Aktionen hinzugefügt (Bedingungen sind immer optional und
können auch weggelassen werden). Diese InitTrig_NAME Funktion
wird eigentlich fast nie verändert. Neulinge sollte also die Finger
davon lassen.

Als nächstes schauen wir nach ganz oben, zu der
Bedingungsfunktion.
function Trig_NAME_Conditions takes nothing returns boolean
Was bedeutet das?
Nach „takes“ kommen immer die Parameter, die die Funktion
erwartet (in diesem Fall eben nothing, also nichts). „Returns“ gibt an,
was die Funktion zurückliefert (in diesem Fall einen Boolean Wert, ob
die Bedingung erfüllt ist oder nicht).
Möchte man z.B. eine Funktion erstellen, die eine Einheit, einen
Ganzzahl Wert und einen Spieler erwartet, würde das in etwa so
aussehen:

function Test takes unit u, integer i, player p returns nothing
     // irgendwelche Aktionen
endfunction
 

Parameter müssen also einmal einen Typ besitzen (unit, integer,
player, usw.), einen Namen für die Variable, die übergeben wird
(damit man innerhalb der Funktion auch damit arbeiten kann) und sie
müssen mit Beistrichen von einander getrennt sein.
Weiters sieht man, dass jede Funktion mit einem „endfunction“
abgeschlossen werden muss.

Die Bedingung selbst

if( not( GetSpellAbilityId() == 'ANbr')) then
     return false
endif
return true
 

wird leider etwas merkwürdig vom Editor umgeformt. Zu Lesen ist es
ungefähr so:
„Wenn die Zauberspruch ID nicht ???Anbr‘ ist, dann ist die Bedingung
nicht erfüllt (gibe falsch zurück, return false), ansonsten ist sie erfüllt
(gib wahr zurück, return true).

In einer Zeile könnte man es einfacher so formulieren:

return GetSpellAbilityId() == 'Anbr'
 

Warum?
Die Funktion muss ja einen boolean (Wahrheit) Wert zurückgeben,
also true oder false. Der Vergleich GetSpellAbilityId() == ???Anbr‘ ist
schon selbst ein Wahrheitswert und somit kann man diese 4 Zeilen
auf eine zusammenfassen.
Bei der Aktionsfunktion gibt es nun nicht mehr viel neues, man kann
es sich aus den obigen Erklärungen zusammenreimen, was dort
passiert.
Somit kommen wir also zu den


zum Anfang


5. Lokalen Variablen

Der Vorteil von lokalen Variablen gegenüber globalen Variablen ist,
dass sie nur innerhalb der Funktion, in der sie erstell werden,
verwendet werden können und sie somit von globalen Dingen
ausgenommen sind. Sie sind sozusagen „sicherer“ als globale
Variablen.
Wie erstellt man eine lokale Variable?
Schauen wir uns Die Aktionsfunktion an:

function Trig_Unit_Spawn_Actions takes nothing returns nothing
     call CreateNUnitsAtLoc   ( 1, 'hfoo', GetOwningPlayer( GetSpellAbilityUnit()), GetUnitLoc( GetSpellTargetUnit()), bj_UNIT_FACING)
     call UnitApplyTimedLifeBJ( 2.00, 'BTLF', GetLastCreatedUnit())
     call UnitAddAbilityBJ    ( 'AHfs', GetLastCreatedUnit())
     call IssuePointOrderLocBJ( GetLastCreatedUnit(), "flamestrike", GetUnitLoc( GetSpellTargetUnit()))
endfunction
 

Dort sieht man die globalen Variablen GetSpellAbilityUnit(), GetSpellTargetUnit(), GetLastCreatedUnit(), usw. (eigentlich sind das
keine Variablen, sondern sogenannte Getter Methoden, die eben
den Wert einer Variable zurückliefern, in dem Fall den Wert der
globalen Variablen für CastingUnit, SpellTargetUnit, LastCreatedUnit,
usw.)
Wir möchten diese Variablen nun auf lokale Variablen zuordnen.
Dazu erst einmal der Code:

function Trig_Unit_Spawn_Actions takes nothing returns nothing
     local unit   caster      = GetSpellAbilityUnit()
     local unit   target      = GetSpellTargetUnit()
     local player CasterOwner = GetOwningPlayer( caster)
     local unit   dummy       = null
 
     call CreateNUnitsAtLoc( 1, 'hfoo', CasterOwner, GetUnitLoc( target), bj_UNIT_FACING)
     set dummy = GetLastCreatedUnit()
     call UnitApplyTimedLifeBJ( 2.00, 'BTLF', dummy)
     call UnitAddAbilityBJ    ( 'AHfs', dummy)
     call IssuePointOrderLocBJ( dummy, "flamestrike", GetUnitLoc( target))
endfunction
 

Wie man sieht, müssen (!) lokale Variablen gleich als Allererstes in
der Funktion definiert werden (man muss ihnen aber noch keinen
Wert zuweisen [local unit dummy = null] ). Diesen Variablen weist
man dann beispielsweise die Werte der globalen Variablen zu (local
unit caster = GetSpellAbilityUnit()). Nun hat man also lokale
Variablen, die den Wert der globalen Variablen zu diesem Zeitpunkt
besitzen jedoch nicht mehr von globalen Einflüssen manipuliert
werden können. Somit könnte man sie auch nach waits und anderen
Dingen verwenden, ihr Wert bleibt gleich (außer man ändert ihn
selbst).
Diese lokale Variablen übergibt man nun den unterschiedlichen
Funktionen anstatt der globalen Variablen (z.B. bei local player
CasterOwner = GetOwningPlayer( caster ), übergibt man der
GetOwningPlayer Funktion die lokale Variable caster, anstatt der
vorhin globalen Variable GetSpellAbilityUnit() ).
Einen Sonderfall sehen wir nach der Funktion CreateNUnitsAtLoc.
Dort setzen wir mittels dem „set“ Statement die unit Variable mit
dem Namen dummy auf GetLastCreatedUnit() (also die Einheit, die
zuletzt kreiert wurde, In diesem Fall unser Footman). Die Variable
wurde also erst im Laufe der Funktion mit einem Wert belegt. Die
Variable GetLastCreatedUnit() wird erst innerhalb der Funktion
CreateNUnitsAtLoc mit einem Wert belegt und deswegen macht es
erst Sinn NACH dieser Funktion die dummy unit Variable zu setzen.


zum Anfang


6. Schlusswort zur Jass-Syntax

Das war nun also die Einführung in die JASS Syntax und in die lokalen
Variablen. Keine Sorge, wenn ihr das beim ersten Mal noch nicht
richtig verstandet habt. Es ist noch kein Meister vom Himmel gefallen.
Wenn man jedoch erst einmal eingeübt ist mit Jass, lassen sich damit
viele Dinge einfacher und besser lösen, als mit GUI-Triggern und auch
die Beseitigung der Memory Leaks ist damit um einiges einfacher,

Um den Locationleak im obigen Trigger zu entfernen, braucht es
beispielsweise nur 3 Zeilen mehr:

function Trig_Unit_Spawn_Actions takes nothing returns nothing
     local unit     caster      = GetSpellAbilityUnit()
     local unit     target      = GetSpellTargetUnit()
     local player   CasterOwner = GetOwningPlayer( caster)
     local unit     dummy       = null
     local location TempPoint   = GetUnitLoc( target)// Wir speichern die Location in eine Variable
 
     call CreateNUnitsAtLoc( 1, 'hfoo', CasterOwner, TempPoint, bj_UNIT_FACING)
     set dummy = GetLastCreatedUnit()
     call UnitApplyTimedLifeBJ( 2.00, 'BTLF', dummy)
     call UnitAddAbilityBJ    ( 'AHfs', dummy)
     call IssuePointOrderLocBJ( dummy, "flamestrike", TempPoint)
     call RemoveLocation      ( TempPoint)// Wir zerstören die Location
     set TempPoint = null// Wir nullen die Location den leak weiter zu minimieren
endfunction
 

Zusammenfassung

  • Ein Trigger besitzt IMMER Events, (Conditions), Actions
  • Funktionen werden wie folgt definiert

    function NAME takes PARAMETERES returns VALUE
         // Actions to do
    endfunction
     
    
  • Funktionen werden mittels „call“ Statement aufgerufen (call DoSomething() )
  • lokale Variablen werden wie folgt definiert

    local VARTYPE VARNAME = VARVALUE
    
  • Lokale Variablen müssen immer zu Beginn der Funktion definiert werden
  • Getter Methoden wie GetSpellAbilityUnit(), GetSpellTargetLoc(), u.ä. liefern den aktuellen Wert der entsprechenden globalen Variablen zurück

    local unit caster = GetSpellAbilityUnit()
    


zum Anfang


7. Einheitengruppen in JASS

Was ist eine Einheitengruppe?
Eine Einheitengruppe ist, wie der Name schon sagt, eine Gruppe von Einheiten, die in einer Variable abgespeichert
werden können. Eine Sortierung gibt es, soweit ich weiß, nicht, d.h. die Einheiten in der Gruppe sind zufällig sortiert.
Es können also beispielsweise zuerst 3 Helden in der Gruppe sein, dann 5 Einheiten, 1 Gebäude und dann wieder
Helden. Die Reihenfolge ist also unabhängig davon, in welcher Reihenfolge man Einheiten in die Gruppe hinzufügt
und vom Typ der Einheiten.

Im GUI kennen wir diese Einheitengruppen aus den Funktionen : Pick every unit in _ and do Actions. Es wird also jede
Einheit in einer bestimmten Gruppe ausgewählt und gewisse Aktionen durchgeführt. Die Aktionen können sich nun
auf die ausgewählte Einheit selbst richten (z.B. indem man ihr Schaden zufügt) oder aber man arbeitet mit der Anzahl von Durchläufen, die die Schleife besitzt (z.B. für jede Einheit in der Gruppe erhält der Held +15 Schaden).

Es ist jetzt schon öfters der Begriff Schleife gefallen, aber was ist das eigentlich?
Eine Schleife kann man sich wie einen Kreis vorstellen. Der Kreis beginnt irgendwo (die Schleife fängt irgendwo an)
,hat irgendwo ein Ende (ein Durchlauf einer Schleife endet, nachdem alle Aktionen abgehandelt wurden) und beginnt
dann wieder von neuem beim Anfang (ein erneuter Durchlauf der Schleife, bis eine Abbruchbedingung einsetzt). Dies
geschieht solange, bis eine Abbruchbedingung einsetzt, oder die Schleife vom Benutzer selbst abgebrochen wird.
Eine Abbruchbedingung kann z.B. sein, dass eine Ganzzahl einen gewissen Wert übersteigt.

Ein Beispiel für eine einfache Schleife in JASS:

loop
     exitwhen <irgendeine Zahl> >= 5
endloop
 

Eine Schleife wird also mit „loop“ gestartet und mit einem „endloop“ beendet. Das „exitwhen“ Statement gibt die
Bedingung an, bei der die Schleife enden soll (im Beispiel: eine Zahlvariable erreicht den Wert größer oder gleich 5 ).

Wie die gezeigte Schleife arbeiten auch Einheitengruppen. Es wird für jede Einheit in der Gruppe eine Aktion
ausgeführt, d.h. die Schleife beginnt mit der ersten Einheit, geht dann jede Einheit durch und wenn jede Einheit
einmal ausgewählt wurde, endet die Schleife.

Was lässt sich nun mit Einheitengruppen in JASS alles anstellen?
Ich glaube diese Frage beantwortet sich selbst, denn es ist eurer Phantasie überlassen, was ihr damit alles anstellen
wollt. Ich werde im Folgendem 2 Beispiele erklären:

  • Das erste wird eine Poison Nova sein (auf jedem feindlichen Held im Umkreis wird ein Shadowstrike gezaubert)
  • Das zweite wird ein Übungsbeispiel für euch sein. Es ist eine Erweiterung des ersten Beispiels (auf 3 zufällige, feindliche Einheiten im Umkreis wird ein Frostbolt gezaubert)
  • Beide Karten werden in einer .rar Datei angehängt

Beispiel 1

Als erstes Beispiel hab ich mir eine Poison Nova genommen. Sie wird feindliche Helden im Umkreis auswählen und
je ein Dummy wird dann einen Shadowstrike auf sie zaubern.

Im GUI würde dies in etwa so aussehen:

Bild:Jass-Trigger

Was sind die Nachteile dieser Variante?
Zum einen wieder das Problem mit den globalen Variablen. Wenn nur wenige Einheiten ausgewählt werden, wird es
wohl selten zu Problemen kommen, aber werden nun 100e Einheiten ausgewählt, kann man kaum garantieren, dass
z.B. Casting Unit noch immer die richtige Casting Unit ist.
Weiters gibt es ein Problem mit Memory Leaks (Speicher Lecks). Zum einen ist das die erstellte Einheitengruppe selbst
( Units within 1500.00 range of Position ) zum anderen ist es die Position of Casting unit, die einen weiteren Leak
verursacht. Somit also keine saube Lösung für unsren Zauberspruch.
Sehen wir uns also einen JASS-Code an, der dasselbe bewirkt, nur eben besser.

function Trig_Poison_Nova_Conditions takes nothing returns boolean
     return GetSpellAbilityId() == 'A000'
endfunction
 
function Trig_Poison_Nova_Actions takes nothing returns nothing
     local unit   caster     = GetTriggerUnit()
     local group  targets    = CreateGroup()
     local unit   group_unit = null
     local unit   dummy      = null
     local real   cx         = GetUnitX( caster)
     local real   cy         = GetUnitY( caster)
     local player p          = GetOwningPlayer( caster)
 
     // Get units within 1500 range of position of caster
     call GroupEnumUnitsInRange( targets, cx, cy, 1500.00, null)
 
     // Go through all units of group targets
     loop
          // Take the first unit of the group
          set group_unit = FirstOfGroup( targets)
          // Repeat the loop until no units left in group
          exitwhen group_unit == null
          // If unit is alive, an enemy of caster and a hero
          if( GetWidgetLife( group_unit) > 0.405 and IsUnitEnemy( group_unit, p) and IsUnitType( group_unit, UNIT_TYPE_HERO)) then
               // Then create dummy and order it to shadowstrike unit
               set dummy = CreateUnit( p, 'hfoo', cx, cy, 270.00)
               // Add the shadowstrike
               call UnitAddAbility( dummy, 'A001')
               // Kill dummy after 2 seconds
               call UnitApplyTimedLife( dummy, 'BTLF', 2.00)
               // Order dummy to cast shadowstrike
               call IssueTargetOrder( dummy, "shadowstrike", group_unit)
          endif
          // Remove unit from group to get next unit in group
          call GroupRemoveUnit( targets, group_unit)
     endloop
 
     // Remove the leaks by destroying the group
     call DestroyGroup( targets )
     // And setting the group to null
     set targets = null
endfunction
 
// ==================================================
function InitTrig_Poison_Nova takes nothing returns nothing
     set gg_trg_Poison_Nova = CreateTrigger()
     call TriggerRegisterAnyUnitEventBJ( gg_trg_Poison_Nova, EVENT_PLAYER_UNIT_SPELL_EFFECT)
     call TriggerAddCondition          ( gg_trg_Poison_Nova, Condition( function Trig_Poison_Nova_Conditions))
     call TriggerAddAction             ( gg_trg_Poison_Nova, function Trig_Poison_Nova_Actions)
endfunction
 

InitTrig_Posion_Nova und Trig_Poison_Nova_Conditions sollten bereits aus der Einführung bekannt sein und werden
deshalb nicht weiter behandelt.
Sehen wir uns also die Aktionen an. Für unsere Nova brauchen wir:

  • eine lokale Variable für die Casting Unit
  • eine lokale Gruppen Variable, die die Einheiten im Umkreis beinhaltet
  • eine lokale Einheiten Variable, die auf eine Einheit in der Gruppe weist (ähnlich der Picked Unit im GUI )
  • eine lokale Einheiten Variable, die wir für den Dummy brauchen, der ShadowStrike zaubert

Diese 3 Dinge sind das wichtigste für unseren Zauber und werden nun also gleich zu Beginn der Aktionsfunktion
initialisiert. Wie man erkennt, wird eine Gruppe mit CreateGroup() kreiert. CreateGroup() ist eine Funktion, die als
Rückgabewert eine leere Gruppe liefert. Diese leere Gruppe speichern wir in die local group targets Variable (group ist
der Variablentyp für Einheitengruppen). Nun wollen wir diese Gruppe mit Einheiten füllen. Dies funktioniert über die
Funktion

native GroupEnumUnitsInRange takes group whichGroup, real x, real y, real radius, boolexpr filter returns nothing
 

Der erste Paramter ist die Gruppe, die aufgefüllt werden soll (im Beispiel die Gruppe „targets“).
Der zweite und dritte Parameter sind die X und Y Koordinate, von der aus ausgewählt werden soll (im Beispiel die X und Y Koordinate der Casting Unit).
Der dritte Parameter ist der Radius, in welchem Bereich um die X, Y Koordinate also Einheiten gewählt werden sollen
(im Beispiel beträgt der Radius 1500 ).
Der letzte Parameter ist eine boolexpr, die ich persönlich nie angebe. Diese Boolean Expression wäre im GUI die
MATCHING Condition. Ich frage diese Bedingungen später mit einem if innerhalb der Schleife ab.
Wie bekommt man die X und Y Koordinate?
X und Y Koordinate bekommt man über mehrere Funktionen. Für Einheiten gibt es die Funktionen,

constant native GetUnitX            takes unit whichUnit returns real
constant native GetUnitY            takes unit whichUnit returns real

die als Parameter die Einheit erwarten, von der die jeweilige Koordinate zurückgegeben werden soll.
Für Punkte (Locations) gibt es die Funktionen,

native GetLocationX             takes location whichLocation returns real
native GetLocationY             takes location whichLocation returns real

die als Parameter den Punkt erwarten, von dem die jeweilige Koordinate zurückgegeben werden soll.

In unsrem Beispiel werden diese Koordinate gleich zu Beginn in 2 real Variablen abgespeichert.

local real cx = GetUnitX( caster )
local real cy = GetUnitY( caster )

Nun haben wir nun also unsre Gruppe voll mit Einheiten. Gehen wir nun also endlich zum interessanten Teil und
erstellen uns unsere Schleife, über die die Dummies kreiert werden, die dann Shadow Strikes zaubern.
Das Grundgerüst für eine Einheitengruppen Schleife sieht wie folgt aus:

loop
    set  = FirstOfGroup( <Gruppen Variable> )
    exithwen  == null
    // Aktionen, die ausgeführt werden sollen
    call GroupRemoveUnit( <Gruppen Variable>, <Einheiten Variable> )
endloop
 

Es wird also eine Schleife mittels „loop“, „endloop“ kreiert und einer Einheitenvariable wird die erste Einheit der
Gruppe zugewiesen (FirstOfGroup( group WhichGroup ) ). Dann wird überprüft, ob der Wert der Variable gleich
NULL ist (sprich, es gab keine Einheit in der Gruppe, die Funktion FirstOfGroup lieferte also null zurück).
Wenn dies der Fall ist, wird die Schleife beendet. Ansonsten werden Aktionen ausgeführt und am Ende der Aktionen
MUSS die Einheit aus der Gruppe entfernt werden, denn ansonsten würde es eine unerwünschte Endlosschleife
geben, die immer und immer wieder die Aktionen mit der ersten Einheit der Gruppe ausführt.
Die Einheit wird mit dem Aufruf der Funktion GroupRemoveUnit von der Gruppe entfernt.

Nun haben wir also den schwierigsten Teil hinter uns und es fehlt nur mehr die if Abfrage und die Erstellung des
Dummy. Im Beispiel sieht dies wie folgt aus:

if( GetWidgetLife( group_unit) > 0.405 and IsUnitEnemy( group_unit, p) and IsUnitType( group_unit, UNIT_TYPE_HERO)) then
     // Then create dummy and order it to shadowstrike unit
     set dummy = CreateUnit( p, 'hfoo', cx, cy, 270.00)
     // Add the shadowstrike
     call UnitAddAbility( dummy, 'A001')
     // Kill dummy after 2 seconds
     call UnitApplyTimedLife( dummy, 'BTLF', 2.00)
     // Order dummy to cast shadowstrike
     call IssueTargetOrder( dummy, "shadowstrike", group_unit)
endif
 

Eine If Abfrage wird also mit „if( CONDITIONS ) then … endif“ erstellt. Als Bedingung haben wir, dass unsere
Einheit noch lebt ( GetWidgetLife( widget whichWidget ) gibt die aktuellen Leben eines Widgets (Einheit, Doodad)
zurück. Wenn dieser Wert größer 0.405 ist, lebt das Widget, ansonsten ist es tot), ob sie ein Gegner vom Besitzer der
Casting Unit ist ( p = GetOwningPlayer( caster ) ) und ob sie ein Held ist.
Da diese 3 Bedingungen mit einem „and“ verknüft sind, müssen ALLE 3 Bedingungen erfüllt sein, damit das Ergebnis
true ist. Wenn auch nur eine dieser Bedingungen verletzt ist, ist das Gesamtergebnis false und die Aktionen nach dem
„then“ werden nicht ausgeführt.
Ist das Ergebnis true, wird nun also unser Dummy kreiert.

native CreateUnit takes player id, integer unitid, real x, real y, real face returns unit
 

Diese Funktion liefert die neu erstellte Einheit zurück.
Der erste Parameter ist der Spieler, für den die Einheit kreiert werden soll.
Der zweite Parameter ist der Ganzzahlwert der Einheit, die kreiert werden soll (im Beispiel ???hfoo‘ also human footman).
Der dritte und vierte Parameter sind wieder die X und Y Koordinate, an der die Einheit erstellt werden soll.
Der fünfte Parameter ist der Sichtwinkel, wohin die neu erstellte Einheit schauen soll.
Als Ergebnis wird dann die neue Einheit zurückgeliefert. Diese speichern wir sofort in unsre Einheitenvariable
„dummy“ ab.
Die nachfolgenden Aktionen sind dann nichts mehr neues. Ein Expirationtimer wird hinzugefügt, der Shadow Strike zauber wird hinzugefügt und der Dummy wird beordert Shadow Strike auf die ausgewählte Einheit zu zaubern.
Zu guter letzt wird das If Statement noch mit einem „endif“ beendet.

Das wäre es gewesen

Ich habe oben erwähnt, dass der Ganzzahl Wert der Einheit bei CreateUnit übergeben wird. Doch was ist dieser
Ganzzahl Wert eigentlich?
Wenn wir den Objekteditor öffnen (F6) und anschließend Strg+D drücken, erscheinen for den Namen immer
4 Zeichen. Diese 4 Zeichen sind der Ganzzahl Wert dieser Einheit, dieses Zauberspruches o.ä.und sind EINZIGARTIG.
Jedes Objekt in Warcraft3 besitzt also einen Ganzzahl Wert (sozusagen die ID des Objekts). Dieser Wert ist meistens
ein Kürzel des Namens des Objekts. Beispielsweise ???hfoo‘ steht für Human FOOtman, oder ???AHfs‘ steht für Ability
Human FlameStrike.
Diese Werte werden oft an Funktionen übergeben:

native UnitAddItemById       takes unit whichUnit, integer itemId returns item
native UnitAddAbility         takes unit whichUnit, integer abilityId returns boolean
native UnitApplyTimedLife takes unit whichUnit, integer buffId, real duration returns nothing
 

Zusammenfassung

  • Schleifen werden mit „loop“, „endloop“ kreiert

  • Einheitengruppen Variablen haben den Typ „group“
  • Eine leere Einheitengruppe wird mit „CreateGroup()“ kreiert
  • Das Einheitenschleifen Grundgerüst
    loop
        set <Einheit> = FirstOfGroup( <Gruppe> )
        exitwhen <Einheit> == null
        call GroupRemoveUnit( <Gruppe>, <Einheit> )
    endloop
    
  • Jedes Objekt in Warcraft3 besitzt einen einzigartigen Ganzzahl Wert (ID)

Übungsbeispiel

Um zu überprüfen, ob ihr das obige auch verstanden habt, solltet ihr selbstständig versuchen, folgenden Zauberspruch
zu realisieren.
Wie bei der Poison Nova werden Einheiten um den Zauberer ausgewählt und auf 3 von ihnen wird ein Frost Bolt
gezaubert. Ihr könnt als Grundgerüst die Poison Nova hernehmen.
Tipp: Die Anzahl der Durchläufe in der Schleife solltet ihr in einem Zähler vom Typ integer abspeichern und diesen
bei jedem Durchlauf überprüfen.

Eine Musterlösung zur Aufgabe findet ihr im RAR-Ordner.

Sonstiges:
Zur Entwicklung von Zaubersprüchen ist es oft nützlich, wenn man Textausgaben verwendet, die beispielsweise Werte
von Variablen ausgeben o.ä.. Ich verwende diese Textausgaben in der Musterlösung des Übungsbeispiels. Damit wird
oftmals die Fehlersuche vereinfacht, wenn man sieht, wo etwas nicht den gewünschten Wert besitzt.

function BJDebugMsg takes string msg returns nothing
 
// Bsp.
call BJDebugMsg( "Trigger ist gestartet“ )

Ebenso empfehle ich euch in der Jass Programmierung das „JassNewGenPack“, das ihr hier findet, zu verwenden:
http://www.wc3campaigns.net/showthread.php?t=90999

Vorteile sind:

  • bessere und farbige Darstellung von Jass Codes

  • Fehlermeldungen zur Laufzeit des Tests, wenn z.B. Funktionen mit ungültigen Werten aufgerufen werden
  • Weniger Abstürze (wenn man z.B. ein „endif“ vergisst, stürzt der Editor nicht sofort ab)
  • usw.

Ebenso empfiehlt es sich JassCraft zuzulegen, das so ziemlich alle Funktionen rechts in einer geordneten Liste ausgibt und ihre Grunddefinitionen beinhaltet wie z.B. die erwarteten Parameter der Funktion, ihr Rückgabewert oder bei komplexeren Funktionen, was genau sie tun.

http://wc3campaigns.net/showthread.php?t=80051

Beispiel-Maps

Download starten

zum Anfang

  • 29.09.2008 um 14:28
30.09. 14:00 :cn: wNv 0:5 :eu: fnatic Corugnoll analysiert nun auch in Tonform
ingame Netzwerk
Call of Duty | Counter-Strike: Global Offensive | Diablo 3 | Dota 2 | League of Legends | Quake 3 | Heroes of the Storm | Unreal Tournament | Overwatch | Starcraft 2 | Torchlight 2 | Warcraft 3 | World of Warcraft | Hearthstone | Kino, TV, Film und Promis | Spiele Datenbank

Support | | AGB |
Online Werbung | Mediadaten | Unternehmen | Karriere | Impressum

© inwave media GmbH, ingame™ ist ein eingetragenes Markenzeichen der inwave media GmbH. Verwendung von Inhalten nur mit schriftlicher Genehmigung.

Design und Umsetzung: Jan Wagner Design