Shared Memory und Custom Events

[contentad]


Shared Memory und Custom Events

zum Anfang

Einleitung

Hier möchte ich mal die grundlegenden Systeme ´Shared Memory´ und ´Custom Events´ vorstellen, die ich seit Längerem für DWC nutze.
Unten ist Code dazu jeweils angehängt, der aber exemplarisch zu verstehen ist. Wichtiger ist das Prinzip.
Die meisten werden ihn sowieso nicht übernehmen können, weil ich bei mir viel customisiere und miteinander verwurzle,
statt das native Zeug zu nehmen. Allerdings beschrifte ich es gut genug, dass man es lesen können müsste (also vielleicht als Pseudocode vorstellen).

zum Anfang

Shared Memory

Man kann nur eine bestimmte Anzahl an HashTables nutzen[1]. Ein HashTable reicht für die meisten Sachen, deswegen möchte ich den lieber
schön aufteilen, statt mehrere zu benutzen. Nun bietet vJass die key-Funktion an, mit der man jeweils einen neuen Integer bekommt, damit man ihn dann als Schlüssel
für den HashTable benutzen kann. Allerdings bekommt man mit key nur eine eindimensionale Reihe an Zahlen, man bekommt nicht nochmal eine andere
Reihe, wo abermals alle Zahlen drin wären. Deshalb sollte die Funktion doch imho für den Haupt-Table eingesetzt werden, spezielle Tables haben dann
wohl sowieso ihre eigene Art, an Schlüssel zu kommen[2]. Außerdem fängt die key-Funktion bei 0 oder 1 mit
der Vergabe von Schlüsseln an anstatt beim Integerminimum[3],
damit sind schonmal die Hälfte der rund 4 Milliarden Slots verschossen. Das gefällt mir nicht – ich versuchs zu ändern. Des Weiteren braucht man oft
noch eine dritte Dimension im Table, weil man beispielsweise einer Einheit eine unbestimmt große Reihe an Fähigkeiten zuordnen möchte und für diese
Einheit selbst schon einen der beiden Schlüssel benötigt[4]. Also zerhacke ich die verfügbaren keys Portionen, um eine dritte Dimension reinzubekommen, so dass jede Einheit dann nicht nur
über einen key für Fähigkeiten verfügt sondern eine ganze Reihe von diesen. Damit das noch effektiv ist, mache ich das nicht wie bei Defragmentierung
dynamisch, dass ich die Größe dieser Reihe nach Bedarf immer neu setzen und mir dann einen neuen Ort suchen würde, wo es hinpässe, sondern ich mach‘ es
einfach statisch mit einer Maximalgröße. Entscheidend ist also, dass wenn ich Reihen von Werten einem Objekt zuordnen möchte, ich nur den ersten key
übergebe und alle anderen werden aus dem Positionsindex in der Reihe berechnet, sind somit für diese eine Reihe (Array) reserviert.

  1. (auch wenn man die schlecht sprengen wird, falls man nicht gerade ein System hat, dass die dynamisch alloziert oder ein arithmetisches Array davon hat)
  2. (zum Beispiel durch Multiplikation und Potenzen)
  3. -(28)4/2 = -231 = -2147483648
  4. (oder sie zumindest in einen einbindet)

Code zu Shared Memory:

Benötigt:

  • Structfollowers
  • Ein paar Basis-Globals
    //! runtextmacro Folder("BasicConstants")
        globals
            constant integer STRUCT_MAX = 8190
            constant integer STRUCT_MIN = 1
    
            constant integer STRUCT_BASE = STRUCT_MAX + 1
            constant integer STRUCT_EMPTY = STRUCT_MIN - 1
    
            constant integer NULL = STRUCT_EMPTY
        endglobals
    endscope
  • wenig Mathe
    //! runtextmacro Folder("Math")
        //! runtextmacro Struct("Integer")
            static integer MIN = -2147483645
        endstruct
    endscope
    
    //! runtextmacro StaticStruct("Math")
        //! runtextmacro LinkToStaticStruct("Math", "Integer")
    
        static method RandomI takes integer lowBound, integer highBound returns integer
            return GetRandomInt(lowBound, highBound)
        endmethod
    endstruct
  • CreateSimpleAddState (ist so mein Standard-Textmacro, um einfache abstrakte, numerische Werte als Structmembers zu erstellen)
    //! textmacro CreateSimpleAddState takes type, defaultValue
        $type$ value
    
        method Get takes nothing returns $type$
            return this.value
        endmethod
    
        method Set takes $type$ value returns nothing
            set this.value = value
        endmethod
    
        method Add takes $type$ value returns nothing
            call this.Set(this.Get() + value)
        endmethod
    
        method Start takes nothing returns nothing
            call this.Set($defaultValue$)
        endmethod
    
        method Subtract takes $type$ value returns nothing
            call this.Set(this.Get() - value)
        endmethod
    //! endtextmacro
  • GameCache und HashTable (muss ich hier nicht unbedingt aufführen, ist ersichtlich)

Code:

