GUI Leitfaden

Aus DGL Wiki
Version vom 10. März 2009, 19:05 Uhr von DGLBot (Diskussion | Beiträge) (Der Ausdruck ''<pascal>(.*?)</pascal>'' wurde ersetzt mit ''<source lang="pascal">$1</source>''.)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

Einleitung

Wer ein etwas komplexeres Spiel mit einer 3D-API entwickelt wird früher oder später an dem Punkt angelangt sein, an dem er eine graphische Benutzeroberfläche (engl. "GUI" - Graphical User Interface) benötigt um dem Spieler wichtige Daten zu präsentieren, und um diesen mit dem Spiel interagieren zu lassen. Mit steigender Spielkomplexität wird eine gute GUI immer wichtiger, denn während z.B. ein Jump'n'Run (Beispiele wären hier Sonic, Mario) nur wenige GUI-Elemente benötigt (Punkte, Leben, Lebensenergie), die zudem meist nur als Anzeigen dienen (also keine Nutzerinteraktion erwarten), benötigen komplexe Spiele wie z.B. Strategiespiele komplexe Nutzeroberflächen um alle wichtigen Daten in ansprechender Form präsentieren zu können, und v.a. um den Spieler auch mit dem Spiel interagieren lassen zu können.

In diesem Artikel (es handelt sich hier nicht um ein Tutorial per se) werde ich mein Erfahrungen, die ich während der Entwicklung einer GUI für Projekt "W" gesammelt habe (und immer noch tue, das Projekt wird ja weiterentwickelt), zusammenfassen und so versuchen eine Leitfaden für die Erstellung einer komplexen Spiele-GUI zu geben. Der Artikel soll nicht als Anleitung gesehen werden, sondern wie gesagt als Leitfaden an dem man sich bei der Erstellung der eigenen GUI orientieren kann, nicht soll. Er soll v.a. zeigen welche Dinge es bei der Erstellung einer solchen GUI zu beachten gibt.


Warum eine eigene GUI?

Zum einen weil OpenGL keine eigene GUI-Bibliothek bietet, und zum anderen weil man für ein Spiel, das eine 3D-API nutzt, letztendlich nicht die Standard UI-Elemente des Betriebssystems nutzen kann. Diese sehen nicht nur langweilig aus, sondern deren Aussehen kann auch nur sehr rudimentär angepasst werden. Außerdem ist nicht garantiert dass diese auch über dem Renderkontext gezeichnet werden. Fertige OpenGL GUI-Bibliotheken gibts es zudem auch kaum, und für Delphi/Pascal (momentan, siehe DGLGUI) erst recht nicht.

Zudem trägt die Benutzeroberfläche viel zum "Look and Feel" eines Spiels bei, weshalb man hier je nach Komplexität letztendlich sowieso eine eigene Lösung entwickeln muss.

wiki gui ingame.jpg

(Finale GUI aus Projekt "W" - "Phase 2")


Gedanken zur GUI

Hinweis : Dieser Abschnitt ist stark von meinem persönlichen Programmierstil gekennzeichnet. Riesige UML-Diagramme oder umfangreiche Planung ist nicht mein Stil, ich programmiere gerne "on-the-fly".


Objektorientiert entwickeln

Eigentlich ein "überflüssiger" Punkt, da selbstverständlich. Bei kaum einem anderen Projekt lässt sich OOP so schön und effektiv anwenden wie bei einer GUI. Es sollte ein Basisobjekt geben (TGLGUIItem) welches alle grundlegenden Eigenschaften (Größe, Position, evtl. auch weiterführende Dinge wie Schriftart) und Funktionen (Speichern und Laden, Zeichnen, dort wo nötig mit virtual arbeiten) besitzt die sich alle GUI-Element teilen. Hier sollte man aber nicht immer den kleinsten gemeinsamen Nenner suchen, also auch ruhig die ein oder andere Eigenschaft bzw. Funktion ins Grundelement verlegen die nur von zwei oder drei abgeleiteten Elementen genutzt wird. Das kostet zwar ein wenig mehr Speicher, spart aber viel Arbeit. Zusätzlich zu den Elementen der GUI gibt es dann als Grundelement noch das Fenster TGLWindow) welches letztendlich ein Container für eben diese Objekt ist und ein paar zusätzliche Funktionen zur Nutzerinteraktion bereitstellt.


