Inspirationsquellen
Dieser Beitrag entstand dank einer kürzlich erschienenen Veröffentlichung von
Aras Prantskevichus über einen Bericht für Junior-Programmierer. Es wird erläutert, wie Sie sich an neue ECS-Architekturen anpassen können. Aras folgt dem üblichen Muster (
Erklärung unten ): zeigt Beispiele für den schrecklichen OOP-Code und zeigt dann, dass das relationale Modell (
nennt es aber eher „ECS“ als relational ) eine großartige Alternative ist. Auf keinen Fall kritisiere ich Aras - ich bin ein großer Fan seiner Arbeit und lobe ihn für seine hervorragende Präsentation! Ich habe seine Präsentation anstelle von Hunderten anderer ECS-Beiträge aus dem Internet gewählt, weil er zusätzliche Anstrengungen unternommen und parallel zur Präsentation ein Git-Repository für Studien veröffentlicht hat. Es enthält ein kleines einfaches „Spiel“, das als Beispiel für die Auswahl verschiedener Architekturlösungen dient. Dieses kleine Projekt ermöglichte es mir, meine Kommentare zu einem bestimmten Material zu demonstrieren. Danke, Aras!
Aras-Folien sind hier verfügbar:
http://aras-p.info/texts/files/2018Academy - ECS-DoD.pdf . Der Code befindet sich auf github:
https://github.com/aras-p/dod-playground .
Ich werde (noch?) Die resultierende ECS-Architektur aus diesem Bericht nicht analysieren, sondern mich von Anfang an auf den „schlechten OOP“ -Code (ähnlich dem ausgestopften Trick) konzentrieren. Ich werde zeigen, wie es wirklich aussehen würde, wenn alle Verstöße gegen die Prinzipien von OOD (objektorientiertes Design, objektorientiertes Design) korrekt korrigiert würden.
Spoiler: Die Beseitigung aller OOD-Verstöße führt zu Leistungsverbesserungen ähnlich wie bei der Konvertierung von Aras in ECS. Außerdem wird weniger RAM benötigt und es werden weniger Codezeilen benötigt als in der ECS-Version!TL; DR: Bevor Sie zu dem Schluss kommen, dass OOP saugt und ECS-Laufwerke verwendet, halten Sie an und untersuchen Sie OOD (um zu wissen, wie OOP richtig verwendet wird) und verstehen Sie auch das relationale Modell (um zu wissen, wie ECS richtig angewendet wird).Ich habe lange Zeit an vielen Diskussionen über ECS im Forum teilgenommen, teilweise weil ich nicht denke, dass dieses Modell als separater Begriff verdient (
Spoiler: Dies ist nur eine Ad-hoc-Version des relationalen Modells ), sondern auch, weil Fast
jeder Beitrag,
jede Präsentation oder
jeder Artikel, der für ein ECS-Muster wirbt, folgt der folgenden Struktur:
- Zeigen Sie ein Beispiel für schrecklichen OOP-Code, dessen Implementierung aufgrund übermäßiger Vererbung schreckliche Fehler aufweist (was bedeutet, dass diese Implementierung gegen viele OOD-Prinzipien verstößt).
- Zu zeigen, dass Komposition eine bessere Lösung als Vererbung ist (und ganz zu schweigen davon, dass OOD uns tatsächlich die gleiche Lektion erteilt).
- Zeigen Sie, dass das relationale Modell hervorragend für Spiele geeignet ist (nennen Sie es jedoch „ECS“).
Eine solche Struktur macht mich wütend, weil:
(A) dies ein Trick ist, der "gestopft" ist ... er vergleicht weich mit warm (schlechter Code und guter Code) ... und dies ist unfair, selbst wenn es unbeabsichtigt gemacht wird und nicht erforderlich ist, um zu demonstrieren, dass die neue Architektur gut ist; und was noch wichtiger ist:
(B) es hat einen Nebeneffekt - ein solcher Ansatz unterdrückt das Wissen und demotiviert die Leser versehentlich davon, mit Studien vertraut zu sein, die seit einem halben Jahrhundert durchgeführt wurden. Sie begannen in den 1960er Jahren über das relationale Modell zu schreiben. In den 70er und 80er Jahren hat sich dieses Modell erheblich verbessert. Anfänger haben oft Fragen wie "In
welche Klasse möchten Sie diese Daten einfügen? " Und als Antwort wird ihnen oft etwas Unbestimmtes gesagt, wie "
Sie müssen nur Erfahrung sammeln und dann lernen Sie nur, innerlich zu verstehen " ... aber in den 70er Jahren war diese Frage aktiv studiert und im allgemeinen Fall wurde eine formelle Antwort abgeleitet; Dies wird als
Datenbanknormalisierung bezeichnet . Wenn Sie vorhandene Forschungsergebnisse verwerfen und ECS als eine völlig neue und moderne Lösung bezeichnen, verbergen Sie dieses Wissen vor Anfängern.
Die Grundlagen der objektorientierten Programmierung wurden vor ebenso langer, wenn nicht früherer Zeit gelegt (
dieser Stil begann in den 1950er Jahren zu erforschen )! In den 1990er Jahren wurde die Objektorientierung jedoch modisch, viral und entwickelte sich sehr schnell zum vorherrschenden Programmierparadigma. Die Popularität vieler neuer OO-Sprachen, einschließlich Java und der (
standardisierten Version ) C ++, ist explosionsartig gestiegen. Da dies jedoch auf einen Hype zurückzuführen war, musste jeder dieses hochkarätige Konzept kennen, um in seinen Lebenslauf schreiben zu können, aber nur wenige gingen wirklich darauf ein. Diese neuen Sprachen haben die Schlüsselwörter -
Klasse ,
virtuell ,
erweitert ,
implementiert - aus vielen Funktionen von OO erstellt, und ich glaube, dass OO in diesem Moment in zwei separate Einheiten unterteilt wurde, die ihr eigenes Leben führen.
Ich werde die Verwendung dieser OO-inspirierten Sprachfunktionen als „
OOP “ und die Verwendung von OO-inspirierten Design- / Architekturtechniken als „
OOD “ bezeichnen. Alle haben sehr schnell die OOP aufgenommen. Bildungseinrichtungen bieten OO-Kurse an, in denen neue OOP-Programmierer gebacken werden. Das Wissen über OOD bleibt jedoch zurück.
Ich glaube, dass Code, der die Sprachfunktionen von OOP verwendet, aber nicht den Prinzipien des OOD-Designs folgt,
kein OO-Code ist . Die meisten Kritikpunkte gegen OOP verwenden beispielsweise entkernten Code, der eigentlich kein OO-Code ist.
OOP-Code hat einen sehr schlechten Ruf, insbesondere weil der größte Teil des OOP-Codes nicht den Prinzipien von OOD folgt und daher kein „wahrer“ OO-Code ist.
Hintergrund
Wie oben erwähnt, wurden die 1990er Jahre zum Höhepunkt der „OO-Mode“, und zu dieser Zeit war die „schlechte OOP“ wahrscheinlich die schlechteste. Wenn Sie zu diesem Zeitpunkt OOP studiert haben, haben Sie höchstwahrscheinlich etwas über die „vier Säulen von OOP“ gelernt:
- Abstraktion
- Kapselung
- Polymorphismus
- Vererbung
Ich nenne sie lieber nicht vier Säulen, sondern „vier OOP-Tools“. Dies sind Tools, mit denen
Sie Probleme lösen können. Es reicht jedoch nicht aus, nur herauszufinden, wie das Tool funktioniert. Sie müssen wissen, wann Sie es verwenden müssen. Seitens der Lehrer ist es unverantwortlich, den Menschen ein neues Tool beizubringen und ihnen nicht zu sagen, wann es sich lohnt, es zu verwenden. In den frühen 2000er Jahren gab es Widerstand gegen den aktiven Missbrauch dieser Werkzeuge, eine Art „zweite Welle“ des OOD-Denkens. Das Ergebnis war die Entstehung der
SOLID- Mnemonik, mit der sich die architektonischen Stärken schnell bewerten ließen. Es sollte angemerkt werden, dass diese Weisheit in den 90er Jahren tatsächlich weit verbreitet war, aber noch kein cooles Akronym erhalten hat, das es ermöglichte, sie als fünf Grundprinzipien festzulegen ...
- Das Prinzip der alleinigen Verantwortung ( Prinzip der Einzelverantwortung). Jede Klasse sollte nur einen Grund für die Änderung haben. Wenn die Klasse "A" zwei Verantwortlichkeiten hat, müssen Sie die Klassen "B" und "C" erstellen, um jede einzeln zu verarbeiten, und dann "A" aus "B" und "C" erstellen.
- Das Prinzip der Offenheit / Schließung ( O Stift / geschlossenes Prinzip). Software ändert sich im Laufe der Zeit ( d. H. Ihre Unterstützung ist wichtig ). Versuchen Sie, die Teile, die sich am wahrscheinlichsten ändern, in Implementierungen ( d. H. In bestimmten Klassen ) zu platzieren und Schnittstellen basierend auf den Teilen zu erstellen, die sich wahrscheinlich nicht ändern ( z. B. abstrakte Basisklassen ).
- Das Substitutionsprinzip von Barbara Liskov ( L iskov-Substitutionsprinzip). Jede Implementierung einer Schnittstelle muss zu 100% die Anforderungen dieser Schnittstelle erfüllen, d. H. Jeder Algorithmus, der mit einer Schnittstelle arbeitet, sollte mit jeder Implementierung funktionieren.
- Das Prinzip der Trennung der Schnittstelle (Prinzip der Schnittstellentrennung ). Machen Sie die Schnittstellen so klein wie möglich, damit jeder Teil des Codes die kleinste Menge an Codebasis „kennt“, um unnötige Abhängigkeiten zu vermeiden. Dieser Tipp ist auch für C ++ geeignet, wo die Kompilierungszeiten sehr lang werden, wenn Sie ihn nicht befolgen.
- Das Prinzip der Abhängigkeitsinversion ( D- Abhängigkeitsinversionsprinzip). Anstelle von zwei spezifischen Implementierungen, die direkt kommunizieren (und voneinander abhängen), können sie normalerweise getrennt werden, indem ihre Kommunikationsschnittstelle als dritte Klasse formalisiert wird, die als Schnittstelle zwischen ihnen verwendet wird. Es kann sich um eine abstrakte Basisklasse handeln, die die Aufrufe der zwischen ihnen verwendeten Methoden definiert, oder auch nur um eine POD- Struktur, die die zwischen ihnen übertragenen Daten definiert.
- Ein anderes Prinzip ist im Akronym SOLID nicht enthalten, aber ich bin sicher, dass es sehr wichtig ist: „Komposition lieber als Vererbung“ (Composite-Wiederverwendungsprinzip). Die Zusammensetzung ist standardmäßig die richtige Wahl . Die Vererbung sollte in Fällen belassen werden, in denen dies unbedingt erforderlich ist.
Also bekommen wir SOLID-C (++)