//! runtextmacro Folder("Memory")
    //! runtextmacro Folder("IntegerKeys")
        //! runtextmacro Struct("Array")
            static constant integer EMPTY = -1
            static constant integer OFFSET = 8192
            static constant integer SIZE = 8192  //das heißt, dass man in einem Array maximal 8192 Werte speichern kann, die Zahl macht Sinn wegen dem Arraysize-Limit, von Structs kann man ja auch nur etwa so viele Instanzen haben
            static constant integer STARTED = 0  //im ersten Index wird die Anzahl der Elemente im Array gekennzeichnet

            //! textmacro Memory_IntegerKeys_Array_CreateType takes name, type, bugConverter
                static method Count$name$s takes integer parentKey, integer key returns integer
                    return (thistype.EMPTY + Memory.IntegerKeys.GetInteger(parentKey, key))
                endmethod

                static method Count$name$sByHandle takes handle handleSource, integer key returns integer
                    return Count$name$s(GetHandleId(handleSource), key)
                endmethod

                static method Get$name$ takes integer parentKey, integer key, integer index returns $type$
                    return Memory.IntegerKeys.Get$name$(parentKey, key + thistype.EMPTY + index + 2)
                endmethod

                static method Get$name$ByHandle takes handle handleSource, integer key, integer index returns $type$
                    return Get$name$(GetHandleId(handleSource), key, index)
                endmethod

                static method Contains$name$ takes integer parentKey, integer key, $type$ value returns boolean
                    local integer iteration = Count$name$s(parentKey, key)

                    loop
                        exitwhen (iteration < thistype.STARTED)

                        exitwhen (Get$name$(parentKey, key, iteration) == value)

                        set iteration = iteration - 1
                    endloop

                    if (iteration < thistype.STARTED) then
                        return false
                    endif

                    return true
                endmethod

                static method Add$name$ takes integer parentKey, integer key, $type$ value returns boolean
                    local integer count = Count$name$s(parentKey, key) + 1

                    call Memory.IntegerKeys.SetInteger(parentKey, key, count - thistype.EMPTY)

                    call Memory.IntegerKeys.Set$name$(parentKey, key + thistype.EMPTY + count + 2, value)

                    return (count == thistype.STARTED)
                endmethod

                static method Add$name$ByHandle takes handle handleSource, integer key, $type$ value returns boolean
                    return Add$name$(GetHandleId(handleSource), key, value)
                endmethod

                static method Remove$name$ takes integer parentKey, integer key, $type$ value returns boolean
                    local integer count = Count$name$s(parentKey, key)

                    local integer iteration = count

                    loop
debug                        exitwhen (iteration < thistype.STARTED)

                        exitwhen (Get$name$(parentKey, key, iteration) == value)

                        set iteration = iteration - 1
                    endloop

debug                    if (iteration < thistype.STARTED) then

debug                        call BJDebugMsg("Failed to remove "+$BugConverter$(value)+" from array "+I2S(key)+" of parentKey "+I2S(parentKey)+" ("+I2S(count)+")")

debug                    else
                    call Memory.IntegerKeys.Set$name$(parentKey, key + thistype.EMPTY + iteration + 2, Get$name$(parentKey, key, count))

                    set count = count - 1

                    call Memory.IntegerKeys.SetInteger(parentKey, key, count - thistype.EMPTY)
