vJass Spell Tutorial

vJass-Spell-Tutorial


zum Anfang


Welchen Zauber wollen wir uns denn heute basteln?

Zum Thema Feuer hatte ich mir überlegt, dass es sicherlich cool wäre irgendwie Feuerbälle über den Bildschirm fliegen zu lassen. Da der normale Feuerball Zauber a la Diablo2 imo aber schon eher an Reiz verliert, basteln wir uns etwas tolleres!
Nehmen wir als Model einmal den Bloodmage zur Verfügung. Er verkörpert ja praktisch schon das Feuer. Wenn wir uns den Bloodmage jetzt anschauen, sehen wir, dass um seinen Kopf so Kugeln herumschwirren. Das sieht doch schonmal recht interessant aus – das nehmen wir als Grundidee für unseren Zauber.
Wir lassen also Feuerbälle um unseren Helden kreisen. Damit es nicht ganz so langweilig ist, lassen wir die Feuerbälle nahe am Magier entstehen und mit der Zeit entfernen sich dann diese Feuerbälle vom Magier und umkreisen ihn in einer fixen Maximaldistanz.
Jetzt können wir noch entscheiden, was diese Feuerbälle bewirken. Ich habe mir eine einfache Kollision ausgedacht. Wenn nun also ein Feuerball mit einer gegnerischen Einheit kollidiert, explodiert er und schädigt dabei die Einheit und stunned sie für kurze Zeit. Ein Ziel, Schaden + Stun und Feuer – was würde sich da wohl besser als Dummyzauber eignen als Firebolt? Stimmt! Gar nichts, und deswegen ist Firebolt unser Dummyzauber, der dann von unseren Dummyeinheiten gezaubert wird.


zum Anfang


Die Spellidee steht – was brauchen wir noch?

Richtig! Wir brauchen noch unsere ganzen Objekte, wie Zauber, Dummyzauber, Dummyeinheit, und eine Einheit, die um den Helden kreist (unsere Feuerbälle).
Damit unser Bloodmage nicht lange herumstehen muss um den Effekt des Zaubers zu wirken, nehmen wir als Grundzauber Berserk der Troll Headhunter her. Somit kann man den Zauber auch während dem Laufen aktivieren. Ebenso haben wir einen schönen Buff in der Statusanzeige im Spiel um unsere Gegner zu informieren: „Achtung! Komm mir bloß nicht mehr zu nahe!“
Als Dummyzauber haben wir uns ja schon auf Firebolt geeignet. Kommen wir also zur Dummyeinheit. Am besten dafür geeignet ist meiner Meinung nach der Footman. Er hat keine störenden Nebeneffekt, wie ein Workersymbol oder dass man seine Acquisition Range per Trigger nicht setzen kann, etc.. Als Dummy soll er natürlich nicht anklickbar sein und erhält somit als einzige Fähigkeit Locust (deutsch: Heuschrecke). Weiters besitzt er kein Model, keinen Schatten und seine Animationsgeschwindigkeiten (für bewegen, zauber, angreifen) setzen wir alle auf 0.00, damit er sofort agieren kann.
Ebenso stellen wir seinen Angriff aus, geben ihm als Movementtype Fly, damit er nicht durch ungewünschte Kollisionsberechnungen verschoben wird.

Zu guter letzt entfernen wir noch sein Soundset, stellen seine Foodkosten und Pointvalue auf 0 und reduzieren seine Sichtweite. Eine Namensänderung wäre wohl auch nicht schlecht.
Bleibt uns also noch die Feuerballeinheit. Dazu kopieren wir am besten unseren Dummy und verpassen der so neu erstellten Einheit ein Firebolt Model -> fertig!


zum Anfang


Die Umsetzung

Hier versuchen wir also unsere vorher beschriebene Idee mit vJass in den Triggereditor zu bekommen. Dazu erstellen wir uns zuerst einen leeren Trigger und konvertieren ihn zu einem Custom Text. Als nächstes löschen wir alles, was durch das konvertieren schon automatisch miterstellt wird (das beinhaltet die action und condition funktion sowie die triggerinit funktion).
Soweit so gut – fangen wir also endlich an!
Zuerst einmal vereweigen wir uns kurz gleich zu Beginn mit ein paar Kommentaren:

  • Name des Zaubers
  • Ersteller
  • Welche Codeversion
  • Kurze Beschreibung des Zaubers
  • Eventuell benötigte Libraries o.ä.
// ***************************************************************************************************************
//*                                                                                                              *
//*                                     F I R E     P R O T E S                                                  *
//*                                               v0.01                                                          *
//*                                                                                                              *
//*                                          By: Quillraven                                                      *
//*                                                                                                              *
//*                                          Description:                                                        *
//*                               Surrounds the hero with rotating firebolts                                     *
//*                               on collision effect -> firebolt cast                                           *
//*                                                                                                              *
//*                                                                                                              *
//*                                          Requires:                                                           *
//*                                 Vexorian's TimerUtils library                                                *
//*                                                                                                              *
// ***************************************************************************************************************

So sieht das bei mir aus, aber ihr könnt natürlich gern eure eigenen Vorlieben, was das Kommentieren anbelangt, verwenden. Als Benutzer des Zaubers, sollte man eben nur gleich zu Beginn sehen, worum es geht, was man benötigt und was man verändern kann/muss.
Wie eben erwähnt, wollen wir gleich zu Beginn die Daten haben, die Benutzer unseres Zaubers verändern können. Das wären z.B. Datenwerte wie Schaden, wieviele Feuerbälle kreisen sollen, wie schnell die Feuerbälle kreisen sollen, usw..
Deswegen legen wir jetzt endlich mit dem eigentlichen Code los! Zuerst einmal erstellen wir uns ein scope