Nicht zu sehr an der Windows-UI orientieren

Auch wenn es nahe liegend wäre die eigene GUI an der UI von Windows (oder Linux) zu orientieren, sollte man davon Abstand halten. Während die Benutzeroberfläche von Windows für Anwendungen (dazu noch mehrere gleichzeitig) ausgelegt ist, soll die eigene GUI ja auch zum "Look-and-Feel" des Spiels beitragen und auch einfach zu nutzen sein. Die Windows UI ist sehr abstrakt gehalten, da es für sie keinen direkt definierten Einsatzbereich gibt, ist also entsprechend komplex. Bei der GUI für das eigene Spiel (die dann wenn überhaupt nur in weiteren eigenen Spielen genutzt wird) muss man nicht krampfhaft versuchen diese Funktionalität nachzubauen. Das kostet viel Zeit und Arbeit die man besser in andere Bereiche investiert (Gameplay, Grafik). Zudem wird man je nach Spiel auch GUI-Elemente benötigen die es so nicht in Windows gibt. In Projekt "W" gibt es z.B. ein Element namens TGLSkinnedControl welches neben einer Hintergrundtextur Unterelemente besitzt die auf einer polygonalen Auswahlkontur basieren. So hat man ein GUI-Element das quasi jede Form annehmen kann und für grafisch aufwendige GUI-Elemente genutzt werden kann, die keiner Standardform entsprechend. Man könnte ein solches Element von der Funktionalität her natürlich auch mit normalen GUI-Elementen (z.B. Buttons) nachbilden, rein optisch wäre dass dann aber weniger schön und würde den "Look-and-Feel" des Spiels stark stören.


Die GUI parallel zum Spiel entwickeln

Dieser Punkt ergänzt quasi den oberen Punkt. Wer von Anfang an einer GUI plant, die alle möglichen Elemente beinhaltet und alles möglich kann macht sich unnötig viel Arbeit. GUI-Elemente sollte man dann implementieren wenn man diese benötigt, die GUI also nur erweitern wenn dies für das Spiel benötigt wird. Man implementiert zuerst also nur einen Grundstock von Elementen die definitiv benötigt werden, z.B. Fenster, Buttons, Panels, Memos, Listboxen und erweitert dann nach und nach. Da man ja ein Grundelement hat (TGLGUIItem), ist es relativ einfach die GUI um neue Elemente zu erweitern. Das ist letztendlich weitaus effizienter als von Anfang an alle möglichen GUI-Elemente zu implementieren, die man dann letztendlich nicht benötigt.


Keine Unterscheidung zwischen Fenster und GUI

In einer frühen Version der GUI für Projekt "W" wurde noch zwischen einer Vollbild-GUI (z.B. Hauptmenü, Spielendeschirm) und den normalen Fenstern unterschieden. So etwas sollte man nicht machen, und in der aktuellen Version der GUI für PjW nutzen auch Vollbild-GUI Schirme ein Fenster als Grundlage. Allerdings ist dieses Fenster nicht bewegbar, nicht sichtbar und immer so groß wie der Viewport des Spiels. Dadurch spart man sich viel Arbeit, da man ansonsten vieles doppelt implementieren muss. Also von Anfang an gleich alles auf Fensterbasis entwickeln.


Ein erweiterbares Format verwenden