debug                    endif

                    return (count == thistype.EMPTY)
                endmethod

                static method Remove$name$ByHandle takes handle handleSource, integer key, $type$ value returns boolean
                    return Remove$name$(GetHandleId(handleSource), key, value)
                endmethod

                static method Random$name$ takes integer parentKey, integer key, integer lowerBound, integer higherBound returns $type$
                    return Get$name$(parentKey, key, Math.RandomI(lowerBound, higherBound))
                endmethod

                static method Random$name$ByHandle takes handle handleSource, integer key, integer lowerBound, integer higherBound returns $type$
                    return Random$name$(GetHandleId(handleSource), key, lowerBound, higherBound)
                endmethod

                static method Random$name$All takes integer parentKey, integer key returns $type$
                    return Random$name$(parentKey, key, thistype.STARTED, Count$name$s(parentKey, key))
                endmethod
            //! endtextmacro

            //! runtextmacro Memory_IntegerKeys_Array_CreateType("Boolean", "boolean", "B2S")
            //! runtextmacro Memory_IntegerKeys_Array_CreateType("Integer", "integer", "I2S")
            //! runtextmacro Memory_IntegerKeys_Array_CreateType("Real", "real", "R2S")
            //! runtextmacro Memory_IntegerKeys_Array_CreateType("String", "string", "")
        endstruct
    endscope

    //! runtextmacro Struct("IntegerKeys")
        static HashTable CACHE

        //! runtextmacro LinkToStruct("IntegerKeys", "Array")

        //! runtextmacro InitLinksToStruct_Start()
        //! runtextmacro InitLinksToStruct_NewMember("Array")
        //! runtextmacro InitLinksToStruct_Ending()

        static method RemoveChild takes integer parentKey returns nothing
            call CACHE.RemoveMission(parentKey)
        endmethod

        //! textmacro Memory_IntegerKeys_CreateType takes name, type
            static method Set$name$ takes integer parentKey, integer key, $type$ value returns nothing
                call CACHE.$name$.Set(parentKey, key, value)
            endmethod

            static method Set$name$ByHandle takes handle handleSource, integer key, $type$ value returns nothing
                call Set$name$(GetHandleId(handleSource), key, value)
            endmethod

            static method Remove$name$ takes integer parentKey, integer key returns nothing
                call CACHE.$name$.Remove(parentKey, key)
            endmethod

            static method Remove$name$ByHandle takes handle handleSource, integer key returns nothing
                call Remove$name$(GetHandleId(handleSource), key)
            endmethod

            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            static method Get$name$ takes integer handleSource, integer key returns $type$
                return CACHE.$name$.Get(handleSource, key)
            endmethod

            static method Get$name$ByHandle takes handle handleSource, integer key returns $type$
                return Get$name$(GetHandleId(handleSource), key)
            endmethod
        //! endtextmacro

        //! runtextmacro Memory_IntegerKeys_CreateType("Boolean", "boolean")
        //! runtextmacro Memory_IntegerKeys_CreateType("Integer", "integer")
        //! runtextmacro Memory_IntegerKeys_CreateType("Real", "real")
        //! runtextmacro Memory_IntegerKeys_CreateType("String", "string")

        static method Init takes nothing returns nothing
            set CACHE = HashTable.Create()
        endmethod
    endstruct

    //! runtextmacro Folder("StringKeys")
        //! runtextmacro Struct("Array")
            static constant integer EMPTY = -1
            static constant integer STARTED = 0

            //! textmacro Memory_StringKeys_Array_CreateType takes name, type, bugConverter
                static method Count$name$s takes string parentKey, string key returns integer
                    return (thistype.EMPTY + Memory.StringKeys.GetInteger(parentKey, key))
                endmethod

                static method Get$name$ takes string parentKey, string key, integer index returns $type$
                    return Memory.StringKeys.Get$name$(parentKey, key + I2S(thistype.EMPTY + index + 2))
                endmethod

                static method Contains$name$ takes string parentKey, string key, $type$ value returns boolean
                    local integer iteration = Count$name$s(parentKey, key)

                    loop
                        exitwhen (iteration < thistype.STARTED)

                        exitwhen (Get$name$(parentKey, key, iteration) == value)

                        set iteration = iteration - 1
                    endloop

                    if (iteration < thistype.STARTED) then
                        return false
                    endif

                    return true
                endmethod

                static method Add$name$ takes string parentKey, string key, $type$ value returns boolean
                    local integer count = Count$name$s(parentKey, key) + 1

                    call Memory.StringKeys.SetInteger(parentKey, key, count - thistype.EMPTY)

                    call Memory.StringKeys.Set$name$(parentKey, key + I2S(thistype.EMPTY + count + 2), value)

                    return (count == thistype.STARTED)
                endmethod

                static method Remove$name$ takes string parentKey, string key, $type$ value returns boolean
                    local integer count = Count$name$s(parentKey, key)

                    local integer iteration = count

                    loop
debug                        exitwhen (iteration < thistype.STARTED)

                        exitwhen (Get$name$(parentKey, key, iteration) == value)

                        set iteration = iteration - 1
                    endloop

debug                    if (iteration < thistype.STARTED) then

debug                        call BJDebugMsg("Failed to remove "+$BugConverter$(value)+" from array "+key+" of parentKey "+parentKey+" ("+I2S(count)+")")

debug                    else
                    call Memory.StringKeys.Set$name$(parentKey, key + I2S(thistype.EMPTY + iteration + 2), Get$name$(parentKey, key, count))

                    set count = count - 1

                    call Memory.StringKeys.SetInteger(parentKey, key, count)
debug                    endif

                    return (count == thistype.EMPTY)
                endmethod

                static method Random$name$ takes string parentKey, string key returns $type$
                    return Get$name$(parentKey, key, Math.RandomI(thistype.STARTED, Count$name$s(parentKey, key)))
                endmethod
            //! endtextmacro

            //! runtextmacro Memory_StringKeys_Array_CreateType("Boolean", "boolean", "B2S")
            //! runtextmacro Memory_StringKeys_Array_CreateType("Integer", "integer", "I2S")
            //! runtextmacro Memory_StringKeys_Array_CreateType("Real", "real", "R2S")
            //! runtextmacro Memory_StringKeys_Array_CreateType("String", "string", "")
        endstruct
    endscope

    //! runtextmacro Struct("StringKeys")
        static GameCache CACHE

        //! runtextmacro LinkToStruct("StringKeys", "Array")

        //! runtextmacro InitLinksToStruct_Start()
        //! runtextmacro InitLinksToStruct_NewMember("Array")
        //! runtextmacro InitLinksToStruct_Ending()

        static method RemoveChild takes string parentKey returns nothing
            call CACHE.RemoveMission(parentKey)
        endmethod

        //! textmacro Memory_StringKeys_CreateType takes name, type
            static method Set$name$ takes string parentKey, string key, $type$ value returns nothing
                call CACHE.$name$.Set(parentKey, key, value)
            endmethod

            static method Remove$name$ takes string parentKey, string key returns nothing
                call CACHE.$name$.Remove(parentKey, key)
            endmethod

            static method Get$name$ takes string parentKey, string key returns $type$
                return CACHE.$name$.Get(parentKey, key)
            endmethod
        //! endtextmacro

        //! runtextmacro Memory_StringKeys_CreateType("Boolean", "boolean")
        //! runtextmacro Memory_StringKeys_CreateType("Integer", "integer")
        //! runtextmacro Memory_StringKeys_CreateType("Real", "real")
        //! runtextmacro Memory_StringKeys_CreateType("String", "string")

        static method Init takes nothing returns nothing
            set CACHE = GameCache.Create("blub")
        endmethod
    endstruct