scope FireProtes initializer init

Ein scope kann man sich sozusagen als einen von außen abgetrennten Bereich vorstellen. Alle Dinge im scope, die als private deklariert sind, können nur innerehalb des scopes verwendet werden. Alle Dinge, die als public deklariert sind, können auch außerhalb des scopes verwendet werden. Dies dient v.a. der Sicherheit, dass Benutzer nicht von außen auf Funktionen des Zaubers zugreifen oder auf Zauberdaten und dadurch den Zauber zum Absturz bringen oder ähnliches.
Ein scope an sich besteht dann aus einem Namen (hier: FireProtes) und optionalen Erweiterungen. Die einzige, für uns wichtige, Erweiterung dabei ist: initializer init. Dies gibt lediglich an, dass wir innerhalb des scopes irgendwo eine Funktion definieren müssen mit Namen init. Init steht hierbei für Initialization und dient dann in unserem Beispiel zur Erstellung des Triggers.

Nachdem wir nun unser scope definiert haben, können wir jetzt endlich, die von oben genannten wichtigen Daten definieren. Dies erledigen wir in einem private globals Block. Auf diese Variablen kann also nur innerhalb des scopes zugegriffen werden und zwar in jeder Funktion, was für uns die Arbeit um einiges erleichtert.
Auch hier schaden Kommentare zu den Variablen nicht und eine eventuelle Strukturierung der Variablen.
Bei mir sieht das dann so aus:

globals
    // these are the spell configuration constants
    // and tempvariables for the unitgroup loop
    //
    // @SpellID = id of a berserk ability -> set the buffduration to 0 seconds for infinite bufftime
    // @BuffID = buffid of the berserkability
    // @DummySpellID = id of a firebolt ability
    // @MissileID = id of a unit who will rotate around the hero
    // @DummyID = id of a dummyunit who casts the stormbolt
    // @MaxMissiles = how many firebolts should rotate around the hero
    // @timerinterval = interval of spelltimer
    // @detectionaoe = when should a firebolt detect a nearby enemy
    // @distancevalues = distance between caster and hero increases in each interval
    // @speedvalues = rotationspeed of the firebolts increase in each interval
    // @TempGroup = global group for groupfunctions like ForGroup or GroupEnumUnits...
    // @Tempvars = needed for the groupfunctions
    // @beFilterFunction = filterfunction for the GroupEnumUnit call
    private constant integer SpellID = 'A00G'
    private constant integer BuffID = 'B003'
    private constant integer DummySpellID = 'A00H'
 
    private constant integer MissileID      = 'h003'
    private constant integer DummyID        = 'hgry'
 
    private constant integer MaxMissiles    = 5
 
    private constant real timerinterval     = 0.03125
 
    private constant real detectionaoe      = 80.00
 
    private constant real distanceincrement = 25.00 * timerinterval
    private constant real distancemax       = 250.00
    private constant real distancestart     = 25.00 * timerinterval
 
    private constant real speedmax          = 180.00 * timerinterval
    private constant real speedincrement    = 3.00 * timerinterval
    private constant real speedstart        = 90.00 * timerinterval
 
    private group TempGroup                 = CreateGroup()
    private unit TempUnit                   = null
    private player TempPlayer               = null
 
    private boolexpr beFilterfunction       = null
endglobals

Folgende Funktion, sowie unsere globale Variable DummyID, sollte man vermeiden in jedem Zaubertrigger zu definieren. Diese Funktionen könnte man z.B. in ein leeres Triggerblatt auslagern, das man z.B. General Spellfunctions nennt. Dort befinden sich dann alle nützliche Zauberdaten, die man in jedem Zauber verwenden möchte.
Da wir das Beispiel aber so einfach wie möglich halten möchten. Definieren wir diese Dinge im scope selbst.
Der Nachteil ist nicht schwer zu erraten – würde man z.B. die DummyID ändern, oder weitere ßberprüfungen in die IsUnitTargetable Funktion einbauen wollen, müsste man das in jedem scope machen und nicht, wie man es eigentlich haben will, einmal.

// this function detects, if a unit is targetable for a spell ( no spellimmunity/invulnerability )
private function IsUnitTargetAble takes unit WhichUnit returns boolean
    return GetUnitAbilityLevel( WhichUnit, 'Avul' ) <= 0 and GetUnitAbilityLevel( WhichUnit, 'Amim' ) <= 0
endfunction

Als nächstes definieren wir uns jetzt noch unser Zauberstruct. Ein struct ist einfach formuliert eine eigene Datenstruktur, die mehrere Variablen und Funktionen beinhaltet. Es ist also „ähnlich“ wie ein scope, mit dem Unterschied, dass man es Erstellen und Zerstören muss, um es zu verwenden. Ebenso kann man mehrere structs gleichzeitig erstellen.

In diesem Zauberstruct erleichtern wir uns nun das Leben und speichern unseren Zauberer ab, die kreisenden Feuerbälle, sowie deren Geschwindigkeit, Abstand zum Zauberer und Winkel.
Das sollte dann in etwas so aussehen

// struct for our spell
// it contains:
// - caster of the spell
// - the firebolt missiles
// - the current angle of the first missle
// - the current speed of missiles
// - the current distance of missiles
private struct spelldata
    unit caster
    unit array missile[MaxMissiles]
    real angle
    real speed
    real distance

Als nächstes definieren wir uns die create Funktion, die aufgerufen wird, wenn wir unser Zauberstruct erstellen wollen. In ihr erstellen wir automatisch unserer Feuerbälle und füllen das struct mit Daten. Als einzigen Parameter für die Funktion, übergeben wir die Zaubereinheit.
In der create Funktion wird der Speicherplatz für das struct allokiert und das fertige struct wird zurückgegeben.
Einen weiteren „Trick“, den wir verwenden, ist, dass wir die Zauberstufe der Berserkability in die Customvalue der Feuerbälle speichern. Später wird uns klar, warum wir das brauchen.