Wie im vorigen Punkt erwähnt wächst die GUI mit dem Spiel, und selbst wenn man abstrakt entwickelt und von Anfang an alles implementiert wird man irgendwann an einen Punkt kommen an dem man die GUI erweitern muss, sei es um neue Elemente, oder einfach nur eine neue Eigenschaft für ein bestehendes Element. Spätestens dann wird es sich auszahlen wenn man ein erweiterbares Format gewählt hat, denn während eigene binäre Format nur schwer zu erweitern sind lassen sich mit einem Format wie XML (für das es in Delphi direkten Support gibt) leicht Veränderungen an der GUI realisieren. Statt sein eigenes Format erweitern zu müssen, kann man in XML einfach einen neuen Attribut an einen bestehenden Knoten anhängen oder einfach neue Knoten hinzufügen. Zusätzlich ist XML (wenn man ordentlich Attribut- und Knotennamen vergibt) auch noch vom Menschen lesbar, man kann dort also schnell Fehler finden bzw. es sogar selbst von Hand erweitern.


Unterstützung für mehrere Sprachen

Hier sind natürlich nicht Programmiersprachen gemeint, sondern gesprochene Sprachen. Auch wenn man sein Projekt erstmal nur in einer Sprache veröffentlichen will, sollte man trotzdem Unterstützung für mehrere Sprachen von Anfang ein einplanen. So etwas direkt einzubinden ist kaum Aufwand, dies später nachzurüsten kann aber durchaus in Arbeit ausarten. Einfachste Methode (die ich auch in Projekt "W" verwende) ist es für alle benötigten Elemente (z.B. die Beschriftung eines Labels oder Buttons) statt eines Strings ein String array zu verwenden (eine TStringList wäre hier zu aufwendig). Wenn man dann auch noch ein erweiterbares Dateiformat wie XML verwendet, ist es mit dieser einfachen Methode kein Problem später weitere Sprachen hinzuzufügen. Zur Ausgabe von Text o.ä. greift man dann nicht mehr direkt auf die Variable zu, sondern nutzt eine Funktion (z.B. TGLLabel.GetCaption) die dann je nach eingestellter Sprache (in Projekt "W" wird die eingestellte Sprache in einer Eigenschaft von TLocalizationDB gespeichert, einer Klasse die sich um das Laden und Wechseln der Sprachen kümmert) die passende Beschriftung zurückgibt.


Von Anfang an einen GUI-Editor mit entwickeln

Auch wenn man anfänglich noch das ein oder andere Fenster seiner GUI von Hand erstellen kann (dank XML kein Problem), so sollte man doch direkt mit der Entwicklung eines GUI-Editors beginnen. Spätestens wenn man komplexere Fenster erstellen will, übersteigt der Zeitaufwand fürs manuelle Erstellen direkt in XML den Zeitaufwand für die Entwicklung eines GUI-Editors. Also am besten gleichzeitig mit der GUI auch den passenden Editor entwickeln. Hilfreich wäre es zudem wenn direkt in der GUI einige Flags für den Editormodus implementiert werden. Rein optisch wird es zwischen der Darstellung der GUI im Editor und Spiel bis auf Sichtbarkeit einiger Elemente keine Unterschiede geben, aber z.B. bei der Auswahl von Elementen sieht es im Editormodus anders aus als im Spielmodus, und während man im Spiel oft Elemente komplett ausblendet, sollten diese im Editor zumindest via Knopfdruck sichtbar zu machen sein.


Optische Anpassbarkeit und Flexibilität

In diesem Punkt wird sich die eigene GUI i.d.R. sehr stark von der des Betriebssystems abheben. Zwar bietet z.B. Windows inzwischen Unterstützung für Themes, diese gelten aber global und sind nicht wirklich flexibel. Da die eigene GUI aber oft selbst innerhalb eines einzigen Spiels optisch anpassbar sein sollte (wird sie in mehreren Spielen verwendet ist dies natürlich mandatorisch), muss entsprechende Funktionalität direkt in ihr verankert werden. Grundlegend sind hier sog. "Skins". Ein "Skin" (einen passenden deutschen Begriff gibt es hierfür leider nicht) ist im Prinzip eine Textur die alle graphischen Teile der GUI beinhaltet (Fensterrahmen, Buttonteile, Pfeile, etc.) die dann später mittels Texturemapping aus dieser Datei gesampelt werden. Da die eigenen GUI-Elemente in Einzelteilen gerendert werden müssen die entsprechenden Teile nicht 1:1 in der Textur abgelegt werden, sondern werden dann entsprechend beim Rendern des Elements mehrfach oder gestreckt aus dieser Textur gerendert.

