Da es sich nicht um ein richtiges HowTo sondern eher um eine Einführung in die Materie handelt, sind die üblichen Angabe zu den notwendigen Hilfsmitteln und dem Zeitaufwand eher nebensächlich. Allerdings sollte man bereits einiges an Erfahrung bezüglich des Erstellens von AddOns haben, um den gesamten Umfang womöglich zu verstehen.

Der Artikel beruht auf Iriels Proof of Concept zu dem Thema.


Einleitung

Vorneweg: Was meinen wir mit kaskadierenden Menüs?
Eines der Hauptprobleme von Post-2.0-AddOns ist, dass sie im Allgemeinen mehr Knöpfe brauchen. Was früher ein einziger “smart button” erledigte, muss nun vom Spieler gesteuert werden. Das bedeutet weniger Platz im Interface und mehr Hotkeys die man sich merken muss. Kaskadierende Menüs versuchen dies zu kompensieren, indem sie Zauber und Fähigkeiten in Untermenüs gruppieren.
Will man nun beispielsweise den ersten Zauber im ersten Untermenü in Iriels AddOn ausführen, benötigt man drei Tastendrücke: F-1-1.
F öffnet das Hauptmenü, 1 das erste Untermenü und schließlich castet der zweite Druck auf 1 den ersten Zauber in diesem Menü.

Ähnliche Ansätze gibt es schon seit Längerem in AddOns, doch erst mit dem Release von Patch 2.0 gewannen sie massiv an Bedeutung. In diesem kleinen Artikel möchte ich etwas näher auf die Implementierung eines solchen Menüs in einem Post-2.0 Umfeld eingehen.

StateActionTests.xml

Die XML-Datei in Iriels AddOn ist sehr übersichtlich, hier wird lediglich ein Template auf Basis von Blizzards SecureActionButtons angelegt. Diese gehören, wie der Name bereits suggeriert, zu den sicheren UI-Elementen, das heisst sie können untainted Code auch im Kampf ausführen. Oder auf Deutsch: Mit SecureActionButtons kann man auch im Kampf noch in Luacode casten, zumindest wenn man ein paar einfache Regeln beachtet.

Allen die mit den Secure* Templates nicht vertraut sind, sei hier die Lektüre der Dokumentation in Blizzards InterfaceFrameXMLSecureTemplates.lua wärmstens empfohlen. Es folgt ein Auszug aus dieser Datei betreffend die SecureActionButtons:


— SecureActionButton

— SecureActionButtons allow you to map different combinations of modifiers and buttons into
— actions which are executed when the button is clicked.

— For example, you could set up the button to respond to left clicks by targeting the focus:
— self:SetAttribute(“unit”, “focus”);
— self:SetAttribute(“type1″, “target”);

— You could set up all other buttons to bring up a menu like this:
— self:SetAttribute(“type*”, “menu”);
— self.showmenu = menufunc;

— SecureActionButtons are also able to perform different actions depending on whether you can
— attack the unit or assist the unit associated with the action button. It does so by mapping
— mouse buttons into “virtual buttons” based on the state of the unit. For example, you can use
— the following to cast “Mind Blast” on a left click and “Shadow Word: Death” on a right click
— if the unit can be attacked:
— self:SetAttribute(“harmbutton1″, “nuke1″);
— self:SetAttribute(“type-nuke1″, “spell”);
— self:SetAttribute(“spell-nuke1″, “Mind Blast”);
— self:SetAttribute(“harmbutton2″, “nuke2″);
— self:SetAttribute(“type-nuke2″, “spell”);
— self:SetAttribute(“spell-nuke2″, “Shadow Word: Death”);

— In this example, we use the special attribute “harmbutton” which is used to map a virtual
— button when the unit is attackable. We also have the attribute “helpbutton” which is used
— when the unit can be assisted.

— Although it may not be immediately obvious, we are able to use this new virtual button
— to set up very complex click behaviors on buttons. For example, we can define a new “heal”
— virtual button for all friendly left clicks, and then set the button to cast “Flash Heal”
— on an unmodified left click and “Renew” on a ctrl left click:
— self:SetAttribute(“*helpbutton1″, “heal”);
— self:SetAttribute(“*type-heal”, “spell”);
— self:SetAttribute(“spell-heal”, “Flash Heal”);
— self:SetAttribute(“ctrl-spell-heal”, “Renew”);

— This system is very powerful, and provides a good layer of abstraction for setting up
— a button”s click behaviors.