// in the create function we initialize the variablevalues
    // and create our missiles
    static method create takes unit u returns spelldata
        local spelldata data = spelldata.allocate()
        local integer i = 1
        local player p = GetOwningPlayer( u )
        local real x = GetUnitX( u )
        local real y = GetUnitY( u )
        local integer spelllevel = GetUnitAbilityLevel( data.caster, SpellID )
 
        set data.caster = u
        set data.angle = 360.00/MaxMissiles
        set data.speed = speedstart
        set data.distance = distancestart
 
        // create our missiles
        loop
            exitwhen i > MaxMissiles
            set data.missile[i-1] = CreateUnit( p, MissileID, x + data.distance * Cos( bj_DEGTORAD * data.angle * i ), y + data.distance * Sin( bj_DEGTORAD * data.angle * i ), data.angle*i )
            // we save the spelllevel in the userdata of the firebolt missle
            call SetUnitUserData( data.missile[i-1], spelllevel )
            set i = i + 1
        endloop
 
        set p = null
        return data
    endmethod

Wo etwas erstellt wird, muss natürlich auch etwas zerstört werden! Deshalb schreiben wir uns noch die destroy Funktion, die dann das Bereinigen für uns übernimmt. Sie zerstört die übrige Feuerbälle, entfernt den Berserkbuff von unserem Zauberer (da wir die Buffzeit ja auf 0 Sekunden gestellt haben) und reduziert Memoryleaks.
Ebenso schließen wir das struct damit ab.

// in the destroy function we remove the buffid of the caster
    // and destroy any missiles left
    method onDestroy takes nothing returns nothing
        local integer i = 1
 
        if( GetUnitAbilityLevel( this.caster, BuffID ) > 0 ) then
            call UnitRemoveAbility( this.caster, BuffID )
        endif
        set this.caster = null
        loop
            exitwhen i > MaxMissiles
            if( this.missile[i-1] != null and GetWidgetLife( this.missile[i-1] ) > 0.405 ) then
                call KillUnit( this.missile[i-1] )
            endif
            set i = i + 1
        endloop
    endmethod
endstruct

Als nächstes benötigen wir nun die Filterfunktion, die wir benutzen, wenn wir alle nahen Einheiten um unsere Feuerbälle auswählen. Jedoch benutzen wir de Filterfunktion nicht nur um gegnerische Einheiten rauszufiltern, sondern fügen auch gleich unsere gewünschten Aktionen hinzu. Nämlich das Erstellen der Dummyeinheit und das Zaubern des Firebolts auf das Ziel. Weiters zerstören wir dann gleich unseren Feuerball.
Damit wir auf die ganzen Daten wie Spieler, der die Feuerbälle besitzt oder den Feuerball selbst Zugriff haben, müssen wir vor dem Aufruf von GroupEnumUnitsInRange (folgt später!) unsere oben definierten globalen Variablen mit Werten füllen.
Ich verwende die TempUnit, um auf den Feuerball zu verweisen. Im Feuerball selbst haben wir auch das Zauberlevel in seiner Customvalue abgespeichert. Ebenso verwende ich TempPlayer um nicht jedesmal GetOwningPlayer( TempUnit ) aufrufen zu müssen.
Weiters erstelle ich noch einen SpecialEffect (Incinerate Special), den ich mir aus dem Dummyzauber (Firebolt) heraushole. Dort habe ich unter Art – Area Effect den Incinerate <Special> Effekt eingetragen. Wichtig ist, dass wir einen Effekt auswählen, der keine Lebensdauer besitzt, damit wir gleich DestroyEffect aufrufen können um den Memoryleak und somit den Effekt zu beseitigen.
Hier nun also der Code:

// this is our group filter function
// we "abuse" it to add actions and not only return a filter
// we check if an enemy unit is near the firebolt missile
// if so, we destroy the missile and create the dummy to cast firebolt on the detected target
// by setting the TempUnit to null, we reduce the missilecounter in the timercallback
private function FilterFunction takes nothing returns boolean
    local unit u = GetFilterUnit()
    local unit dummy = null
    local real x = 0.00
    local real y = 0.00
 
    if( IsUnitEnemy( u, TempPlayer ) and GetWidgetLife( u ) > 0.405 and IsUnitTargetAble( u ) and TempUnit != null and GetWidgetLife( TempUnit ) > 0.405 ) then
        set x = GetUnitX( u )
        set y = GetUnitY( u )
        // create our dummy to cast the thunderbolt
        set dummy = CreateUnit( TempPlayer, DummyID, x, y, 0 )
        call UnitAddAbility( dummy, DummySpellID )
        // we saved the spelllevel in the userdata of our missle in the struct create function
        call SetUnitAbilityLevel( dummy, DummySpellID, GetUnitUserData( TempUnit ) )
        // remove the dummy after 2 seconds and order it to cast firebolt
        call UnitApplyTimedLife( dummy, 'BTLF', 2.00 )
        call IssueTargetOrder( dummy, "firebolt", u )
        // kill the missile and add a special effect to the target's position
        // the effect is specified in the firebolt's Art - area of effect table ( first entry )
        call KillUnit( TempUnit )
        call DestroyEffect( AddSpecialEffect( GetAbilityEffectById( DummySpellID, EFFECT_TYPE_AREA_EFFECT, 0 ), x, y ) )
        set TempUnit = null
        set dummy = null
    endif
 
    set u = null
    return false
endfunction

