Immediate Mode Model/View/Controller
Verfasst: 12.01.2014, 15:19
Pflichtlektüre für alle, die über die Entwicklung einer UI nachdenken: Immediate Mode Model/View/Controller
Die deutsche Spieleentwickler-Community (seit 1999).
https://www.zfx.info/
Ich kann mich nicht an Einzelheiten des Artikels erinnern (ich glaube gar, ich habe nur den Titel gelesen und Code überflogen), aber aus meiner Sicht verfehlst du den Punkt: Es geht genau um den Aufruf pro Control - also Code statt Daten/Objekte - egal ob die Parameter als struct oder direkt reingehen - insbesondere keine persistenten Objekte für angezeigte UI-Elemente: Das ist essenziell, da in einem großen Baum von Daten auch ganze Unterbäume von UI-Elementen von einem Frame zum anderen verschwinden können, ohne dass du Add/Remove-Events behandeln willst. Davon abgesehen bist du frei zu tun und zu lassen was du willst, ob du das Layout von Teilbäumen deiner UI im Voraus erstellst und in wie auch immer gearteten Objekten vorhälst, damit du es jedes Frame der UI füttern kannst, ist deine Sache. Der bedeutendste Anwendungsfall, der mir einfällt, ist aber ein einfaches Tree Layout, wo derartige manuelle Layouting-Features wenig nütze sind und nur unnötig Komplexität einführen.Chromanoid hat geschrieben:Zu IMGUI: Ich bin eigentlich MVVM Fan. Der IMGUI Weg erscheint mir nicht sehr skalierbar. Wenn man sowas sieht https://github.com/AdrienHerubel/imgui/ ... le_gl3.cpp juckt es doch in den Fingern, die Aufrufe in eigene Objekte zu packen. Die Parameter der Methoden werden zu Eigenschaften, man lädt die Objekte dann aus einer Datei und man möchte nur noch das Verhalten in der Anwendung beschreiben. *Zack* man ist wieder kurz davor Events zu verwenden... Vielleicht wäre ein Weg das ganze trotzdem direkt zu steuern, statt Events einfach aus der Liste die relevanten Elemente herauszusuchen und das besondere Verhalten pro Durchlauf per Methoden-Pointer einzufügen.
Die Zuordnung von Controls über die Frames hinweg ist tatsächlich bisweilen eine Herausforderung. Ich konstruiere mir für jedes Control einen eindeutigen Pfad von Objektzeigern und speichere diesen bei Notwendigkeit einer solchen Zuordnung für die nachfolgenden Frames. Da der Pfad in den nachfolgenden Frames immer nur zum Vergleich herangezogen wird, macht es nichts, wenn die darin enthaltenen Zeiger ungültig werden - findet sich im nächsten Frame das Control nicht mehr, wird der Pfad einfach gelöscht. Darüber kriegst du schonmal Fokus, Mouse Capture, gepufferte Eingabe etc.Schrompf hat geschrieben:Ich mache mir allerdings Sorgen, ob man da auch die ganzen Produktivitätsmerkmale aktueller GUIs unterbekommt.
Prinzipiell lasse ich im Moment tatsächlich einfach immer das betroffene Control Eingaben verarbeiten, das funktioniert für Fokus/Texteingabe soweit sehr gut. Tab-Fokus kannst du genauso umsetzen, indem du immer den Pfad des vorangegangene Objekts pufferst (Fokus zurück) bzw. ein Flag einführst, womit du dem nachfolgenden Element Fokus signalisierst (Fokus weiter).Schrompf hat geschrieben:Ein Controller dagegen gibt mit seinem eigenen State nur die erste Ableitung an. Wer aber berechnet daraus den neuen Zustand, also welches Widget als nächstes fokussiert ist, wenn der Button beim Zeichnen noch nicht weiß, welche Controls nach ihm noch kommen?
Mir ging es genau darum, dass man eben schnell den Punkt verfehlt, weil es mir doch einen eher unpraktischen Eindruck macht. Das hat dot ja auch schon geschrieben. Sehr gute Ergänzung zum Thema findet man IMO auch in der verlinkten Präsentation: http://sol.gfxile.net/files/Assembly07_IMGUI.pdf Am Ende wird die Entkopplung von Design und Code angesprochen.CodingCat hat geschrieben:Ich kann mich nicht an Einzelheiten des Artikels erinnern (ich glaube gar, ich habe nur den Titel gelesen und Code überflogen), aber aus meiner Sicht verfehlst du den PunktChromanoid hat geschrieben:Wenn man sowas sieht https://github.com/AdrienHerubel/imgui/ ... le_gl3.cpp juckt es doch in den Fingern, die Aufrufe in eigene Objekte zu packen. Die Parameter der Methoden werden zu Eigenschaften, man lädt die Objekte dann aus einer Datei und man möchte nur noch das Verhalten in der Anwendung beschreiben. *Zack* man ist wieder kurz davor Events zu verwenden...
Ja, genau das tue ich in meinem Prototyp, und genau das erachte ich im Kontext von Content Creation Tools als gangbar und sinnvoll ...dot hat geschrieben:Entweder, ich berechne in jedem Frame das komplette Layout ständig neu
... nein, im Allgemeinen ist das eigentlich nicht realitätsfremd, es sei denn du entwickelst einen WYSIWYG Text-Editor. Einfaches Alignment an festen Grenzen ist kein Problem, Kerning lässt sich genau wie Glyphzuordnung seitens UI-Bibliothek in entsprechenden Datenstrukturen sehr schnell nachschlagen, wenn notwendig kannst du auch noch problemlos eine UTF-8-Dekodierung voranstellen.dot hat geschrieben:Ein extrem wichtiger Spezialfall von Layout versteckt sich hinter Text Rendering. Text Layout ist extrem komplex (Unicode auf und ab, Spacing, Kerning, Justification, Text Directionality ...). Anzunehmen, dass man eben mal drawString("hello world!") macht und gut ist, ist nett, ab einem gewissen Punkt aber leider auch einfach nur noch realitätsfremd.
Input-Fokus sowie Mouse Capture habe ich bereits wie oben skizziert implementiert, das ist kein Problem. Persistentes Resize auf Control-granularer Ebene passt tatsächlich schlecht ins zustandslose Konzept, ich sehe aber dafür im von mir angestrebten Zielkontext der Zugänglichmachung und Bearbeitung großer Datenbäume auch keine Notwendigkeit. Mittelfristiger Zustand während der Bearbeitung einer konkreten Untermenge der Daten ist hingegen kein Problem, analog zu Input-Fokus lässt sich auch gepufferte Text-Eingabe, Resize etc. für gerade aktive Controls implementieren.dot hat geschrieben:Standardinteraktionen mit dem User wie z.B. Resizen von Controls müssten wohl durchgehend selbst implementiert werden, es kann ja nichtmal sowas wie z.B. das Konzept eines Inputfokus geben.
Culling/Tree Pruning ist bei großen Datenmengen definitiv ein Muss, aber genau Aufgabe der UI-Bibliothek und nicht des Benutzers. Auch dies lässt sich über versteckten Zustand und eindeutige Control-Pfad-Identifier lösen - der Trick ist abermals, den Zustand ohne Rückabhängigkeiten zu den Daten zu speichern, um in nachfolgenden Frames fehlende/neue Teile im Rahmen des Aufbaus der UI erkennen zu können, ohne dass Benachrichtigung/Synchronisation seitens der Daten erforderlich wird.dot hat geschrieben:Außerdem will ich nicht in jedem Frame für alle 223 Buttons testen müssen, ob der Mauszeiger im Button liegt oder nicht, wenn gerade mal 3 davon auch nur in der Nähe wären. Klar, natürlich kann man auch hier eine bessere Lösung dem User überlassen. Langsam stellt sich dann aber unweigerlich die Frage, inwiefern sowas nützlich ist und sich "GUI Library" nennen darf, wenn alle möglichen Dinge, die eigentlich Implementierungsdetails sein sollten, dem User nicht nur überlassen, sondern für den User schon überhaupt auch nur sichtbar werden.
Das ist ein wichtiger Punkt und ich sehe für diese radikale Art von UI tatsächlich nur ein begrenztes Einsatzgebiet, dort spart es aber meines Erachtens einen riesen Haufen Arbeit verglichen mit unzähligen UI-Bindings bzw. ganzen Ports von Engines in andere Sprachen (C#), wo Frameworks mehr Unterstützung für automatisierte Bindings auf Kosten von Speicher- und Laufzeiteffizienz bieten.dot hat geschrieben:Auch im Hinblick auf Energieeffizienz möge so ein Ansatz in einem Spiel vielleicht gerade noch vertretbar sein, in einer normalen Anwendung ist es imo aber ein absolutes No-Go, die ganze GUI ständig neu zu rendern.
Soweit ich weiß arbeiten einige Frameworks (Android?) bereits genau so. Das eigentliche Problem von nicht-Immediate UIs bleibt aber bestehen: Sobald nicht konsequent die ganze Zeit Updates durchgeführt werden, brauchst du Benachrichtigung über Veränderung bzw. Invalidierung von Teilbereichen deiner UI. Deshalb funktioniert das IMGUI-Konzept überhaupt nur in dieser radikalen Form.dot hat geschrieben:Ich hab auch schon länger drüber nachgedacht; ich denke, dass GUI vermutlich einfach von Natur aus untrennbar mit gewissem State verbunden ist, weswegen es eine völlig zustandslose Lösung rein prinzipiell gar nicht geben kann. State duplication allerdings, lässt sich wohl vermeiden. Ich denk im Moment über einen Ansatz nach, der rein presentation-spezifischen Zustand in einer retained mode Struktur hält, sich content-spezifischen Zustand wie z.B. den Text auf einem Label aber, wann immer benötigt, per Callback vom User holt. Genau in dieser Unterscheidung zwischen Presentation und Content liegt imo der eigentliche Knackpunkt; obige immediate mode Lösung setzt implizit gewissermaßen ja ebenfalls genau dort an...
CodingCat hat geschrieben:Ja, genau das tue ich in meinem Prototyp, und genau das erachte ich im Kontext von Content Creation Tools als gangbar und sinnvoll ...dot hat geschrieben:Entweder, ich berechne in jedem Frame das komplette Layout ständig neu
Der Punkt ist: It adds up...CodingCat hat geschrieben:... nein, im Allgemeinen ist das eigentlich nicht realitätsfremd, es sei denn du entwickelst einen WYSIWYG Text-Editor. Einfaches Alignment an festen Grenzen ist kein Problem, Kerning lässt sich genau wie Glyphzuordnung seitens UI-Bibliothek in entsprechenden Datenstrukturen sehr schnell nachschlagen, wenn notwendig kannst du auch noch problemlos eine UTF-8-Dekodierung voranstellen.dot hat geschrieben:Ein extrem wichtiger Spezialfall von Layout versteckt sich hinter Text Rendering. Text Layout ist extrem komplex (Unicode auf und ab, Spacing, Kerning, Justification, Text Directionality ...). Anzunehmen, dass man eben mal drawString("hello world!") macht und gut ist, ist nett, ab einem gewissen Punkt aber leider auch einfach nur noch realitätsfremd.
CodingCat hat geschrieben:Input-Fokus sowie Mouse Capture habe ich bereits wie oben skizziert implementiert, das ist kein Problem. Persistentes Resize auf Control-granularer Ebene passt tatsächlich schlecht ins zustandslose Konzept, ich sehe aber dafür im von mir angestrebten Zielkontext der Zugänglichmachung und Bearbeitung großer Datenbäume auch keine Notwendigkeit. Mittelfristiger Zustand während der Bearbeitung einer konkreten Untermenge der Daten ist hingegen kein Problem, analog zu Input-Fokus lässt sich auch gepufferte Text-Eingabe, Resize etc. für gerade aktive Controls implementieren.dot hat geschrieben:Standardinteraktionen mit dem User wie z.B. Resizen von Controls müssten wohl durchgehend selbst implementiert werden, es kann ja nichtmal sowas wie z.B. das Konzept eines Inputfokus geben.
Naja, dieser Trick funktioniert wohl in der Praxis ganz gut, ich empfinde ihn aber als ein wenig unsauber. Egal ob man jetzt Pointer oder sonst irgendwelche IDs verwendet. Wenn ich einen Button lösche und gleich drauf einen neuen mach, hab ich zumindest bei Pointern das Problem, dass mein neuer Button potentiell die selbe Adresse hat und das GUI niemals mitbekommt, dass das jetzt plötzlich ein anderer Button ist. Insbesondere bei generierten IDs kann man dies zwar bis zu einem gewissen Grad beeinflussen, aber es ist irgendwie dennoch ein Hack um ein prinzipielles Problem eher auszugleichen als zu lösen. Auch brauch ich natürlich wieder einen weiteren Lookup und mein User muss für jedes Control nun auf einmal eine ID bereitstellen... ;)CodingCat hat geschrieben:Culling/Tree Pruning ist bei großen Datenmengen definitiv ein Muss, aber genau Aufgabe der UI-Bibliothek und nicht des Benutzers. Auch dies lässt sich über versteckten Zustand und eindeutige Control-Pfad-Identifier lösen - der Trick ist abermals, den Zustand ohne Rückabhängigkeiten zu den Daten zu speichern, um in nachfolgenden Frames fehlende/neue Teile im Rahmen des Aufbaus der UI erkennen zu können, ohne dass Benachrichtigung/Synchronisation seitens der Daten erforderlich wird.dot hat geschrieben:Außerdem will ich nicht in jedem Frame für alle 223 Buttons testen müssen, ob der Mauszeiger im Button liegt oder nicht, wenn gerade mal 3 davon auch nur in der Nähe wären. Klar, natürlich kann man auch hier eine bessere Lösung dem User überlassen. Langsam stellt sich dann aber unweigerlich die Frage, inwiefern sowas nützlich ist und sich "GUI Library" nennen darf, wenn alle möglichen Dinge, die eigentlich Implementierungsdetails sein sollten, dem User nicht nur überlassen, sondern für den User schon überhaupt auch nur sichtbar werden.
CodingCat hat geschrieben:Das ist ein wichtiger Punkt und ich sehe für diese radikale Art von UI tatsächlich nur ein begrenztes Einsatzgebiet, dort spart es aber meines Erachtens einen riesen Haufen Arbeit verglichen mit unzähligen UI-Bindings bzw. ganzen Ports von Engines in andere Sprachen (C#), wo Frameworks mehr Unterstützung für automatisierte Bindings auf Kosten von Speicher- und Laufzeiteffizienz bieten.dot hat geschrieben:Auch im Hinblick auf Energieeffizienz möge so ein Ansatz in einem Spiel vielleicht gerade noch vertretbar sein, in einer normalen Anwendung ist es imo aber ein absolutes No-Go, die ganze GUI ständig neu zu rendern.
Ich sage ja nicht, dass ich die Vorteile des immediate mode Ansatzes nicht sehe. Leider ist er aber wohl doch eher recht ineffizient und funktioniert überhaupt nur unter sehr speziellen Voraussetzungen, die viele Dinge, die man von einem modernen UI erwarten würde, von vornherein ausschließen oder nur in beschränktem Umfang erlauben. Die meisten UI Toolkits sind in der Tat furchtbar, Events ein direkter Pfad in die Hölle, ich wünschte, es gäbe bessere Lösungen...CodingCat hat geschrieben:Soweit ich weiß arbeiten einige Frameworks (Android?) bereits genau so. Das eigentliche Problem von nicht-Immediate UIs bleibt aber bestehen: Sobald nicht konsequent die ganze Zeit Updates durchgeführt werden, brauchst du Benachrichtigung über Veränderung bzw. Invalidierung von Teilbereichen deiner UI. Deshalb funktioniert das IMGUI-Konzept überhaupt nur in dieser radikalen Form.dot hat geschrieben:Ich hab auch schon länger drüber nachgedacht; ich denke, dass GUI vermutlich einfach von Natur aus untrennbar mit gewissem State verbunden ist, weswegen es eine völlig zustandslose Lösung rein prinzipiell gar nicht geben kann. State duplication allerdings, lässt sich wohl vermeiden. Ich denk im Moment über einen Ansatz nach, der rein presentation-spezifischen Zustand in einer retained mode Struktur hält, sich content-spezifischen Zustand wie z.B. den Text auf einem Label aber, wann immer benötigt, per Callback vom User holt. Genau in dieser Unterscheidung zwischen Presentation und Content liegt imo der eigentliche Knackpunkt; obige immediate mode Lösung setzt implizit gewissermaßen ja ebenfalls genau dort an...
Genau hier sehe ich aber in einer Anwendung, die jedes Frame mehrere Millionen Dreiecke rendert, nicht das Problem. :Pdot hat geschrieben:Natürlich, bis zu einem gewissen Grad kann man alles machen. Aber es ist imo einfach dermaßen verschwenderisch, dass ich nichtmehr ruhig schlafen könnte, wenn ich wüsste, dass mein UI tatsächlich in jedem Frame rekursiv das komplette two-pass Layout durchrechnet und jeden einzelnen Buchstaben neu platziert... ;)
ID muss in der Tat bereitgestellt werden. Dass IDs unbemerkt den Besitzer wechseln ist in der Tat möglich, aber nach meiner aktuellen Einschätzung nicht störend. In Fällen, in denen so viele Aktionen aufeinander treffen, dass so ein Fall eintritt, gibt meiner Erfahrung nach bis heute jede nicht auf Datenverwaltung spezialisierte Event-basierte Applikation über kurz oder lang den Geist auf, weil so viel Zustand mit indirekter Daten-Abhängigkeit vorliegt, dass sich ungültige Zustände und Crash-Möglichkeiten schon in Anzahl überhaupt nicht mehr überblicken lassen. In der Immediate Mode UI hingegen wird schlimmstenfalls der Fokus ungefragt auf ein anderes Control übertragen und ggf. gepufferte Eingabe auf das falsche Datenelement angewandt - und auch das nur, wenn ohne Benutzerinteraktion unerwarteterweise die gerade in der Bearbeitung befindlichen Daten verschwinden. Es ist klar, dass solches Verhalten in vielen Anwendungen inakzeptabel wäre, dort wäre der UI-Zustand dann jedoch eine gewollte Kopie eines vergangenen Datenzustandes und die Synchronisation nebenläufig bearbeiteter Daten wäre ohnehin ein entscheidender Teil der Programmlogik.dot hat geschrieben:Naja, dieser Trick funktioniert wohl in der Praxis ganz gut, ich empfinde ihn aber als ein wenig unsauber. Egal ob man jetzt Pointer oder sonst irgendwelche IDs verwendet. Wenn ich einen Button lösche und gleich drauf einen neuen mach, hab ich zumindest bei Pointern das Problem, dass mein neuer Button potentiell die selbe Adresse hat und das GUI niemals mitbekommt, dass das jetzt plötzlich ein anderer Button ist. Insbesondere bei generierten IDs kann man dies zwar bis zu einem gewissen Grad beeinflussen, aber es ist irgendwie dennoch ein Hack um ein prinzipielles Problem eher auszugleichen als zu lösen. Auch brauch ich natürlich wieder einen weiteren Lookup und mein User muss für jedes Control nun auf einmal eine ID bereitstellen... ;)
Geht mir genauso :-/ Letztlich ist die UI-Zustandsproblematik leider äquivalent mit der Cache-Kohärenz-Problematik, entsprechend schlecht sieht es mit optimalen allgemeinen Lösungsmöglichkeiten aus.dot hat geschrieben:Ich sage ja nicht, dass ich die Vorteile des immediate mode Ansatzes nicht sehe. Leider ist er aber wohl doch eher recht ineffizient und funktioniert überhaupt nur unter sehr speziellen Voraussetzungen, die viele Dinge, die man von einem modernen UI erwarten würde, von vornherein ausschließen oder nur in beschränktem Umfang erlauben. Die meisten UI Toolkits sind in der Tat furchtbar, Events ein direkter Pfad in die Hölle, ich wünschte, es gäbe bessere Lösungen...
Also zumindest bei mir macht das mit dem Millionen-Dreiecke-Rendern die GPU und nicht die CPU... :PCodingCat hat geschrieben:Genau hier sehe ich aber in einer Anwendung, die jedes Frame mehrere Millionen Dreiecke rendert, nicht das Problem. :Pdot hat geschrieben:Natürlich, bis zu einem gewissen Grad kann man alles machen. Aber es ist imo einfach dermaßen verschwenderisch, dass ich nichtmehr ruhig schlafen könnte, wenn ich wüsste, dass mein UI tatsächlich in jedem Frame rekursiv das komplette two-pass Layout durchrechnet und jeden einzelnen Buchstaben neu platziert... ;)
So sehe ich das eigentlich auch. Für größere Spiele scheint mir Scaleform das Tool der Wahl zu sein, da merkt man ja schon wie wichtig die Designer sind (Tweening und Co. sind bestimmt ein herrlicher Spaß mit IMGUI, oder nicht?). Für andere Dinge sollte HTML5 immer interessanter werden.The good..
● No object creation
● No cleanup either
● No queries for information
● No message passing
● Data owned by application, not the widget
● Everything is “immediate” - one call per widget, each frame, handles behavior and rendering.
..the bad..
● Requires different kind of thinking
● Wastes CPU time
– But in games you're re-rendering stuff 50+ fps anyway..
● UI generated from code; No designer-friendly tools.
– Unless you make some...
..and the ugly.
● While making easy things dead easy, makes complicated things very complicated.
– The UI system internals may become even more complex than in “traditional” GUI library!
● UI logic interleaved to rendering
– Can be overcome by more complex internals.
● Pretty “anti-OOP” (although this is debatable)
● Not a silver bullet.
Uuh, nur mal als Einwurf, aber ist eine IMGUI nicht einfach quasi genau das gleiche wie HTML?Für andere Dinge sollte HTML5 immer interessanter werden
Code: Alles auswählen
//anstatt
//1.Frame
Label = new Label(xpos, ypos, width,height,"Hello");
//2.Frame
label.setText("Hello2"); //greift auf den Zustand zu, ist meist wesentlich komplexer
//machen wir
//1. Frame
gui.clear();
gui.CreateLabel(xpos,ypos, width,height,"Hello");
gui.render();
//2. Frame
gui.clear();
gui.CreateLabel(xpos,ypos, width,height,"Hello2");
gui.render();
Code: Alles auswählen
//Ausgabe bei aufruf der index.html
echo("<b class="label">Hello</b>");
//Ausgabe nach POST
echo("<b class="label">Hello2</b>");
Code: Alles auswählen
gui.CreateButton(xpos,ypos, width,height,gui.CreateLabel("Yeah"));
//erzeugt dann sowas wie:
//<p class="button"><b class="label">Yeah</b></p>