wiki gui skin.jpgwiki gui skinnedcontrol.jpg

Außerdem sollte die GUI nicht komplett statisch sein, sprich ein Button sollte beginnen sich optisch abzuheben, wenn man den Cursor darauf positioniert. Dank OpenGL sollte dies kein Problem sein und hier sind vielfältige Effekte denkbar. In Projekt "W" z.B. ändern sich Buttons beim Mouseover (im "Skin" gibt es für Buttons je einen normalen Teil und einen hervorgehobene), Teile einer TGLSkinnedControl hingegen werden weich eingeblendet und vergrößert und vice versa. Hier sind also kaum Grenzen gesetzt, man könnte dies sogar mit einem Partikelsystem verbinden dass z.B. Funken sprühen lässt, wenn man einen Button selektiert, oder gar Flammen um diesen herum erscheinen lässt. Animationen sind natürlich auch denkbar. Auch hier sollte man sich keinesfalls an Windows orientieren, sondern die Möglichkeiten nutzen die OpenGl bietet. Die GUI für ein Spiel muss nicht immer rein praktischer Natur sein (je nach Spiel darf sie das auch gar nicht sein, man denke an Spiele die die GUI direkt ins Gameplay integrieren), hier kann man ruhig mal etwas verspieltes machen.


Nachrichtenbehandlung (aka Messagehandling)

In diesem Punkt darf man sich ruhig ein wenig an Windows orientieren, allerdings nicht zu sehr, eine Nachrichtenbehandlung für die eigene GUI muss selten so komplex sein wie eines Betriebssystems. Denn die eigene GUI muss nur auf die Ereignisse einer einzelnen Anwendung reagieren, und nicht auf dutzende geöffnete Anwendungen verteilen. Daher ist diese recht einfach zu implementieren. Im Normalfall sollte es reichen wenn eine Nachricht an alle offenen Fenster weitergereicht wird (in Projekt "W" geschieht dies über einen Callback, siehe weiter unten). Wenn also ein Button gedrückt wird, erzeugt dieser eine Nachricht die dann an einen globalen Verteiler (TGUI.GlobalWindowMessage) übergeben wird der diese dann an die Fenster weiterleitet. Innerhalb des entsprechenden Callbacks (TGLWindow.MessageCallBack) wird dann überprüft ob die Nachricht für dieses Fenster vorgesehen ist und wenn ja, wird diese auch hier verarbeitet. Klingt sehr einfach und ist es auch. Mehr wird man aber selten benötigen.


Callbacks verwenden