Im Folgenden werde ich auf diese Prinzipien verweisen und sie als Akronyme bezeichnen - SRP, OCP, LSP, ISP, DIP, CRP ...
Noch ein paar Anmerkungen:
- In OOD können die Konzepte von Schnittstellen und Implementierungen nicht an bestimmte OOP-Schlüsselwörter gebunden werden. In C ++ erstellen wir häufig Schnittstellen mit abstrakten Basisklassen und virtuellen Funktionen und erben dann Implementierungen, die von diesen Basisklassen geerbt werden. Dies ist jedoch nur eine spezielle Methode, um das Prinzip der Schnittstelle zu implementieren. In C ++ können wir auch PIMPL , undurchsichtige Zeiger , Ententypisierung, typedef usw. verwenden. Sie können eine OOD-Struktur erstellen und diese dann in C implementieren, in dem es überhaupt keine OOP-Sprachschlüsselwörter gibt! Wenn ich also über Schnittstellen spreche, meine ich nicht unbedingt virtuelle Funktionen - ich spreche über das Prinzip, die Implementierung zu verbergen . Schnittstellen können polymorph sein , aber meistens sind sie es! Polymorphismus wird sehr selten richtig verwendet, aber Schnittstellen sind ein grundlegendes Konzept für jede Software.
- Wie ich oben klargestellt habe, wird diese Struktur als Schnittstelle verwendet, wenn Sie eine POD-Struktur erstellen, in der einfach einige Daten für die Übertragung von einer Klasse zu einer anderen gespeichert werden . Dies ist eine formale Beschreibung der Daten .
- Selbst wenn Sie nur eine separate Klasse mit dem öffentlichen und dem privaten Teil erstellen, ist alles, was sich im gemeinsamen Teil befindet, eine Schnittstelle , und alles im privaten Teil ist eine Implementierung .
- Die Vererbung hat tatsächlich (mindestens) zwei Typen - Schnittstellenvererbung und Implementierungsvererbung.
- In C ++ umfasst die Schnittstellenvererbung abstrakte Basisklassen mit rein virtuellen Funktionen, PIMPL und bedingtem Typedef. In Java wird die Schnittstellenvererbung durch das Schlüsselwort implements ausgedrückt.
- In C ++ erfolgt die Vererbung von Implementierungen jedes Mal, wenn die Basisklassen etwas anderes als reine virtuelle Funktionen enthalten. In Java wird die Vererbung der Implementierung mit dem Schlüsselwort extenses ausgedrückt.
- OOD hat viele Regeln für das Erben von Schnittstellen, aber die Vererbung von Implementierungen ist normalerweise als "Code mit einem Biss" zu betrachten !
Und schließlich sollte ich einige Beispiele für das schreckliche OOP-Training zeigen und wie es im wirklichen Leben zu schlechtem Code führt (und zu OOPs schlechtem Ruf).
- Als Ihnen Hierarchien / Vererbung beigebracht wurde, wurde Ihnen möglicherweise eine ähnliche Aufgabe übertragen: Angenommen, Sie haben eine Universitätsanwendung, die ein Verzeichnis von Studenten und Mitarbeitern enthält. Sie können die Basisklasse Person und dann die von Person geerbte Schülerklasse und die Staff-Klasse erstellen.
Nein nein Nein. Hier werde ich dich aufhalten. Die unausgesprochene Implikation des LSP-Prinzips ist, dass Klassenhierarchien und die Algorithmen, die sie verarbeiten, symbiotisch sind. Dies sind zwei Hälften des gesamten Programms. OOP ist eine Erweiterung der prozeduralen Programmierung und wird immer noch hauptsächlich mit diesen Prozeduren in Verbindung gebracht. Wenn wir nicht wissen, welche Arten von Algorithmen mit Schülern und Mitarbeitern funktionieren ( und welche Algorithmen aufgrund von Polymorphismus vereinfacht werden ), ist es völlig unverantwortlich, mit der Erstellung der Struktur von Klassenhierarchien zu beginnen. Zuerst müssen Sie die Algorithmen und Daten kennen. - Als Ihnen Hierarchien / Vererbung beigebracht wurde, wurde Ihnen wahrscheinlich eine ähnliche Aufgabe übertragen: Angenommen, Sie haben eine Klasse von Formen. Wir haben auch Quadrate und Rechtecke als Unterklassen. Sollte ein Quadrat ein Rechteck oder ein Rechteck ein Quadrat sein?
Dies ist tatsächlich ein gutes Beispiel, um den Unterschied zwischen der Vererbung von Implementierungen und der Vererbung von Schnittstellen zu demonstrieren.
TL; DR - Ihre OOP-Klasse hat Ihnen gesagt, wie Vererbung war. Ihre fehlende OOD-Klasse hätte Ihnen sagen sollen, dass Sie sie 99% der Zeit nicht verwenden sollen!
Entitäts- / Komponentenkonzepte
Nachdem wir uns mit den Voraussetzungen befasst haben, gehen wir weiter zu dem Punkt, an dem Aras begonnen hat - zum sogenannten Ausgangspunkt eines „typischen OOP“.
Aber für den Anfang noch eine Ergänzung: Aras nennt diesen Code „traditionelles OOP“, und ich möchte dem widersprechen. Dieser Code mag für OOP in der realen Welt typisch sein, verstößt jedoch wie die obigen Beispiele gegen alle Arten von Grundprinzipien von OO und sollte daher überhaupt nicht als traditionell angesehen werden.
Ich werde mit dem ersten Commit beginnen, bevor er die Struktur in Richtung ECS neu erstellt:
„Damit es wieder unter Windows funktioniert“ 3529f232510c95f53112bbfff87df6bbc6aa1fae
Ja, es ist schwierig, sofort hundert Codezeilen herauszufinden. Beginnen wir also schrittweise ... Wir brauchen einen weiteren Aspekt der Voraussetzungen - es war beliebt, die Vererbung in Spielen der 90er Jahre zu verwenden, um alle Probleme der Wiederverwendung von Code zu lösen. Sie hatten Entität, erweiterbaren Charakter, erweiterbaren Player und Monster und so weiter ... Dies ist eine Vererbung von Implementierungen, wie wir zuvor beschrieben haben (
"Code mit einem Choke" ), und es scheint richtig zu sein, damit zu beginnen, aber als Ergebnis führt dies zu einem sehr unflexible Codebasis. Weil OOD das oben beschriebene Prinzip „Zusammensetzung über Vererbung“ hat. In den 2000er Jahren wurde das Prinzip „Komposition über Vererbung“ populär, und Spieleentwickler begannen, ähnlichen Code zu schreiben.
Was macht dieser Code? Na ja nicht gut