Iriels Template setzt übrigens selbst noch keine Attribute.

Lua-Code

Stürzen wir uns also auf die StateActionTests.lua 😉
Der für uns relevante Lua-Code endet in Zeile 158, alles darunter betrifft das Gruppenbuffmenü des gleichen AddOns, auf das ich aber hier nicht weiter eingehen will. Wer mag, kann einfach sämtlichen Code unterhalb von Zeile 158 rauslöschen.

Jetzt aber zum Interessanten: Als erstes wird ein namenloses Root-Objekt angelegt:

local p = CreateFrame(“Frame”, nil, UIParent, “SecureStateHeaderTemplate”);

Das verwendete SecureStateHeaderTemplate bildet die Basis des ganzen Systems. Die sehr ausführliche Dokumentation hierzu findet sich in InterfaceFrameXMLSecureStateHeader.lua.

Es folgen die Elemente auf Basis des zuvor definierten StateActionTestTemplates.
Das SATKeyFrame-Element ignorieren wir erstmal und stürzen uns gleich auf die sichtbaren Knöpfe des Menüs. Die Namenskonvention für die Buttons ist dabei wie folgt:

  • SATR[1..3] für die Knöpfe zum Öffnen der Untermenüs
  • SATA[1..5] für die Knöpfe in den Untermenüs

Die SATA*s werden recyclet, das heisst Iriel verwendet den selben Satz Knöpfe für alle drei Untermenüs.

Neben dem Erstellen der Elemente mittels CreateFrame() werden auch gleich eine ganze Reihe von StateHeader-Attributen der einzelnen Elemente gesetzt. Diese wollen wir uns nun kurz anschauen.
Die Attribute für das Elternobjekt p sind schnell erklärt:
Es gibt 4 mögliche Zustände (States) des Headers:

  • 0: Alles unsichtbar
  • 1: Knöpfe zur Auswahl des Untermenüs (SATR*s) sichtbar
  • 2,3 und 4: Für je eines der drei Untermenüs; Alle Knöpfe (SATR*s und SATA*s) sichtbar

Zuerst werden einige Positionsangaben gemacht, in diesem Fall wird p bei einem Übergang in einen sichtbaren Zustand auf den Mauscursor gesetzt:

p:SetAttribute(“headofsx”, “1:0″); –wenn p in Zustand 1 übergeht, setze headofsx auf 0
p:SetAttribute(“headofsy”, “1:0″); –wenn p in Zustand 1 übergeht, setze headofsx auf 0
p:SetAttribute(“headofsrelpoint”, “cursor”);
–setze headofsrelpoint auf die aktuellen koordinaten des mauscursors

Außerdem werden verschiedene Keymappings für die States festgelegt. Das Setzen der delay*-Attribute bewirkt, dass das Menü verschwindet wenn der Mauscursor den Menübereich für länger als eine Sekunde verlässt, siehe hierzu auch die Beispiele in der Dokumentation des StateHeaders.

Zur Verarbeitung von Benutzereingaben wird nun ein eigenes Objekt (SATKeyFrame) erstellt. Der Grund hierfür ist einfach der, dass unser Rootobjekt vom Typ Frame ist, und daher nicht auf Eingaben reagieren kann. SATKeyFrame ist vom Typ Button und dementsprechend auch einfacher zu konfigurieren. Um Tainted Code zu vermeiden, muss SATKeyFrame allerdings von einem sicheren Template ableiten, in diesem Fall StateActionTestTemplate, dem Template das wir zuvor in der XML-File definiert haben.
Das eigentliche Zuweisen der Tastenbelegung ist nicht weiter spektakulär. Es werden lediglich zwei Eingaben überprüft: F zum Öffnen des Menüs und Escape zum Schließen. Interessant ist hier lediglich die Tatsache, dass wir nicht direkt an die States binden, sondern an die Profile die wir weiter oben im Attribut “statebindings” angegeben haben.

k:SetAttribute(“bindings-wait”, “F”);
k:SetAttribute(“bindings-top”, “F;ESCAPE:RightButton”);
k:SetAttribute(“bindings-sub”, “F;ESCAPE:RightButton”);

Zum Schluss wird SATKeyFrame noch mittels AddChild an p angehängt. Das Setzen von AddChild bewirkt übrigens implizit, das k nun ein Kind von p ist. Ein Aufruf von k:GetParent() würde also p zurückliefern.