Da wir die Feuerbälle kreisen lassen wollen, werden wir früher oder später auch einen Timer benötigen.
Deshalb schreiben wir uns als nächstes unserer Timer_callback Funktion, die dann periodisch vom Timer aufgerufen wird.
Wie oben in den Kommentaren bereits erwähnt, verwende ich Vexorian’s TimerUtils library. Dadurch kann ich das Zauberstruct einfach an den Timer antackern und in der callback Funktion wiederholen.
In der Funktion selbst überprüfen wir, ob unsere Zaubereinheit noch lebt und den Buff besitzt. Wenn dies nicht der Fall ist, zerstören wir das struct und geben den Timer wieder frei.
Ansonsten überprüfen wir auf nahe Gegner um unsere Feuerbälle und falls Feuerbälle noch leben, werden sie bewegt.

Der Code sollte ansonsten soweit selbsterklärend sein:

// this is the timer callback function
// we check for nearby enemy units around the firebolt missiles
// or if our caster is dead or hasn't the buff anymore
private function timer_callback takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local spelldata data = GetTimerData( t )
    local integer i = 1
    // this is our counter to count living missiles
    // if the counter becomes smaller or equal than 0, we destroy the struct and release the timer
    local integer missiles = MaxMissiles
    local real x = 0.00
    local real y = 0.00
    local real tempangle = 0.00
 
    if( data.caster != null and GetWidgetLife( data.caster ) > 0.405 and GetUnitAbilityLevel( data.caster, BuffID ) > 0 ) then
        set data.distance = RMinBJ( distancemax, data.distance + distanceincrement )
        set data.speed = RMinBJ( speedmax, data.speed + speedincrement )
        set data.angle = ModuloReal( data.angle + data.speed, 360.00 )
        set x = GetUnitX( data.caster )
        set y = GetUnitY( data.caster )
 
        // loop through all missiles and detect nearby enemies
        loop
            exitwhen i > MaxMissiles
            if( data.missile[i-1] != null and GetWidgetLife( data.missile[i-1] ) > 0.405 ) then
                set TempUnit = data.missile[i-1]
                set TempPlayer = GetOwningPlayer( TempUnit )
 
                // clear the global group to remove any units
                call GroupClear( TempGroup )
                // check for nearby enemy units around the firebolt missile
                call GroupEnumUnitsInRange( TempGroup, GetUnitX( TempUnit ), GetUnitY( TempUnit ), detectionaoe, beFilterfunction )
                if( TempUnit != null ) then
                    // no target was found -> move the missile
                    set tempangle = bj_DEGTORAD * (data.angle + (i*360/MaxMissiles))
                    call SetUnitX( TempUnit, x + data.distance * Cos( tempangle ) )
                    call SetUnitY( TempUnit, y + data.distance * Sin( tempangle ) )
                    call SetUnitFacing( TempUnit, ModuloReal( bj_RADTODEG * tempangle + 90, 360 ) )
                else
                    // there was a nearby enemy unit -> remove the counter
                    set missiles = missiles - 1
                endif
            else
                // missile is already dead -> remove the counter
                set missiles = missiles - 1
            endif
            set i = i + 1
        endloop
 
        if( missiles <= 0 ) then
            // no missiles left -> destroy the struct and release the timer
            call data.destroy()
            call ReleaseTimer( t )
        endif
    else
        // caster is dead or buff was "dispelled" -> destroy the struct and release the timer
        call data.destroy()
        call ReleaseTimer( t )
    endif
endfunction

Wir nähern uns dem Ende!
Es fehlt jetzt nur noch unsere Triggeraktionsfunktion, die das struct erstellt und den Timer startet. Ich verwende als Triggeraktionen nur Triggercondition Funktionen, da sie schneller sind. Wem das jedoch egal ist, kann natürlich auch gerne Triggeraction Funktionen verwenden.

// this is our conditions function that we've add to our trigger
// it checks, if a unit starts the berserk ability's effect
// and creates our spelldata struct and the periodic timer
private function Conditions takes nothing returns boolean
    local timer t = null
    local spelldata data = 0
    local unit u = null
 
    if ( GetSpellAbilityId() == SpellID ) then
        set u = GetTriggerUnit()
        if( GetUnitAbilityLevel( u, BuffID ) <= 0 ) then
            set data = data.create( u )
            set t = NewTimer()
            call SetTimerData( t, data )
            call TimerStart( t, timerinterval, true, function timer_callback )
        endif
        set u = null
    endif
 
    return false
endfunction

Jetzt fehlt uns nur noch die ganz zu Beginn erwähnte init Funktion und das schließen des scopes.
Zusätzlich zur Triggererstellung, erstellen wir noch die boolexpr für unseren Groupfilter.