endscope

//! runtextmacro StaticStruct("Memory")
    //! runtextmacro LinkToStaticStruct("Memory", "IntegerKeys")
    //! runtextmacro LinkToStaticStruct("Memory", "StringKeys")

    static method Init takes nothing returns nothing
        call IntegerKeys.Init()
        call StringKeys.Init()
    endmethod
endstruct

Damit bekommt man dann einen neuen Key oder ein Array von Keys für den Shared Memory:<(p>

//! textmacro GetKey takes name
    static key $name$_BASE

    static constant integer $name$ = Math.Integer.MIN + $name$_BASE
//! endtextmacro

//! textmacro GetKeyArray takes name
    static key $name$_BASE

    static constant integer $name$ = Math.Integer.MIN + Memory.IntegerKeys.Array.OFFSET + $name$_BASE * Memory.IntegerKeys.Array.SIZE
//! endtextmacro

Eigentlich könnte man sich GetKey sparen und stattdessen immer GetKeyArray nehmen, weil der key so und so verpulvert wird, finds aber semantisch ordentlicher.

Implementierung:

Und so kann mans noch in ein Struct implementieren, damit es automatisch als Parentkey verwendet wird (also man der Structinstanz Werte zuordnen kann) und man ihn nicht jedesmal selbst hinschreiben muss:

Makros dafür:

//! textmacro Data_Create takes baseType
    method Destroy takes nothing returns nothing
        call Memory.IntegerKeys.RemoveChild($baseType$(this).Id.Get())
    endmethod
//! endtextmacro

//! textmacro Data_Type_Create takes baseType, whichTypeName, whichType
    method Remove takes integer key returns nothing
        call Memory.IntegerKeys.Remove$whichTypeName$($baseType$(this).Id.Get(), key)
    endmethod

    method Get takes integer key returns $whichType$
        return Memory.IntegerKeys.Get$whichTypeName$($baseType$(this).Id.Get(), key)
    endmethod

    method Set takes integer key, $whichType$ value returns nothing
        call Memory.IntegerKeys.Set$whichTypeName$($baseType$(this).Id.Get(), key, value)
    endmethod
//! endtextmacro

//! textmacro Data_Array_Create takes baseType, whichTypeName, whichType
    method Count takes integer key returns integer
        return Memory.IntegerKeys.Array.Count$whichTypeName$s($baseType$(this).Id.Get(), key)
    endmethod

    method Remove takes integer key, $whichType$ value returns boolean
        return Memory.IntegerKeys.Array.Remove$whichTypeName$($baseType$(this).Id.Get(), key, value)
    endmethod

    method Get takes integer key, integer index returns $whichType$
        return Memory.IntegerKeys.Array.Get$whichTypeName$($baseType$(this).Id.Get(), key, index)
    endmethod

    method Add takes integer key, $whichType$ value returns boolean
        return Memory.IntegerKeys.Array.Add$whichTypeName$($baseType$(this).Id.Get(), key, value)
    endmethod

    method Random takes integer key, integer lowerBound, integer higherBound returns $whichType$
        return Memory.IntegerKeys.Array.Random$whichTypeName$($baseType$(this).Id.Get(), key, lowerBound, higherBound)
    endmethod

    method RandomAll takes integer key returns $whichType$
        return this.Random(key, Memory.IntegerKeys.Array.STARTED, this.Count(key))
    endmethod
//! endtextmacro

Beispiel an Unit:

//! runtextmacro Folder("Unit")
    //! runtextmacro Folder("Data")
        //! runtextmacro Folder("Integer")
            //! runtextmacro Struct("Array")
                //! runtextmacro Data_Array_Create("Unit", "Integer", "integer")
            endstruct
        endscope

        //! runtextmacro Struct("Boolean")
            //! runtextmacro Data_Type_Create("Unit", "Boolean", "boolean")
        endstruct

        //! runtextmacro Struct("Integer")
            //! runtextmacro LinkToStruct("Integer", "Array")

            //! runtextmacro InitLinksToStruct_Start()
            //! runtextmacro InitLinksToStruct_NewMember("Array")
            //! runtextmacro InitLinksToStruct_Ending()

            //! runtextmacro Data_Type_Create("Unit", "Integer", "integer")
        endstruct
    endscope

    //! runtextmacro Struct("Data")
        //! runtextmacro LinkToStruct("Data", "Boolean")
        //! runtextmacro LinkToStruct("Data", "Integer")

        //! runtextmacro InitLinksToStruct_Start()
        //! runtextmacro InitLinksToStruct_NewMember("Boolean")
        //! runtextmacro InitLinksToStruct_NewMember("Integer")
        //! runtextmacro InitLinksToStruct_Ending()

        //! runtextmacro Data_Create("Unit")
    endstruct

    //! runtextmacro Struct("Id")
        //! runtextmacro GetKeyArray("KEY_ARRAY")

        //! runtextmacro CreateSimpleAddState("integer", "KEY_ARRAY + this")
    endstruct
endscope

//! runtextmacro BaseStruct("Unit", "UNIT")
    //! runtextmacro LinkToStruct("Unit", "Data")
    //! runtextmacro LinkToStruct("Unit", "Id")

    static method Create takes <params> returns thistype
        local unit self = <unit>
        local thistype this = thistype.allocate()

        set this.self = self

        call this.Id.Start()  //muss aufgerufen werden, damit die Einheit ihren Parentkey im Memory bekommt und man dann weitere Daten daran festhalten kann

        return this
    endmethod
endstruct

Aufruf:

Tjo, und wenn man es nun also benutzen möchte:

struct Eis
    //! runtextmacro GetKey("KEY")
    //! runtextmacro GetKeyArray("KUGELN_KEY_ARRAY")

    Waffle base

    method AddToUnit takes Unit whichUnit returns nothing
        call whichUnit.Data.Integer.Set(KEY, this)
    endmethod

    method AddKugel takes Eissorte whichType returns nothing
        call this.Data.Integer.Array.Add(KUGELN_KEY_ARRAY, whichType)
    endmethod

    static method Create takes nothing returns thistype
        local thistype this = thistype.allocate()

        set this.base = Waffle.Create()

        return this
    endmethod
endstruct

zum Anfang

Custom Events

Die Zaubererstellung bietet sich hier wieder einmal als Beispiel: Nehmen wir an, eine Einheit soll gebufft werden. Also nicht ein normaler Buff aus dem Objekt-Editor, sondern es soll vielleicht ein Timer einmal nach 10 Sekunden ablaufen und um die Einheit wird dann Flächenschaden ausgeteilt. Wie üblich soll der Buff unterbrochen werden, wenn die Einheit zwischendurch stirbt. Nun machen die meisten das so, dass sie ein „A unit Stirbt“-Ereignis anlegen und dann in einer Bedingung abfragen, ob die gestorbene Einheit den Buff besitzt. Wenn ja, wird er entfernt, wenn nicht, wird abgebrochen. Bei dieser Methode sehe ich zwei Nachteile. Zum Einen finde ich es unperformant und unschön, dass man generell mal für jede Einheit den Trigger startet, die nichts damit zu tun hat. Zum anderen möchte man manchmal gerne die Reihenfolge festlegen, in der bestimmte Codeteile ausgelöst werden. Man stelle sich vor, es gäbe jetzt noch einen zweiten Trigger, der die Einheit beim Tod sofort löscht und wenn der als Erstes auslöst, könnte man nicht mehr auslesen, dass die Einheit den Buff hat und damit auch nicht das Zubehör wie Timer entfernen. Der Timer könnte es höchstens noch am Ende checken, was auch wieder unschön ist und darauf ankommt, was man so aus einer zerstörten Einheit noch in Erfahrung bringen kann.

Deshalb nehme ich einen anderen Ansatz: Der dynamische Codeaufruf. Dabei würde ich der Einheit zu Beginn des Buffs den Codeschnipsel zuordnen, den sie bei ihrem Ableben triggern soll.

Was gibt es für Möglichkeiten, dynamisch Code aufzurufen?

Ein großes Manko ist ja, dass man keine Arrays vom Variablentyp code anlegen kann und auch andersweitig kann man einen code-Wert nicht direkt zuordnen. Was man aber machen kann, ist die nativen Container BoolExpr und TriggerAction zu benutzen. Diese ruft man dann über diverse Funktionen auf:

über BoolExpr:

TriggerEvaluate, die verschiedenen Enum-Funktionen für Destructables/Items/Players/Units, die And/Or/Not-Funktionen und wenn ein Trigger über ein Event gestartet wird, löst es zuvor per TriggerAddCondition angegebene BoolExprs aus, bei manchen Events kann man bei der Erstellung dieser auch noch einen eigenen BoolExpr angeben

über TriggerAction:

TriggerExecute oder ebenfalls, wenn der Trigger durch ein Event gestartet wurde

Eine weitere Möglichkeit wäre noch, eine Funktion nicht über einen code-Parameter auslösen zu lassen, sondern ExecuteFunc kanns auch und nimmt den Funktionsnamen als String.

1.1 Code zu Shared Memory:

Support für GameCache ist auch gleich noch drin. Das finde ich immer noch ganz nützlich, um Werte an Strings zu attachen.

Benötigt:

  • Shared Memory (siehe oben)
  • auch hier wieder Structfollowers
  • auch hier wieder Basis-Globals
    //! runtextmacro Folder("BasicConstants")
        globals
            constant integer STRUCT_MAX = 8190
            constant integer STRUCT_MIN = 1
    
            constant integer STRUCT_BASE = STRUCT_MAX + 1
            constant integer STRUCT_EMPTY = STRUCT_MIN - 1
    
            constant integer NULL = STRUCT_EMPTY
        endglobals
    endscope
  • analog zum Custom Type GameCache/HashTable verwende ich hier einen Wrapper für Trigger, den ich jetzt aber nicht angeben muss

Code:

//! runtextmacro Folder("Event")
    //! runtextmacro Struct("Id")
        //! runtextmacro GetKeyArray("KEY_ARRAY")

        //! runtextmacro CreateSimpleAddState("integer", "KEY_ARRAY + this")
    endstruct

    //! runtextmacro Folder("Data")
        //! runtextmacro Folder("Integer")
            //! runtextmacro Struct("Array")
                //! runtextmacro Data_Array_Create("Event", "Integer", "integer")
            endstruct
        endscope

        //! runtextmacro Struct("Boolean")
            //! runtextmacro Data_Type_Create("Event", "Boolean", "boolean")
        endstruct

        //! runtextmacro Struct("Integer")
            //! runtextmacro LinkToStruct("Integer", "Array")

            //! runtextmacro InitLinksToStruct_Start()
            //! runtextmacro InitLinksToStruct_NewMember("Array")
            //! runtextmacro InitLinksToStruct_Ending()

            //! runtextmacro Data_Type_Create("Event", "Integer", "integer")

            static method Init takes nothing returns nothing
                call thistype.InitStructLinks()
            endmethod
        endstruct
    endscope

    //! runtextmacro Struct("Data")
        //! runtextmacro LinkToStruct("Data", "Boolean")
        //! runtextmacro LinkToStruct("Data", "Integer")

        //! runtextmacro InitLinksToStruct_Start()
        //! runtextmacro InitLinksToStruct_NewMember("Boolean")
        //! runtextmacro InitLinksToStruct_NewMember("Integer")
        //! runtextmacro InitLinksToStruct_Ending()

        //! runtextmacro Data_Create("Event")

        static method Init takes nothing returns nothing
            call thistype.InitStructLinks()

            call thistype(NULL).Integer.Init()
        endmethod
    endstruct
endscope

//! runtextmacro BaseStruct("EventPriorities", "EVENT_PRIORITIES")
    static integer ALL_AMOUNT
    static integer ALL_COUNT = ARRAY_EMPTY

    static thistype array ALL

    static thistype AI
    static thistype HEADER
    static thistype SPELLS


    static method Create takes nothing returns thistype
        local thistype this = thistype.allocate()

        set thistype.ALL_COUNT = thistype.ALL_COUNT + 1

        set thistype.ALL[thistype.ALL_COUNT] = this

        return this
    endmethod

    static method Init takes nothing returns nothing
        set thistype.HEADER = thistype.Create()

        set thistype.AI = thistype.Create()

        set thistype.SPELLS = thistype.Create()

        set thistype.ALL_AMOUNT = thistype.ALL_COUNT + 1
    endmethod
endstruct

//! runtextmacro BaseStruct("EventTypes", "EVENT_TYPES")
    static thistype DEATH
    static thistype DEATH_AS_KILLER

    static method Create takes nothing returns thistype
        return thistype.allocate()
    endmethod

    static method Init takes nothing returns nothing
        set thistype.DEATH = thistype.Create()
        set thistype.DEATH_AS_KILLER = thistype.Create()
    endmethod
endstruct

//! runtextmacro BaseStruct("Event", "EVENT")
    //! runtextmacro GetKey("KEY")
    static constant integer KEY_ARRAY = 1  //ein 8192er-Keyarray reicht hier lange nicht aus (siehe die GetKey-Funktion unten) und da ich auch ständig neue EventTypen und manchmal auch Priorities anlege, habe ich einfach mal großzügig abgeschätzt und den Events einen ganz anderen Speicherbereich gewidmet, bei 40 Typen und 6 Priorities braucht man schon 1,6Mio Steckplätze
    //! runtextmacro GetKey("STATICS_PARENT_KEY")

    Trigger action
    EventPriority priority
    BoolExpr whichConditions
    EventType whichType

    //! runtextmacro LinkToStruct("Event", "Data")
    //! runtextmacro LinkToStruct("Event", "Id")

    method GetAction takes nothing returns Trigger
        return this.action
    endmethod

    method GetConditions takes nothing returns BoolExpr
        return this.whichConditions
    endmethod

    method GetPriority takes nothing returns integer
        return this.priority
    endmethod

    method GetType takes nothing returns integer
        return this.whichType
    endmethod

    static method GetKey takes EventType whichType, EventPriority priority returns integer
        return (KEY_ARRAY + Memory.IntegerKeys.Array.SIZE * ((whichType - 1) * Priorities.ALL_AMOUNT + (priority - 1)))
    endmethod

    static method CountAtStatics takes EventType whichType, EventPriority priority returns integer
        return Memory.IntegerKeys.Array.CountIntegers(STATICS_PARENT_KEY, GetKey(whichType, priority))
    endmethod

    static method GetFromStatics takes EventType whichType, EventPriority priority, integer index returns thistype
        return Memory.IntegerKeys.Array.GetInteger(STATICS_PARENT_KEY, GetKey(whichType, priority), index)
    endmethod

    method AddToStatics takes nothing returns nothing
        call Memory.IntegerKeys.Array.AddInteger(STATICS_PARENT_KEY, GetKey(this.whichType, this.priority), this)
    endmethod

    method Run takes nothing returns nothing
        if (this.GetConditions().Run()) then
            call this.GetAction().Run()
        endif
    endmethod

    method SetAction takes code actionFunction returns nothing
        local Trigger action = this.GetAction()

        if (action != NULL) then
            call action.Clear()
        endif

        call action.Data.Integer.Set(KEY, this)
        call action.AddCode(actionFunction)
    endmethod

    method SetConditions takes BoolExpr whichConditions returns nothing
        set this.whichConditions = whichConditions
    endmethod

    static method Create takes EventType whichType, EventPriority priority, code actionFunction returns thistype
        local Trigger action = Trigger.Create()
        local thistype this = thistype.allocate()

        set this.action = action
        set this.priority = priority
        set this.whichConditions = NULL
        set this.whichType = whichType
        call this.Id.Start()

        call this.SetAction(actionFunction)

        return this
    endmethod

    static method Init takes nothing returns nothing
        call EventPriorities.Init()
        call EventTypes.Init()
    endmethod
endstruct

Erklärungen: Jedes Event hat einen Typ und eine Priority. Der Typ sagt aus, zu welchem Zeitpunkt das Event gerunnt wird, indem das Basis-Codestück eben die ganze Liste der Events mit seinen zugehörigen Typen durchgeht. Diesen Basis-Code sieht man gleich unten. Die Priority sagt genauer aus, in welcher Reihenfolge diese zum gleichen Zeitpunkt stattfindenden Ereignisse auslösen. Dann benötigt das Event natürlich noch die Aktion, die es aufrufen will. Da diese drei Sachen grundlegend sind, habe ich sie also alle für die Create-Funktion verlangt. Optional kann man noch einen BoolExpr angeben, ähnlich wie bei TriggerConditions werden die Aktionen nur gestartet, wenn die Conditions-Funktion true zurückgibt. Außerdem habe ich noch ein paar Dinge weggestrichen, die die Sache erweitern würden, jetzt aber erstmal nicht relevant sind.

Implementierung:

//! runtextmacro Folder("Unit")
    //! runtextmacro Struct("Event")
        //! textmacro Unit_Event_CreateResponse takes var, func
            static Unit $var$ = NULL

            static method Get$func$ takes nothing returns Unit
                return thistype.$var$
            endmethod

            static method Set$func$ takes Unit parent returns nothing
                set thistype.$var$ = parent
            endmethod
        //! endtextmacro

        //! runtextmacro Unit_Event_CreateResponse("TARGET", "Target")
        //! runtextmacro Unit_Event_CreateResponse("TRIGGER", "Trigger")

        method Count takes EventType whichType, EventPriority priority returns integer
            return Unit(this).Data.Integer.Array.Count(Event.GetKey(whichType, priority))
        endmethod

        method Get takes EventType whichType, EventPriority priority, integer index returns Event
            return Unit(this).Data.Integer.Array.Get(Event.GetKey(whichType, priority), index)
        endmethod

        method Remove takes Event whichEvent returns nothing
            call Unit(this).Data.Integer.Array.Remove(Event.GetKey(whichEvent.GetType(), whichEvent.GetPriority()), whichEvent)
        endmethod

        method Add takes Event whichEvent returns nothing
            call Unit(this).Data.Integer.Array.Add(Event.GetKey(whichEvent.GetType(), whichEvent.GetPriority()), whichEvent)
        endmethod
    endstruct
endscope

Aufruf:

//! runtextmacro BaseStruct("BuffSpell", "BUFF_SPELL")
    static Event DEATH_EVENT
    //! runtextmacro GetKey("KEY")
    //! runtextmacro GetKeyArray("KEY_ARRAY")

    Timer durationTimer
    Unit target

    method Ending takes Timer durationTimer, Unit target returns nothing
        call this.deallocate()
        call durationTimer.Data.Integer.Remove(KEY)
        call durationTimer.Destroy()
        if (target.Data.Integer.Array.Remove(KEY_ARRAY, this)) then
            call target.Event.Remove(DEATH_EVENT)
        endif
    endmethod

    static method EndingByTimer takes nothing returns nothing
        local Timer durationTimer = Timer.GetExpired()

        local thistype this = durationTimer.Data.Integer.Get(KEY)

        call this.Ending(durationTimer, this.target)
    endmethod

    static method Event_Death takes nothing returns nothing
        local integer iteration = target.Data.Integer.Array.Count(KEY_ARRAY)
        local thistype this

        loop
            set this = target.Data.Integer.Array.Get(KEY_ARRAY, iteration)

            call this.Ending(this.durationTimer, target)

            set iteration = iteration - 1
            exitwhen (iteration < Memory.IntegerKeys.Table.STARTED)
        endloop
    endmethod

    static method Start takes nothing returns nothing
        local Unit target = UNIT.Event.GetTarget()
        local Timer durationTimer = Timer.Create()
        local thistype this = thistype.allocate()

        set this.durationTimer = durationTimer
        set this.target = target
        call durationTimer.Data.Integer.Set(KEY, this)
        if (target.Data.Integer.Array.Add(KEY_ARRAY, this)) then
            call target.Event.Add(DEATH_EVENT)
        endif

        call durationTimer.Start(DURATION, false, function thistype.EndingByTimer)
    endmethod

    static method Init takes nothing returns nothing
        set DEATH_EVENT = Event.Create(EventType.DEATH, EventPriority.SPELLS, function thistype.Event_Death)
    endmethod
endstruct

Eine Basis für ein Event könnte dann so aussehen:

//! runtextmacro BaseStruct("UnitDies", "UNIT_DIES")
    static method TriggerEvents takes Unit killer, Unit whichUnit returns nothing
        local integer iteration = EVENT.Priorities.ALL_COUNT
        local integer iteration2

        local EventPriority priority

        loop
            exitwhen (iteration < ARRAY_MIN)

            set priority = EVENT.Priorities.ALL[iteration]

            set iteration2 = Event.CountAtStatics(EVENT.Types.DEATH, priority)

            loop
                exitwhen (iteration2 < Memory.IntegerKeys.Array.STARTED)

                call UNIT.Event.SetKiller(killer)
                call UNIT.Event.SetTrigger(whichUnit)

                call Event.GetFromStatics(EVENT.Types.DEATH, priority, iteration2).Run()

                set iteration2 = iteration2 - 1
            endloop

            set iteration2 = killer.Event.Count(EVENT.Types.DEATH_AS_KILLER, priority)

            loop
                exitwhen (iteration2 < Memory.IntegerKeys.Array.STARTED)

                call UNIT.Event.SetTarget(whichUnit)
                call UNIT.Event.SetTrigger(killer)

                call killer.Event.Get(EVENT.Types.DEATH_AS_KILLER, priority, iteration2).Run()

                set iteration2 = iteration2 - 1
            endloop

            set iteration2 = whichUnit.Event.Count(EVENT.Types.DEATH, priority)

            loop
                exitwhen (iteration2 < Memory.IntegerKeys.Array.STARTED)

                call UNIT.Event.SetKiller(killer)
                call UNIT.Event.SetTrigger(whichUnit)

                call whichUnit.Event.Get(EVENT.Types.DEATH, priority, iteration2).Run()

                set iteration2 = iteration2 - 1
            endloop

            set iteration = iteration - 1
        endloop
    endmethod
endstruct

Diese Funktion müsste beim Tod einer Einheit aufgerufen werden. Die äußere Schleife geht die Priorities nacheinander durch, die Inneren nehmen dann alle Events unter dieser Priority. Es sind hier jetzt drei innere Schleifen, weil man sieht, ich nehme nicht nur Events, die der sterbenden Einheit zugeordnet wurden (das wollten wir ja oben im Buffbeispiel), sondern schaue auch noch, ob der Killer „UnitKillsUnit“-Ereignisse hat und Static-Events sind einfach solche, die nicht bestimmten Objekten zugeordnet wurden, sondern unabhängig davon immer feuern.

Das mit der Priority-Schleife könnte man noch eleganter lösen, indem man der Einheit beim Zuordnen der Events auch gleich noch anhängt, welche Priorities nun mindestens 1 Event haben, die anderen könnte man dann ja vernachlässigen.

Was sind nun die Zeilen wie

                call UNIT.Event.SetTrigger(whichUnit)

?

Nun, das sind einfach unsere Ereignis-Reaktionen. Da wir jetzt also Codeteile dynamisch aufrufen, damit einen neuen Thread starten, sind dort natürlich nicht mehr die nativen Ereignisreaktionen bekannt. Finds sowieso unsicher, manche Dinge werden ja überschrieben und so, deswegen speichere ich Ereignis-Reaktionen wenn möglich immer gleich in lokale Variablen. Das sollte man dann auch im neuen Thread tun. Außerdem hätte ich jetzt nicht sowas wie „UnitDies“ nehmen müssen, wofür es sowieso schon einen Ereignistyp gibt. Ich hätte auch „UnitIsDispelled“ erfinden können, muss ich nur irgendwo den Ansatz dazu haben. Bei SpawnWave-Maps wie DWC ists zum Beispiel auch nützlich, ein „WaveStarts“/“WaveEnds“-Event zu haben, weil ich die Funktionen, die dabei auslösen, halt ordentlich trennen will und das Starten eines neuen Threads hat noch den Vorteil, dass die unabhängig voneinander sind, wenn es um das gefürchtete Operationslimit geht. Zudem ist das noch besser, wenn irgendwo aufgrund eines Fehlers ein Thread abbricht, betrifft das noch nicht gleich die anderen Funktionalitäten. Damit kann man also besser den Standort von Bugs bestimmen.

Sensitive Events:

So, ein was möchte ich jetzt am Ende noch von den weggestrichenen Sachen andeuten. Neben einfachen Events wie „UnitDies“ gibt es ja noch solche wie „Unit’s mana becomes greater than x“. Da werde ich mich jetzt natürlich hüten, Ereignistypen MANA_GREATER_0, MANA_GREATER_1, MANA_GREATER_2, …, MANA_GREATER_9999 anzulegen. Stattdessen würde ich dem Event noch die geforderte Manamenge zuordnen und am besten auch gleich noch den Vergleichsoperator. Da können es also durchaus noch mehr Parameter werden oder man erstellt gleich einen neuen erweiterten Typ. Das hier wäre vielleicht ein „LimitEvent“.

zum Anfang

Schlusswort

Najo, denkt mal drüber nach. Das hier ist natürlich nur für fortgeschrittene Benutzer und komplexe Maps gedacht, wo man wirklich auf die Ordnung achten muss. Aber wie gesagt, ich verwende es gerade bei DWC, und komme sehr gut damit zurecht. Das Starten eines neuen Threads kostet zwar etwas mehr, als wenn man alles zusammenstopft, ist aber zum Entwickeln besser und im Gegensatz zu verteilten Blizzardevents müsste das hier in der Masse performanter sein, weil hier halt mit steigendem Content nicht zwangsläufig immer mehr Code gerunnt wird.

Ein weiterführendes Thema, das ich vielleicht mal später vorstelle, sind „EventCombinations“, die finde ich gerade für AI-Triggering interessant, sind aber etwas komplizierter und da bin ich auch noch nicht wirklich durch.

zum Anfang

WaterKnight

  • 18.12.2010 um 20:59
RGBL-Finale mit Videostream Map Special: Castle Fight
Comments
Du musst eingeloggt sein, um kommentieren zu können!