Wenn Du einen Core übrig hast, ist HTML sicher auch ne gute Idee. In Echtzeit stell ich mir das kniffliger vor. Bekommt man die auch sinnvoll transparent mit einem 3D-Hintergrund gemerged?
Ich bin aufgrund verschiedener Umstände auch gerade dabei, eine GUI zu schreiben. Ansprüche waren explizite Wiederverwendbarkeit, z.B. für die Splitterwelten später, und einige grundlegende Komponenten für InGame-GUIs: Text, Button, Checkbox, ComboBox, LineEdit, TabWidget. Ich tippe hier mal kurz einen Erfahrungsbericht:
Der Anfang war viel Tipparbeit. Ich wollte, dass die GUI nicht selbst aktiv wird, also keine Eingaben abfragt oder gar die Hauptschleife übernimmt. Daher wurden ein Rudel Ereignisse definiert, mit denen man die Gui füttern kann. An der Stelle kann man dann auch gleich verschiedene Eingabemedien aufeinander abbilden, wodurch die GUI inzwischen auch per GamePad oder Tastatur bedienbar ist.
Die Basisklasse war sehr schlicht - Position, Größe, virtuelle BehandleEreignis-Funktionen, Zeichen-Funktion. Dazu kommt Fokus und Sichtbarkeit. Zu letzteren habe ich auch immer die Zeitpunkte der letzten Aktivierung und Deaktivierung gespeichert, was der Gui-Renderer dann für Überblend-Effekte nach seinem Geschmack verwenden kann. Das funktioniert richtig gut und es ist sehr erfrischend, sich nicht über x Übergangszustände Gedanken machen zu müssen, weil der GUI-State immer sofort und eindeutig wechselt. Nur die optische Darstellung zieht z.B. über die nächste halbe Sekunde nach. Eine sehr angenehme Arbeitsweise, solange die Übergangsanimationen kurz genug sind, damit der Unterschied zwischen Darstellung und GUI-State nicht die Nutzer verwirrt.
Ich habe hier das erste Mal mit einer Endknoten-Basisklasse experimentiert, wo erst die erste Ableitung Kinder haben kann. Bislang waren alle meine Objekt-Hierarchien immer so ausgelegt, dass jedes Objekt Kinder haben kann. Jetzt gibt es eine spezielle Ableitung, die Kinder haben kann, während die meisten Klassen keine haben können. Das erfordert vereinzelt einen dynamic_cast, ist aber größtenteils eine angenehme Arbeitsweise.
Ich habe frühzeitig automatisches Layout eingebaut - bislang gibt es nur vertikale und horizontale Gruppierungen, in denen wachstumsfähige Objekte Zusatzgröße erhalten und der Rest nach Ausrichtung und Abständen angeordnet wird. In das System habe ich ein paar Stunden Arbeit gesteckt, weil es doch einige hässliche Dynamiken gibt, wenn das Layout bei Hinzufügen, Löschen oder Umgruppieren von Komponenten jedesmal neu berechnet werden muss und man dann einige davon ineinander stapelt. Jetzt klappt es aber prima und ich bin sehr dankbar, mich nicht mit den manuellen Pixel-Schubsereien beschäftigen zu müssen. Anpassung an Auflösungsänderungen kommt da quasi vollautomatisch mit.
Das Rendering steckt komplett in einer externen Klasse, die per
Double Dispatch einzelne Komponenten zeichnet. Eine sehr angenehme Herangehensweise, da man mit einer einzelnen Klasse und ein paar Dutzend Zeilen das Aussehen komplett umkrempeln kann. Bei hierarchischen Zeicheneffekten wie z.B. einer ScrollArea, in der die Kinder mit Scrollbalken bewegt und gegen die Vater-Komponente geclippt werden, versagt diese Herangehensweise allerdings. Ich weiß noch nicht, wie ich das lösen werde - vielleicht ein fieser kleiner Spezialhack für diesen einen Fall. Ich bin ja für die Ergebnisse hier und nicht für den OO-Designpreis.
Man definiert übrigens sehr viele Parameter, wenn man eine GUI schreibt (Komponenten-Standardgrößen und -abstände) und vor allem, wenn man den Renderer schreibt (Größen, Farben, Anim-Zeiten). Manche davon, speziell die Animationszeiten, müssen auch noch beiden Seiten - Gui und Renderer - bekannt sein. Ich habe dazu einfach einen
boost::property_tree benutzt, den man am Anfang mit einer XML-Konfig füttert und den auch der Renderer mit gängigen Einstellungen füllt. Damit kann man nicht nur entspannt das GUI-Aussehen und -Verhalten konfigurieren, sondern erfreut sich auch an abstrakter Werte-Teilung, ohne das GUI und Renderer plötzlich voneinander wissen müssen.
Die Ereignis-Verteilung war noch ein gewisses Problem. Man muss für alle möglichen Zwecke ermitteln, ob eine Komponente gerade ansprechbar ist oder nicht. Eine Sichtbarkeitsprüfung reicht da nicht, da die optische Verdeckung nur wenig mit der Bedienbarkeit zu tun hat. Es gibt oft Fälle, wo ein kleiner (OK|Abbrechen)-Dialog den Dialog dahinter stilllegt, ihn aber optisch nicht verdeckt. Ich habe das jetzt so gelöst, dass Komponenten als "exklusiv" definiert werden können und dann Nachbarkomponenten auf der selben Hierarchieebene und deren Kinder vom Ereignis-Empfang ausschließen. Das klappt jetzt prima - endlich werden auch Tasten-Shortcuts nicht mehr an Buttons im Hintergrund durchgestellt.
Die Ereignisverteilung an die GUI-Nutzer funktioniert über
boost::signal. Mehr ist dazu nicht zu sagen.
Es gab in der Hierarchie noch manche Probleme mit Verweisen auf gelöschte oder entfernte Komponenten, aber das war im Endeffekt nur Inkonsequenz von mir. Es gibt jetzt eine virtuelle SetParent()-Funktion, in der sich beim Löschen Komponenten auch von Vater und der Wurzelkomponente abmelden. Das wird an den paar Stellen, wo es kritisch ist, benutzt, um Verweise aufzuräumen. Man sollte dabei auch an die ganzen Timer denken, die zeitverzögert Komponenten löschen oder erzeugen. Eine schlichte ID beim Erzeugen des Timers funktionierte da gut, das Löschen von Timern anhand der Zielfunktion ist spätestens mit C++0x Lambdas nicht mehr möglich.
Dialoge sind jetzt einfach nur Komponenten, die im Konstruktor sich selbst mit Zeug füllen und deren Signale auf eigene Funktionen umbiegen. Ein durchschnittlicher InGame-Pausedialog mit ein paar Optionen ist in 20 Zeilen gemacht. Sehr angenehm. Man könnte an der Stelle jetzt auch stressfrei Dialog-Erstellung und Handler-Funktionen in Skripte rausziehen, aber das war mir zu mühsam. Der Double Dispatch-Ansatz im Renderer funktioniert natürlich mit solchen außerhalb des GUI-Projekts definierten Komponenten nicht mehr, aber es gibt ja immernoch den klassischen Ansatz des Selber-Renderns oder alternativ ein Verzweigen anhand dynamic_cast in der CatchAll-Funktion des Renderers
Und final noch: ich habe in den letzten Tagen C++0x sehr zu schätzen gelernt. Allen voran die Lambdas, die selten aber effektiv anwendbar sind.
auto habe ich direkt ins Herz geschlossen.
override ist eine unauffällige, aber effekte Fehlerabsicherung, vor allem im Umgang mit vielen kleinen Ableitungen. Leider kann VC10 noch kein
foreach oder
strong enums.
So, das waren meine bisherigen Erfahrungen. Ich hoffe, es war für den Einen oder Anderen interessant. Ich habe auch kein Problem damit, den ganzen Code irgendwo hochzuladen, aber die GUI hat halt doch noch ein paar Dependencies auf unser Framework - Timer und Matheklassen. Wen es interessiert, der möge sich melden.