// this is our init function
// it is called during the maploading and creates our trigger for the spell
// we only add a condition to our trigger, because it is faster then actionfunctions
// we also initialize our boolexpr filter for the groupfunction
private function init takes nothing returns nothing
    local trigger trig = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( trig, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( trig, Condition( function Conditions ) )
 
    set beFilterfunction = Condition( function FilterFunction )
endfunction
 
endscope

zum Anfang


Schlusswort

Und das wars dann auch schon!
Ich hoffe, es hat euch Spaß gemacht und falls ihr irgendwelche Fragen oder Anregungen habt, könnt ihr die mir natürlich gerne zukommen lassen (ob nun im Thread oder per pm).

Viel Erfolg wünsche ich euch weiterhin beim Zaubererstellen

lg
euer Quillraven

P.S.: Wenn der Spell bei euch nicht funktioniert könnt ihr auch die Map zum Tutorial herunterladen.

zum Anfang

  • 07.04.2009 um 00:01
Nominierte für eStars 2009 bekanntgegeben Check gewinnt AvalonOnline Masters

vJass Spell Tutorial

vJass-Spell-Tutorial


zum Anfang


Welchen Zauber wollen wir uns denn heute basteln?

Zum Thema Feuer hatte ich mir überlegt, dass es sicherlich cool wäre irgendwie Feuerbälle über den Bildschirm fliegen zu lassen. Da der normale Feuerball Zauber a la Diablo2 imo aber schon eher an Reiz verliert, basteln wir uns etwas tolleres!
Nehmen wir als Model einmal den Bloodmage zur Verfügung. Er verkörpert ja praktisch schon das Feuer. Wenn wir uns den Bloodmage jetzt anschauen, sehen wir, dass um seinen Kopf so Kugeln herumschwirren. Das sieht doch schonmal recht interessant aus – das nehmen wir als Grundidee für unseren Zauber.
Wir lassen also Feuerbälle um unseren Helden kreisen. Damit es nicht ganz so langweilig ist, lassen wir die Feuerbälle nahe am Magier entstehen und mit der Zeit entfernen sich dann diese Feuerbälle vom Magier und umkreisen ihn in einer fixen Maximaldistanz.
Jetzt können wir noch entscheiden, was diese Feuerbälle bewirken. Ich habe mir eine einfache Kollision ausgedacht. Wenn nun also ein Feuerball mit einer gegnerischen Einheit kollidiert, explodiert er und schädigt dabei die Einheit und stunned sie für kurze Zeit. Ein Ziel, Schaden + Stun und Feuer – was würde sich da wohl besser als Dummyzauber eignen als Firebolt? Stimmt! Gar nichts, und deswegen ist Firebolt unser Dummyzauber, der dann von unseren Dummyeinheiten gezaubert wird.


zum Anfang


Die Spellidee steht – was brauchen wir noch?

Richtig! Wir brauchen noch unsere ganzen Objekte, wie Zauber, Dummyzauber, Dummyeinheit, und eine Einheit, die um den Helden kreist (unsere Feuerbälle).
Damit unser Bloodmage nicht lange herumstehen muss um den Effekt des Zaubers zu wirken, nehmen wir als Grundzauber Berserk der Troll Headhunter her. Somit kann man den Zauber auch während dem Laufen aktivieren. Ebenso haben wir einen schönen Buff in der Statusanzeige im Spiel um unsere Gegner zu informieren: „Achtung! Komm mir bloß nicht mehr zu nahe!“
Als Dummyzauber haben wir uns ja schon auf Firebolt geeignet. Kommen wir also zur Dummyeinheit. Am besten dafür geeignet ist meiner Meinung nach der Footman. Er hat keine störenden Nebeneffekt, wie ein Workersymbol oder dass man seine Acquisition Range per Trigger nicht setzen kann, etc.. Als Dummy soll er natürlich nicht anklickbar sein und erhält somit als einzige Fähigkeit Locust (deutsch: Heuschrecke). Weiters besitzt er kein Model, keinen Schatten und seine Animationsgeschwindigkeiten (für bewegen, zauber, angreifen) setzen wir alle auf 0.00, damit er sofort agieren kann.
Ebenso stellen wir seinen Angriff aus, geben ihm als Movementtype Fly, damit er nicht durch ungewünschte Kollisionsberechnungen verschoben wird.

Zu guter letzt entfernen wir noch sein Soundset, stellen seine Foodkosten und Pointvalue auf 0 und reduzieren seine Sichtweite. Eine Namensänderung wäre wohl auch nicht schlecht.
Bleibt uns also noch die Feuerballeinheit. Dazu kopieren wir am besten unseren Dummy und verpassen der so neu erstellten Einheit ein Firebolt Model -> fertig!


zum Anfang


Die Umsetzung

Hier versuchen wir also unsere vorher beschriebene Idee mit vJass in den Triggereditor zu bekommen. Dazu erstellen wir uns zuerst einen leeren Trigger und konvertieren ihn zu einem Custom Text. Als nächstes löschen wir alles, was durch das konvertieren schon automatisch miterstellt wird (das beinhaltet die action und condition funktion sowie die triggerinit funktion).
Soweit so gut – fangen wir also endlich an!
Zuerst einmal vereweigen wir uns kurz gleich zu Beginn mit ein paar Kommentaren:

  • Name des Zaubers
  • Ersteller
  • Welche Codeversion
  • Kurze Beschreibung des Zaubers
  • Eventuell benötigte Libraries o.ä.
// ***************************************************************************************************************
//*                                                                                                              *
//*                                     F I R E     P R O T E S                                                  *
//*                                               v0.01                                                          *
//*                                                                                                              *
//*                                          By: Quillraven                                                      *
//*                                                                                                              *
//*                                          Description:                                                        *
//*                               Surrounds the hero with rotating firebolts                                     *
//*                               on collision effect -> firebolt cast                                           *
//*                                                                                                              *
//*                                                                                                              *
//*                                          Requires:                                                           *
//*                                 Vexorian's TimerUtils library                                                *
//*                                                                                                              *
// ***************************************************************************************************************

So sieht das bei mir aus, aber ihr könnt natürlich gern eure eigenen Vorlieben, was das Kommentieren anbelangt, verwenden. Als Benutzer des Zaubers, sollte man eben nur gleich zu Beginn sehen, worum es geht, was man benötigt und was man verändern kann/muss.
Wie eben erwähnt, wollen wir gleich zu Beginn die Daten haben, die Benutzer unseres Zaubers verändern können. Das wären z.B. Datenwerte wie Schaden, wieviele Feuerbälle kreisen sollen, wie schnell die Feuerbälle kreisen sollen, usw..
Deswegen legen wir jetzt endlich mit dem eigentlichen Code los! Zuerst einmal erstellen wir uns ein scope

scope FireProtes initializer init

Ein scope kann man sich sozusagen als einen von außen abgetrennten Bereich vorstellen. Alle Dinge im scope, die als private deklariert sind, können nur innerehalb des scopes verwendet werden. Alle Dinge, die als public deklariert sind, können auch außerhalb des scopes verwendet werden. Dies dient v.a. der Sicherheit, dass Benutzer nicht von außen auf Funktionen des Zaubers zugreifen oder auf Zauberdaten und dadurch den Zauber zum Absturz bringen oder ähnliches.
Ein scope an sich besteht dann aus einem Namen (hier: FireProtes) und optionalen Erweiterungen. Die einzige, für uns wichtige, Erweiterung dabei ist: initializer init. Dies gibt lediglich an, dass wir innerhalb des scopes irgendwo eine Funktion definieren müssen mit Namen init. Init steht hierbei für Initialization und dient dann in unserem Beispiel zur Erstellung des Triggers.

Nachdem wir nun unser scope definiert haben, können wir jetzt endlich, die von oben genannten wichtigen Daten definieren. Dies erledigen wir in einem private globals Block. Auf diese Variablen kann also nur innerhalb des scopes zugegriffen werden und zwar in jeder Funktion, was für uns die Arbeit um einiges erleichtert.
Auch hier schaden Kommentare zu den Variablen nicht und eine eventuelle Strukturierung der Variablen.
Bei mir sieht das dann so aus:

globals
    // these are the spell configuration constants
    // and tempvariables for the unitgroup loop
    //
    // @SpellID = id of a berserk ability -> set the buffduration to 0 seconds for infinite bufftime
    // @BuffID = buffid of the berserkability
    // @DummySpellID = id of a firebolt ability
    // @MissileID = id of a unit who will rotate around the hero
    // @DummyID = id of a dummyunit who casts the stormbolt
    // @MaxMissiles = how many firebolts should rotate around the hero
    // @timerinterval = interval of spelltimer
    // @detectionaoe = when should a firebolt detect a nearby enemy
    // @distancevalues = distance between caster and hero increases in each interval
    // @speedvalues = rotationspeed of the firebolts increase in each interval
    // @TempGroup = global group for groupfunctions like ForGroup or GroupEnumUnits...
    // @Tempvars = needed for the groupfunctions
    // @beFilterFunction = filterfunction for the GroupEnumUnit call
    private constant integer SpellID = 'A00G'
    private constant integer BuffID = 'B003'
    private constant integer DummySpellID = 'A00H'
 
    private constant integer MissileID      = 'h003'
    private constant integer DummyID        = 'hgry'
 
    private constant integer MaxMissiles    = 5
 
    private constant real timerinterval     = 0.03125
 
    private constant real detectionaoe      = 80.00
 
    private constant real distanceincrement = 25.00 * timerinterval
    private constant real distancemax       = 250.00
    private constant real distancestart     = 25.00 * timerinterval
 
    private constant real speedmax          = 180.00 * timerinterval
    private constant real speedincrement    = 3.00 * timerinterval
    private constant real speedstart        = 90.00 * timerinterval
 
    private group TempGroup                 = CreateGroup()
    private unit TempUnit                   = null
    private player TempPlayer               = null
 
    private boolexpr beFilterfunction       = null
endglobals

Folgende Funktion, sowie unsere globale Variable DummyID, sollte man vermeiden in jedem Zaubertrigger zu definieren. Diese Funktionen könnte man z.B. in ein leeres Triggerblatt auslagern, das man z.B. General Spellfunctions nennt. Dort befinden sich dann alle nützliche Zauberdaten, die man in jedem Zauber verwenden möchte.
Da wir das Beispiel aber so einfach wie möglich halten möchten. Definieren wir diese Dinge im scope selbst.
Der Nachteil ist nicht schwer zu erraten – würde man z.B. die DummyID ändern, oder weitere Überprüfungen in die IsUnitTargetable Funktion einbauen wollen, müsste man das in jedem scope machen und nicht, wie man es eigentlich haben will, einmal.

// this function detects, if a unit is targetable for a spell ( no spellimmunity/invulnerability )
private function IsUnitTargetAble takes unit WhichUnit returns boolean
    return GetUnitAbilityLevel( WhichUnit, 'Avul' ) <= 0 and GetUnitAbilityLevel( WhichUnit, 'Amim' ) <= 0
endfunction

Als nächstes definieren wir uns jetzt noch unser Zauberstruct. Ein struct ist einfach formuliert eine eigene Datenstruktur, die mehrere Variablen und Funktionen beinhaltet. Es ist also „ähnlich“ wie ein scope, mit dem Unterschied, dass man es Erstellen und Zerstören muss, um es zu verwenden. Ebenso kann man mehrere structs gleichzeitig erstellen.

In diesem Zauberstruct erleichtern wir uns nun das Leben und speichern unseren Zauberer ab, die kreisenden Feuerbälle, sowie deren Geschwindigkeit, Abstand zum Zauberer und Winkel.
Das sollte dann in etwas so aussehen

// struct for our spell
// it contains:
// - caster of the spell
// - the firebolt missiles
// - the current angle of the first missle
// - the current speed of missiles
// - the current distance of missiles
private struct spelldata
    unit caster
    unit array missile[MaxMissiles]
    real angle
    real speed
    real distance

Als nächstes definieren wir uns die create Funktion, die aufgerufen wird, wenn wir unser Zauberstruct erstellen wollen. In ihr erstellen wir automatisch unserer Feuerbälle und füllen das struct mit Daten. Als einzigen Parameter für die Funktion, übergeben wir die Zaubereinheit.
In der create Funktion wird der Speicherplatz für das struct allokiert und das fertige struct wird zurückgegeben.
Einen weiteren „Trick“, den wir verwenden, ist, dass wir die Zauberstufe der Berserkability in die Customvalue der Feuerbälle speichern. Später wird uns klar, warum wir das brauchen.

// in the create function we initialize the variablevalues
    // and create our missiles
    static method create takes unit u returns spelldata
        local spelldata data = spelldata.allocate()
        local integer i = 1
        local player p = GetOwningPlayer( u )
        local real x = GetUnitX( u )
        local real y = GetUnitY( u )
        local integer spelllevel = GetUnitAbilityLevel( data.caster, SpellID )
 
        set data.caster = u
        set data.angle = 360.00/MaxMissiles
        set data.speed = speedstart
        set data.distance = distancestart
 
        // create our missiles
        loop
            exitwhen i > MaxMissiles
            set data.missile[i-1] = CreateUnit( p, MissileID, x + data.distance * Cos( bj_DEGTORAD * data.angle * i ), y + data.distance * Sin( bj_DEGTORAD * data.angle * i ), data.angle*i )
            // we save the spelllevel in the userdata of the firebolt missle
            call SetUnitUserData( data.missile[i-1], spelllevel )
            set i = i + 1
        endloop
 
        set p = null
        return data
    endmethod

Wo etwas erstellt wird, muss natürlich auch etwas zerstört werden! Deshalb schreiben wir uns noch die destroy Funktion, die dann das Bereinigen für uns übernimmt. Sie zerstört die übrige Feuerbälle, entfernt den Berserkbuff von unserem Zauberer (da wir die Buffzeit ja auf 0 Sekunden gestellt haben) und reduziert Memoryleaks.
Ebenso schließen wir das struct damit ab.

// in the destroy function we remove the buffid of the caster
    // and destroy any missiles left
    method onDestroy takes nothing returns nothing
        local integer i = 1
 
        if( GetUnitAbilityLevel( this.caster, BuffID ) > 0 ) then
            call UnitRemoveAbility( this.caster, BuffID )
        endif
        set this.caster = null
        loop
            exitwhen i > MaxMissiles
            if( this.missile[i-1] != null and GetWidgetLife( this.missile[i-1] ) > 0.405 ) then
                call KillUnit( this.missile[i-1] )
            endif
            set i = i + 1
        endloop
    endmethod
endstruct

Als nächstes benötigen wir nun die Filterfunktion, die wir benutzen, wenn wir alle nahen Einheiten um unsere Feuerbälle auswählen. Jedoch benutzen wir de Filterfunktion nicht nur um gegnerische Einheiten rauszufiltern, sondern fügen auch gleich unsere gewünschten Aktionen hinzu. Nämlich das Erstellen der Dummyeinheit und das Zaubern des Firebolts auf das Ziel. Weiters zerstören wir dann gleich unseren Feuerball.
Damit wir auf die ganzen Daten wie Spieler, der die Feuerbälle besitzt oder den Feuerball selbst Zugriff haben, müssen wir vor dem Aufruf von GroupEnumUnitsInRange (folgt später!) unsere oben definierten globalen Variablen mit Werten füllen.
Ich verwende die TempUnit, um auf den Feuerball zu verweisen. Im Feuerball selbst haben wir auch das Zauberlevel in seiner Customvalue abgespeichert. Ebenso verwende ich TempPlayer um nicht jedesmal GetOwningPlayer( TempUnit ) aufrufen zu müssen.
Weiters erstelle ich noch einen SpecialEffect (Incinerate Special), den ich mir aus dem Dummyzauber (Firebolt) heraushole. Dort habe ich unter Art – Area Effect den Incinerate <Special> Effekt eingetragen. Wichtig ist, dass wir einen Effekt auswählen, der keine Lebensdauer besitzt, damit wir gleich DestroyEffect aufrufen können um den Memoryleak und somit den Effekt zu beseitigen.
Hier nun also der Code:

// this is our group filter function
// we "abuse" it to add actions and not only return a filter
// we check if an enemy unit is near the firebolt missile
// if so, we destroy the missile and create the dummy to cast firebolt on the detected target
// by setting the TempUnit to null, we reduce the missilecounter in the timercallback
private function FilterFunction takes nothing returns boolean
    local unit u = GetFilterUnit()
    local unit dummy = null
    local real x = 0.00
    local real y = 0.00
 
    if( IsUnitEnemy( u, TempPlayer ) and GetWidgetLife( u ) > 0.405 and IsUnitTargetAble( u ) and TempUnit != null and GetWidgetLife( TempUnit ) > 0.405 ) then
        set x = GetUnitX( u )
        set y = GetUnitY( u )
        // create our dummy to cast the thunderbolt
        set dummy = CreateUnit( TempPlayer, DummyID, x, y, 0 )
        call UnitAddAbility( dummy, DummySpellID )
        // we saved the spelllevel in the userdata of our missle in the struct create function
        call SetUnitAbilityLevel( dummy, DummySpellID, GetUnitUserData( TempUnit ) )
        // remove the dummy after 2 seconds and order it to cast firebolt
        call UnitApplyTimedLife( dummy, 'BTLF', 2.00 )
        call IssueTargetOrder( dummy, "firebolt", u )
        // kill the missile and add a special effect to the target's position
        // the effect is specified in the firebolt's Art - area of effect table ( first entry )
        call KillUnit( TempUnit )
        call DestroyEffect( AddSpecialEffect( GetAbilityEffectById( DummySpellID, EFFECT_TYPE_AREA_EFFECT, 0 ), x, y ) )
        set TempUnit = null
        set dummy = null
    endif
 
    set u = null
    return false
endfunction

Da wir die Feuerbälle kreisen lassen wollen, werden wir früher oder später auch einen Timer benötigen.
Deshalb schreiben wir uns als nächstes unserer Timer_callback Funktion, die dann periodisch vom Timer aufgerufen wird.
Wie oben in den Kommentaren bereits erwähnt, verwende ich Vexorian’s TimerUtils library. Dadurch kann ich das Zauberstruct einfach an den Timer antackern und in der callback Funktion wiederholen.
In der Funktion selbst überprüfen wir, ob unsere Zaubereinheit noch lebt und den Buff besitzt. Wenn dies nicht der Fall ist, zerstören wir das struct und geben den Timer wieder frei.
Ansonsten überprüfen wir auf nahe Gegner um unsere Feuerbälle und falls Feuerbälle noch leben, werden sie bewegt.

Der Code sollte ansonsten soweit selbsterklärend sein:

// this is the timer callback function
// we check for nearby enemy units around the firebolt missiles
// or if our caster is dead or hasn't the buff anymore
private function timer_callback takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local spelldata data = GetTimerData( t )
    local integer i = 1
    // this is our counter to count living missiles
    // if the counter becomes smaller or equal than 0, we destroy the struct and release the timer
    local integer missiles = MaxMissiles
    local real x = 0.00
    local real y = 0.00
    local real tempangle = 0.00
 
    if( data.caster != null and GetWidgetLife( data.caster ) > 0.405 and GetUnitAbilityLevel( data.caster, BuffID ) > 0 ) then
        set data.distance = RMinBJ( distancemax, data.distance + distanceincrement )
        set data.speed = RMinBJ( speedmax, data.speed + speedincrement )
        set data.angle = ModuloReal( data.angle + data.speed, 360.00 )
        set x = GetUnitX( data.caster )
        set y = GetUnitY( data.caster )
 
        // loop through all missiles and detect nearby enemies
        loop
            exitwhen i > MaxMissiles
            if( data.missile[i-1] != null and GetWidgetLife( data.missile[i-1] ) > 0.405 ) then
                set TempUnit = data.missile[i-1]
                set TempPlayer = GetOwningPlayer( TempUnit )
 
                // clear the global group to remove any units
                call GroupClear( TempGroup )
                // check for nearby enemy units around the firebolt missile
                call GroupEnumUnitsInRange( TempGroup, GetUnitX( TempUnit ), GetUnitY( TempUnit ), detectionaoe, beFilterfunction )
                if( TempUnit != null ) then
                    // no target was found -> move the missile
                    set tempangle = bj_DEGTORAD * (data.angle + (i*360/MaxMissiles))
                    call SetUnitX( TempUnit, x + data.distance * Cos( tempangle ) )
                    call SetUnitY( TempUnit, y + data.distance * Sin( tempangle ) )
                    call SetUnitFacing( TempUnit, ModuloReal( bj_RADTODEG * tempangle + 90, 360 ) )
                else
                    // there was a nearby enemy unit -> remove the counter
                    set missiles = missiles - 1
                endif
            else
                // missile is already dead -> remove the counter
                set missiles = missiles - 1
            endif
            set i = i + 1
        endloop
 
        if( missiles <= 0 ) then
            // no missiles left -> destroy the struct and release the timer
            call data.destroy()
            call ReleaseTimer( t )
        endif
    else
        // caster is dead or buff was "dispelled" -> destroy the struct and release the timer
        call data.destroy()
        call ReleaseTimer( t )
    endif
endfunction

Wir nähern uns dem Ende!
Es fehlt jetzt nur noch unsere Triggeraktionsfunktion, die das struct erstellt und den Timer startet. Ich verwende als Triggeraktionen nur Triggercondition Funktionen, da sie schneller sind. Wem das jedoch egal ist, kann natürlich auch gerne Triggeraction Funktionen verwenden.

// this is our conditions function that we've add to our trigger
// it checks, if a unit starts the berserk ability's effect
// and creates our spelldata struct and the periodic timer
private function Conditions takes nothing returns boolean
    local timer t = null
    local spelldata data = 0
    local unit u = null
 
    if ( GetSpellAbilityId() == SpellID ) then
        set u = GetTriggerUnit()
        if( GetUnitAbilityLevel( u, BuffID ) <= 0 ) then
            set data = data.create( u )
            set t = NewTimer()
            call SetTimerData( t, data )
            call TimerStart( t, timerinterval, true, function timer_callback )
        endif
        set u = null
    endif
 
    return false
endfunction

Jetzt fehlt uns nur noch die ganz zu Beginn erwähnte init Funktion und das schließen des scopes.
Zusätzlich zur Triggererstellung, erstellen wir noch die boolexpr für unseren Groupfilter.

// this is our init function
// it is called during the maploading and creates our trigger for the spell
// we only add a condition to our trigger, because it is faster then actionfunctions
// we also initialize our boolexpr filter for the groupfunction
private function init takes nothing returns nothing
    local trigger trig = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ( trig, EVENT_PLAYER_UNIT_SPELL_EFFECT )
    call TriggerAddCondition( trig, Condition( function Conditions ) )
 
    set beFilterfunction = Condition( function FilterFunction )
endfunction
 
endscope

zum Anfang


Schlusswort

Und das wars dann auch schon!
Ich hoffe, es hat euch Spaß gemacht und falls ihr irgendwelche Fragen oder Anregungen habt, könnt ihr die mir natürlich gerne zukommen lassen (ob nun im Thread oder per pm).

Viel Erfolg wünsche ich euch weiterhin beim Zaubererstellen

lg
euer Quillraven

P.S.: Wenn der Spell bei euch nicht funktioniert könnt ihr auch die Map zum Tutorial herunterladen.

zum Anfang

  • 06.04.2009 um 22:01
Check gewinnt AvalonOnline Masters Gui-Spell-Tutorial