Besonders Anfänger haben gerne mal Probleme mit sog. Callbacks, aber wer etwas fortgeschritten ist lernt diese zu schätzen, tragen sie doch stark zur Flexibilität einer Programmiersprache bei und erlauben es z.B. Zirkelbezüge (zwei Units die gegenseitig auf sich zugreifen wollen) zu umschiffen. Für größere Projekte also fast unumgänglich, und in einer GUI für ein Spiel quasi ein Muss. Warum? Weil man nur so die benötigte (optische) Flexibiliät bekommt die man benötigt, denn nicht immer reicht es wenn man z.B. eine Iconbox (in meinem Falle ein Objekt ähnlich einer Listbox, nur mit Texturen statt Einträgen) einfach alle Texturen rendern lässt. In bestimmten Fällen will man evtl. noch zusätzliche Informationen anzeigen. Ein gutes Beispiel ist die Liste der Spione auf dem Screenshot am Anfang des Artikels. Dahinter steckt eine TGLIconBox die normalerweise nur die Personalbilder darstellen würde. Dank eines Callbacks der (wenn zugewiesen) bei jedem Rendern eines solchen Bildes aufgerufen wird kann man aber bequem mehr Informationen rendern (Erfahrung, Zusatzsymbole, Name). Ohne Callbacks wäre das kaum, bzw. nur über Umwege machbar. Aber nicht nur für optische Anpassungen sollten Callback verwendet werden, sondern auch um auf Ereignisse zu reagieren. In Projekt "W" hat jedes Fenster einen festen Satz von Callbacks :

 TGLWindow = class
 ...
 BeforeItemDrawCallBack : TGLWindowContentCallBack;      // Wird aufgerufen bevor GUI Elemente gezeichnet werden
 AfterItemDrawCallBack  : TGLWindowContentCallBack;      // Wird aufgerufen nachdem alle GUI Elemente gezeichnet wurden
 MessageCallBack        : TGLWindowGUIMessageCallBack;   // Verarbeitet Nachrichten (also z.B. die Nachricht die von einem gedrückten Button gesendet wird)
 UpdateCallBack         : TGLWindowUpdateCallBack;       // Wird aufgerufen wenn der Inhalt des Fensters aktualisiert werden soll, i.d.R. durch einen globalen Aufruf (''TGLGUI.GlobalWindowUpdate'')
 ...
end;


Gruppensichtbarkeit

In einer komplexen GUI wird man irgendwann bestimmte Objekte ein- bzw. ausblenden müssen. Ein Beispiel aus Projekt "W" : Im Spionageschirm können Spion unterschiedlichen Typs angezeigt werden, und je nach gewähltem Spiontyp müssen z.B. diverse Panels und Labels gezeigt oder auch versteckt werden. In der GUI von Projekt "W" hat daher jedes GUI-Element eine Eigenschaft namens GroupID über dass die Elemente bestimmten Sichtbarkeitsgruppen zugeordnet werden. Die GUI bietet dann eine Funktion namens ToggleVisibility mit der man ganze Gruppen ein- und ausblenden kann. Leicht zu implementieren und äußerst praktisch.


Der GUI-Editor

Eine komplexe GUI benötigt wie bereits angesprochen einen entsprechenden Editor mit dem man seine Fenster erstellen kann, und da ich im Forum bereits mehrfach darauf angesprochen wurde, werde ich anhand des Editors von Projekt "W" aufzeigen wie ich einen GUI-Editor aufgebaut habe. Natürlich ist dieser auch auf meine GUI angepasst (deshalb auch keine Veröffentlichung), aber sicherlich findet hier jeder den ein oder anderen Punkt den er auch gerne in seinem eigenen Editor sehen würde.

wiki gui editor.jpg


Echtzeitvorschau

Die vermutlich wichtigste Komponente des Editors. Sie stellt das aktuelle geladene Fenster bzw. die GUI so dar wie man es im Spiel sieht. Bei Bedarf werden natürlich noch ein paar Dinge eingeblendet, wie z.B. Debuglinien oder Auswahlrahmen. Für den Editor muss man hier noch auf Klicks reagieren um Objekte auszuwählen, diese Funktionalität sollte man aber direkt in der GUI verankern.


Eigenschaftseditor

Natürlich muss man die Eigenschaften der gewählten GUI-Elemente irgendwie verändern können. Ähnlich wie in Delphi bietet sich hier ein TValueListEditor an, der sehr flexibel ist. Neben einfachen Werteingaben kann man hier auch Dropdownlisten für vorgegebene Werte, oder sogar Buttons einfügen die dann einen externen Editor (z.B. für die Punkte einer Region oder den Text eines Memos) aufrufen lässt. Neben dem Eigenschaftseditor für GUI-Elemente benötigt man natürlich auch einen für das aktuelle Fenster. Hier kann man dann Fenstertitel, Größe und ein paar andere Kleinigkeiten anpassen.