Kurz gesagt,
dieser Code implementiert ein vorhandenes Merkmal der Sprache erneut - Komposition als Laufzeitbibliothek und nicht als Merkmal der Sprache. Sie können sich das so vorstellen, als würde der Code tatsächlich eine neue Metasprache über C ++ und eine virtuelle Maschine (VM) erstellen, um diese Metasprache auszuführen. Im Aras-Demospiel ist dieser Code nicht erforderlich (
wir werden ihn bald vollständig entfernen! ) Und dient nur dazu, die Spielleistung um das Zehnfache zu reduzieren.
Aber was macht er eigentlich? Dies ist das Konzept des "
E ntity /
C omponent-Systems" (
manchmal aus irgendeinem Grund als " E ntity / C omponent-System" bezeichnet ), es unterscheidet sich jedoch vollständig vom Konzept des "
E ntity
C" omponent System "(" Entity-Component-System ") (
das aus offensichtlichen Gründen niemals als " Entity Component System System "
bezeichnet wird ). Es formalisiert mehrere Prinzipien der" EC ":
- Das Spiel besteht aus Funktionen von "Entities" ("Entity") ( in diesem Beispiel GameObjects), die aus "Components" ("Component") bestehen.
- GameObjects implementieren das Muster "Service Locator" - ihre untergeordneten Komponenten werden nach Typ abgefragt.
- Komponenten wissen, zu welchem GameObject sie gehören - sie können Komponenten finden, die sich auf derselben Ebene wie sie befinden, indem sie das übergeordnete GameObject abfragen.
- Die Komposition kann nur eine Ebene tief sein ( Komponenten können keine eigenen untergeordneten Komponenten haben, GameObjects können keine untergeordneten GameObjects haben ).
- GameObject kann nur eine Komponente jedes Typs haben ( in einigen Frameworks ist dies eine obligatorische Anforderung, in anderen nicht ).
- Jede Komponente ändert sich (wahrscheinlich) im Laufe der Zeit auf eine nicht spezifizierte Weise, sodass die Schnittstelle ein "Virtual Void Update" enthält.
- GameObjects gehören zu einer Szene, die Abfragen für alle GameObjects (und damit für alle Komponenten) ausführen kann.
Ein ähnliches Konzept war in den 2000er Jahren sehr beliebt und erwies sich trotz seiner Einschränkungen als flexibel genug, um damals und heute unzählige Spiele zu erstellen.
Dies ist jedoch nicht erforderlich. Ihre Programmiersprache unterstützt bereits die Komposition als Merkmal der Sprache - es ist kein aufgeblähtes Konzept erforderlich, um darauf zuzugreifen ... Warum gibt es diese Konzepte dann? Um ehrlich zu sein, können Sie zur
Laufzeit dynamische Kompositionen durchführen . Anstatt GameObject-Typen im Code hart zu definieren, können Sie sie aus Datendateien laden. Und das ist sehr praktisch, weil es Spiel- / Level-Designern ermöglicht, ihre eigenen Objekttypen zu erstellen ... In den meisten Spielprojekten gibt es jedoch nur sehr wenige Designer und buchstäblich eine ganze Armee von Programmierern, daher würde ich argumentieren, dass dies eine wichtige Gelegenheit ist. Schlimmer noch, dies ist nicht die einzige Möglichkeit, eine Komposition zur Laufzeit zu implementieren! Zum Beispiel verwendet Unity C # als "Skriptsprache", und viele andere Spiele verwenden seine Alternativen, zum Beispiel Lua - ein praktisches Tool für Designer, das C # / Lua-Code generieren kann, um neue Spielobjekte zu definieren, ohne dass ein derart aufgeblähtes Konzept erforderlich ist! Wir werden dieses "Feature" im nächsten Beitrag erneut hinzufügen und so gestalten, dass es uns keinen zehnfachen Leistungsabfall kostet ...
Lassen Sie uns diesen Code gemäß OOD bewerten:
- GameObject :: GetComponent verwendet dynamic_cast. Die meisten Leute werden Ihnen sagen, dass dynamic_cast ein "Code mit einem Choke" ist, ein großer Hinweis darauf, dass Sie irgendwo einen Fehler haben. Ich würde dies sagen - dies ist ein Beweis dafür, dass Sie gegen den LSP verstoßen haben - Sie haben eine Art Algorithmus, der mit der Basisschnittstelle funktioniert, aber er muss verschiedene Implementierungsdetails kennen. Aus diesem Grund riecht der Code schlecht.
- GameObject ist im Prinzip nicht schlecht, wenn Sie sich vorstellen, dass es die Vorlage "Service Locator" implementiert ... aber wenn Sie aus Sicht von OOD über die Kritik hinausgehen, stellt diese Vorlage implizite Verbindungen zwischen Teilen des Projekts her, und ich denke ( ohne einen Link zu Wikipedia, der dies unterstützen kann Ich mit Kenntnissen aus der Informatik ), dass implizite Kommunikationskanäle ein Gegenmuster sind und explizite Kommunikationskanäle bevorzugen sollten. Das gleiche Argument gilt für das aufgeblähte "Konzept der Ereignisse", die manchmal in Spielen verwendet werden ...
- Ich möchte feststellen, dass eine Komponente eine Verletzung von SRP darstellt, weil ihre Schnittstelle ( Virtual Void Update (Zeit) ) zu breit ist. Die Verwendung von "Virtual Void Update" in der Spieleentwicklung ist allgegenwärtig, aber ich würde auch sagen, dass es sich um ein Antimuster handelt. Gute Software sollte es Ihnen ermöglichen, leicht über Kontrollfluss und Datenfluss nachzudenken. Wenn Sie jedes Element des Gameplay-Codes hinter dem Aufruf "Virtual Void Update" platzieren, werden der Kontrollstrom und der Datenstrom vollständig und vollständig verschleiert. IMHO, unsichtbare Nebenwirkungen , auch als Fernwirkung bezeichnet, sind einige der häufigsten Fehlerquellen, und „Virtual Void Update“ stellt sicher, dass fast alles eine unsichtbare Nebenwirkung ist.
- Obwohl das Ziel der Component-Klasse darin besteht, die Komposition zu aktivieren, erfolgt dies durch Vererbung, was eine Verletzung von CRP darstellt .
- Die einzig gute Seite dieses Beispiels ist, dass der Spielcode übertrieben ist, um den Prinzipien von SRP und ISP zu entsprechen - er ist in viele einfache Komponenten mit sehr geringer Verantwortung unterteilt, was sich hervorragend für die Wiederverwendung von Code eignet.
Er ist jedoch nicht so gut darin, DIP aufrechtzuerhalten - viele Komponenten kennen sich direkt.
Der gesamte oben gezeigte Code kann also tatsächlich gelöscht werden. Diese ganze Struktur. Löschen Sie GameObject (in anderen Frameworks auch Entity genannt), entfernen Sie Component und löschen Sie FindOfType. Dies ist Teil einer nutzlosen VM, die gegen OOD-Prinzipien verstößt und unser Spiel schrecklich verlangsamt.
Komposition ohne Frameworks (d. H. Unter Verwendung von Merkmalen der Programmiersprache selbst)
Wie können unsere GameObjects die Komposition verwenden und aus Komponenten bestehen, wenn wir das Kompositionsframework entfernen und nicht über die Component-Basisklasse verfügen? Wie der Titel schon sagt, anstatt diese aufgeblähte VM zu schreiben und GameObjects in einer seltsamen Metasprache darüber zu erstellen, schreiben wir sie einfach in C ++, weil wir Spielprogrammierer sind und dies buchstäblich unsere Aufgabe ist.
Hier ist das Commit, mit dem das Entity / Component-Framework entfernt wurde:
https://github.com/hodgman/dod-playground/commit/f42290d0217d700dea2ed002f2f3b1dc45e8c27cHier ist die Originalversion des Quellcodes:
https://github.com/hodgman/dod-playground/blob/3529f232510c95f53112bbfff87df6bbc6aa1fae/source/game.cppHier ist die geänderte Version des Quellcodes:
https://github.com/hodgman/dod-playground/blob/f42290d0217d700dea2ed002f2f3b1dc45e8c27c/source/game.cppKurz zu den Änderungen:
- ": Public Component" wurde aus jedem Komponententyp entfernt.
- Jedem Komponententyp wurde ein Konstruktor hinzugefügt.
- Bei OOD geht es hauptsächlich darum, den Status einer Klasse zu kapseln. Da diese Klassen jedoch so klein / einfach sind, gibt es nichts Besonderes zu verbergen: Eine Schnittstelle ist eine Beschreibung der Daten. Einer der Hauptgründe dafür, dass die Kapselung die Hauptsäule ist, besteht darin, dass wir die konstante Wahrheit von Klasseninvarianten garantieren können. Wenn die Invariante gebrochen ist, müssen Sie nur den gekapselten Implementierungscode untersuchen, um den Fehler zu finden. In diesem Codebeispiel lohnt es sich, Konstruktoren hinzuzufügen, um eine einfache Invariante zu implementieren - alle Werte müssen initialisiert werden.
- Ich habe die zu allgemeinen "Update" -Methoden umbenannt, damit ihre Namen das widerspiegeln, was sie tatsächlich tun - UpdatePosition für MoveComponent und ResolveCollisions für AvoidComponent.
- Ich habe drei fest codierte Codeblöcke entfernt, die einer Vorlage / einem Fertighaus ähnelten - den Code, der ein GameObject mit bestimmten Komponententypen erstellt, und es durch drei C ++ - Klassen ersetzt.
- Antipattern "Virtual Void Update" beseitigt.
- Anstatt dass die Komponenten über die Vorlage "Service Locator" nacheinander suchen, werden sie während des Aufbaus vom Spiel explizit miteinander verbunden.
Die Objekte
Daher anstelle dieses Codes für "virtuelle Maschinen":
Wir haben jetzt regulären C ++ - Code:
struct RegularObject { PositionComponent pos; SpriteComponent sprite; MoveComponent move; AvoidComponent avoid; RegularObject(const WorldBoundsComponent& bounds) : move(0.5f, 0.7f)
Algorithmen
Eine weitere wichtige Änderung wurde an den Algorithmen vorgenommen. Denken Sie daran, am Anfang habe ich gesagt, dass Schnittstellen und Algorithmen in Symbiose funktionieren und sich gegenseitig beeinflussen sollten. So ist auch hier das Antipattern "
Virtual Void Update " zum Feind geworden. Der Anfangscode enthält den Hauptschleifenalgorithmus, der nur aus folgenden Elementen besteht:
Sie können argumentieren, dass es schön und einfach ist, aber meiner Meinung nach ist es sehr, sehr schlecht. Dies verschleiert sowohl den
Kontrollfluss als auch den
Datenfluss innerhalb des Spiels vollständig. Wenn wir unsere Software verstehen wollen, wenn wir sie unterstützen wollen, wenn wir neue Dinge hinzufügen, optimieren, effizient auf mehreren Prozessorkernen ausführen wollen, müssen wir sowohl den Kontrollfluss als auch den Datenfluss verstehen. Daher muss "Virtual Void Update" in Brand gesetzt werden.
Stattdessen haben wir eine explizitere Hauptschleife erstellt, die das Verständnis des Kontrollflusses erheblich vereinfacht (der darin
enthaltene Datenfluss ist immer noch verschleiert, wir werden dies jedoch in den folgenden Commits beheben ).
Der Nachteil dieses Stils ist, dass wir für
jeden neuen Objekttyp , der dem Spiel hinzugefügt wird, mehrere Zeilen zur Hauptschleife hinzufügen müssen. Ich werde in einem späteren Beitrag aus dieser Serie darauf zurückkommen.
Leistung
Es gibt viele große OOD-Verstöße, einige schlechte Entscheidungen werden bei der Auswahl einer Struktur getroffen und es gibt viele Möglichkeiten zur Optimierung, aber ich werde im nächsten Beitrag der Serie darauf eingehen. Bereits zu diesem Zeitpunkt ist jedoch klar, dass die Version mit „festem OOD“ fast vollständig mit dem endgültigen „ECS“ -Code am Ende der Präsentation übereinstimmt oder diesen gewinnt. Und wir haben nur den schlechten Pseudo-OOP-Code genommen und dafür gesorgt, dass er den Prinzipien entspricht OOP (und auch hundert Codezeilen gelöscht)!
Nächste Schritte
Hier möchte ich ein viel breiteres Spektrum von Problemen betrachten, einschließlich der Lösung der verbleibenden OOD-Probleme, unveränderlicher Objekte (
Programmierung in einem funktionalen Stil ) und der Vorteile, die sie in Diskussionen über Datenflüsse, Nachrichtenübermittlung und Anwendung der DOD-Logik auf unseren OOD-Code bringen können. Anwenden relevanter Weisheit in OOD-Code, Entfernen dieser Klassen von „Entitäten“, mit denen wir am Ende endeten, und Verwenden nur reiner Komponenten unter Verwendung verschiedener Stile zum Verbinden von Komponenten (Vergleichen von Zeigern und die Verantwortung zu tragen) Komponenten von Behältern aus der realen Welt, ECS-Revision Version für eine bessere Optimierung, sowie die weitere Optimierung, die nicht im Bericht Aras
(wie Multi-Threading / SIMD) erwähnt. Die Reihenfolge wird nicht unbedingt so sein, und vielleicht werde ich nicht alle oben genannten Punkte berücksichtigen ...
Ergänzung
Die Links zu dem Artikel haben sich über die Kreise der Spieleentwickler hinaus verbreitet, daher füge ich hinzu: "
ECS " (
dieser Wikipedia-Artikel ist übrigens schlecht, er kombiniert die Konzepte von EC und ECS, und dies ist nicht dasselbe ... ) - dies ist eine gefälschte Vorlage, die innerhalb von Communities verbreitet wird Spieleentwickler. Tatsächlich handelt es sich um eine Version des relationalen Modells, bei der „Entitäten“ nur IDs sind, die ein formloses Objekt kennzeichnen, „Komponenten“ Zeilen in bestimmten Tabellen sind, die auf IDs verweisen, und „Systeme“ Prozedurcode sind, der Komponenten ändern kann . Diese „Vorlage“ wurde immer als Lösung für das Problem der übermäßigen Verwendung von Vererbung positioniert, es wird jedoch nicht erwähnt, dass die übermäßige Verwendung von Vererbung tatsächlich gegen die Empfehlungen des OOP verstößt. Daher meine Empörung. Dies ist nicht der „einzig wahre Weg“, Software zu schreiben. Der Beitrag soll sicherstellen, dass die Leute tatsächlich etwas über bestehende Designprinzipien lernen.