Die folgenden Zeilen legen das Verhalten der SATR*s fest. Diese sollen kreisförmig um den Mauscursor angeordnet werden (für alle Matheverweigerer: Dafür sind die sin() und cos()-Aufrufe da 😉 ). Besonders wichtig sind hier die Zustandsübergänge: Sichtbar sind die SATR*s in den States 1 bis 4, wobei in 2 bis 4 zusätzlich noch eines der Untermenüs sichtbar sein soll. Welches Untermenü angezeigt wird, ist implizit über den Zustandsübergang definiert:

  1. for i = 1, 3 do
  2. local b = CreateFrame("Button", "SATR" .. i, nil, "StateActionTestTemplate, SecureAnchorEnterTemplate");
  3. ...
  4. b:SetAttribute("showstates", "1-4");
  5. b:SetAttribute("newstate", tostring(i+1));
  6. -Zustandsübergänge
  7. ...
  8. end

Nehmt euch die Zeit diesen Zusammenhang zu verstehen! Er ist ein grundlegender Baustein für eleganten StateHeader-Code. Der Rest des Codes dürfte selbsterklärend sein, bis auf das Setzen von “childstate”, auf das wir aber weiter unten eingehen werden.

Es folgen zwei kleine Hilfsfunktionen und dann endlich das Erstellen der SATA*s.
Auch hier passiert erstaunlich wenig, lediglich zwei Sachen bedürfen der Erklärung:
Zunächst einmal wäre da die Positionierung der SATA*s. Wie bereits erwähnt werden die Buttons recyclet, das heisst wir müssen jedes Mal wenn ein Untermenü geöffnet wird, die Position der Buttons neu setzen. Die Positionen werden in dieser Schleife berechnet:

  1. DIST = 80;
  2. local ofsx = "";
  3. local ofsy = "";
  4. for j=1, 3 do
  5. local r = roots;
  6. local subangle = (i-3) * 30;
  7. if (j == 3) then subangle = -subangle;
  8. end
  9. local angle = rad((j - 1.5) * 120 + subangle);
  10. local x = math.sin(angle) * DIST;
  11. local y = math.cos(angle) * DIST;
  12.  
  13. ofsx = ofsx .. tostring(j+1) .. ":" .. (r.x + x) .. ";";
  14. ofsy = ofsy .. tostring(j+1) .. ":" .. (r.y + y) .. ";";
  15. end

roots ist hier eine Tabelle von Referenzen auf die SATR*s. Die Strings ofsx und ofsy enthalten nach dem Ausführen der Schleife die Positionsangaben für alle States von 2 bis 4.
Diese States werden allerdings von den SATR*s gesetzt, da dort bereits die gesamte Programmlogik für das Öffnen der Untermenüs drin steckt. Die Lösung für dieses Problem ist der Aufruf von SetAttribute(“childstate”, ..) oben beim Erstellen der SATR*s! Da SATA* nicht weiss welches Menü gerade aufgeklappt ist, wird diese Information eben von SATR* durchgereicht.

Der zweite Punkt ist ein nicht ganz unwesentlicher: Wo zum Teufel werden eigentlich die Zauber gecastet? 😉
Die Lösung liegt in folgenden Zeilen verborgen:

  1. b:SetAttribute("type", "action");
  2. b:SetAttribute("action1", i);
  3. b:SetAttribute("action2", i + 5);
  4. b:SetAttribute("action3", i + 10);

Was hier passiert, ist das den SATA*s einfach die Aktionen der ActionButtons der Zauberleiste zugeteilt werden! Der Vorteil dieser Methode liegt auf der Hand: Da die ActionButtons bereits “secure” sind, können wir den damit assoziierten Code ohne Probleme auch im Kampf ausführen.

Zusammenfassung

Zugegeben, der StateHeader-Code liest sich zunächst sehr ungewohnt. Im Gegensatz zu klassischen Pre-2.0 AddOns agiert man sehr passiv. Man designt in Begriffen von Zuständen und Übergängen und arbeitet allgemein sehr viel abstrakter. Doch gerade durch diese Abstraktion wird die nötige Distanz geschaffen, die das Ausführen von tainted code unterbindet. Es wird sicherlich noch einige Zeit vergehen, ehe die eigentümliche Eleganz dieser Programmiermethode Eingang in die Praxis des Addon-Schreibens findet und einfacher wird das Entwickeln mit WoW-2.0 sicherlich nicht.
Aber eben doch ein Stückchen interessanter

zurück zur HowTo-übersicht
zurück zur Skript-Sektion