Möglichkeit direkt neue GUI-Elemente zu erstellen

Hier gibts nicht viel zu erzählen. Ein GUI-Editor wäre natürlich kaum nützlich wenn man keine neuen GUI-Elemente hinzufügen könnte. Wie man das präsentiert ist dem persönlichen Geschmack überlassen. Mir reicht eine einfache Liste mit allen verfügbaren GUI-Elemente, schöner wären natürlich Icons wie in Delphi. Die Implementation ist hier auch recht einfach, denn die GUI an sich sollte bereits eine Möglichkeit bieten einem Fenster neue Elemente hinzuzufügen (z.B. TGLWindow.AddButton(...)). Dann wird im Editor letztendlich nur (je nach ausgewählten Element) die entsprechende Funktion aus der GUI aufgerufen.


Umschalten zwischen Editor- und Ingame-GUI

Eher von praktischem Nutzen ist die Möglichkeit zwischen Editor und der Ingame-GUI umzuschalten. So kann man im Editor schnell sehen wie die GUI im Spiel letztendlich aussieht und reagiert. Denn während im Editormodus i.d.R. alle Elemente angezeigt werden gibt es im Ingame-Modus z.B. graphische Buttons die nur beim Mouseover angezeigt werden sollen.


Debugmodus

Wo würde der Debugmodus besser reinpassen als direkt in den Editor? Im Spiel würde er kaum Sinn machen, denn besonders bei Spielen die lange Laden und komplex sind würde es immer ewig dauern bis man den Debugmodus zu Gesicht bekäme. Daher sollte der Editor eine Möglichkeit bieten eben diesen Modus aktivieren zu können. So kann man schnell sehen ob bzw. was nicht stimmt. In der GUI für Projekt "W" ist dieser Modus besonders für das TGLSkinnedControl sehr nützlich, so kann man direkt sehen wenn z.B. der polygonale Selektionsbereich an der falschen Stelle liegt. wiki gui editor debug.jpg


Gruppensichtbarkeit

Die entsprechende Funktionalität ist ja bereits in der GUI selbst vorhanden, und da man im Editor durchaus mehrere Objekte direkt an der selben Stelle liegen hat (siehe Beispiel mit den verschiedenen Spiontypen) ist eine Möglichkeit die Gruppensichtbarkeit einzustellen sehr praktisch. Hierzu iteriert man am besten durch alle GUI-Elemente des Fensters und packt alle Gruppen in eine TCheckListBox. Dort kann man dann per Haken wählen welche Gruppen gerade sichtbar sein sollen. Am besten sollte man direkt in der GUI einen Zeiger auf diese TGLCheckListBox unterbringen, und beim Rendern in einem Fenster prüft man auf den Editiermodus (Flag in der GUI-Unit) und checkt dann gegen diese Liste welche GUI-Elemente gerendert werden sollen.


Mehrfachauswahl

Der Editor sollte die Möglichkeit bieten mehrere GUI-Elemente gleichzeitig auswählen zu können. So kann man mehrere Objekte gleichzeitig bearbeiten. Hier muss man dann natürlich Funktionalität implementieren die die gemeinsamen Eigenschaften der gewählten GUI-Elemente ermittelt und diese dann in den Objekteditor schreibt.


Kleinigkeiten

Damit sind weniger wichtige Funktionen gemeint die das Arbeiten mit dem GUI-Editor erleichtern oder erweitern. Im Falle des Editors für Projekt "W" gibt es da einige kleine Helfer :

  • Hintergrundbild laden (sehr praktisch um zu sehen wie die GUI auf Spielhintergrund aussieht)
  • GUI-Elemente sortieren (sortiert die Elemente der GUI nach Z-Wert neu, u.a. nützlich wenn man Alphatestprobleme hat)
  • GUI-Elemente nach Gruppe auswählen (wenn man z.B. ne ganze Objektgruppe auf einmal verschieben oder anpassen will)