Was für eine Mutter von einer Frage! Es könnte mir peinlich sein, dies mit meinen skurrilen Gedanken zu versuchen (und ich würde gerne Vorschläge hören, wenn ich wirklich abwesend bin). Für mich war es jedoch das Nützlichste, was ich in letzter Zeit in meiner Domäne gelernt habe (einschließlich Spielen in der Vergangenheit, jetzt VFX), Interaktionen zwischen abstrakten Schnittstellen mit Daten als Entkopplungsmechanismus zu ersetzen (und letztendlich die zwischen ihnen erforderliche Informationsmenge zu reduzieren Dinge und über einander bis auf das kleinste Minimum). Das klingt vielleicht völlig verrückt (und ich verwende möglicherweise alle möglichen schlechten Begriffe).
Nehmen wir jedoch an, ich gebe Ihnen einen einigermaßen überschaubaren Job. Sie haben diese Datei mit Szenen- und Animationsdaten zum Rendern. Es gibt eine Dokumentation zum Dateiformat. Ihre einzige Aufgabe besteht darin, die Datei zu laden, hübsche Bilder für die Animation mithilfe der Pfadverfolgung zu rendern und die Ergebnisse in Bilddateien auszugeben. Das ist eine ziemlich kleine Anwendung, die wahrscheinlich nicht mehr als Zehntausende von LOC umfassen wird, selbst für einen ziemlich hoch entwickelten Renderer (definitiv nicht Millionen).
Sie haben Ihre eigene kleine isolierte Welt für diesen Renderer. Es wird nicht von der Außenwelt beeinflusst. Es isoliert seine eigene Komplexität. Abgesehen von den Bedenken, diese Szenendatei zu lesen und Ihre Ergebnisse in Bilddateien auszugeben, können Sie sich ganz auf das reine Rendern konzentrieren. Wenn dabei etwas schief geht, wissen Sie, dass es sich im Renderer befindet und nichts anderes, da dieses Bild nichts anderes enthält.
Nehmen wir stattdessen an, Sie müssen Ihren Renderer im Kontext einer großen Animationssoftware arbeiten lassen, die tatsächlich Millionen von LOC enthält. Anstatt nur ein optimiertes, dokumentiertes Dateiformat zu lesen, um an die für das Rendern erforderlichen Daten zu gelangen, müssen Sie alle Arten von abstrakten Schnittstellen durchlaufen, um alle Daten abzurufen, die Sie für Ihre Arbeit benötigen:
Plötzlich befindet sich Ihr Renderer nicht mehr in seiner eigenen kleinen isolierten Welt. Das fühlt sich so viel komplexer an. Sie müssen das Gesamtdesign einer ganzen Menge der Software als ein organisches Ganzes mit möglicherweise vielen beweglichen Teilen verstehen und vielleicht sogar manchmal über Implementierungen von Dingen wie Netzen oder Kameras nachdenken, wenn Sie auf einen Engpass oder einen Fehler in einem von ihnen stoßen die Funktionen.
Funktionalität vs. optimierte Daten
Einer der Gründe ist, dass die Funktionalität viel komplexer ist als statische Daten. Es gibt auch so viele Möglichkeiten, wie ein Funktionsaufruf auf eine Weise schief gehen kann, die das Lesen statischer Daten nicht kann. Es gibt so viele versteckte Nebenwirkungen, die beim Aufrufen dieser Funktionen auftreten können, obwohl konzeptionell nur schreibgeschützte Daten zum Rendern abgerufen werden. Es kann auch so viele weitere Gründe geben, sich zu ändern. In einigen Monaten kann es vorkommen, dass die Netz- oder Texturschnittstelle Teile so ändert oder ablehnt, dass Sie wichtige Abschnitte Ihres Renderers neu schreiben und mit diesen Änderungen Schritt halten müssen, obwohl Sie genau dieselben Daten abrufen Die Dateneingabe in Ihren Renderer hat sich nicht geändert (nur die Funktionalität, die erforderlich ist, um letztendlich auf alles zuzugreifen).
Wenn möglich, habe ich festgestellt, dass optimierte Daten ein sehr guter Entkopplungsmechanismus sind, mit dem Sie wirklich vermeiden müssen, über das gesamte System als Ganzes nachzudenken, und sich nur auf einen bestimmten Teil des Systems konzentrieren können Verbesserungen, Hinzufügen neuer Funktionen, Beheben von Problemen usw. Es folgt einer sehr E / A-Denkweise für die sperrigen Teile, aus denen Ihre Software besteht. Geben Sie dies ein, machen Sie Ihr Ding, geben Sie das aus, und ohne Dutzende abstrakter Schnittstellen zu durchlaufen, beenden Sie endlose Funktionsaufrufe auf dem Weg. Und es fängt an, bis zu einem gewissen Grad einer funktionalen Programmierung zu ähneln.
Dies ist also nur eine Strategie und möglicherweise nicht für alle Menschen anwendbar. Und wenn Sie alleine fliegen, müssen Sie natürlich immer noch alles beibehalten (einschließlich des Formats der Daten selbst), aber der Unterschied besteht darin, dass Sie sich wirklich auf den Renderer konzentrieren können, wenn Sie sich hinsetzen, um Verbesserungen an diesem Renderer vorzunehmen zum größten Teil und sonst nichts. Es wird in seiner eigenen kleinen Welt so isoliert - ungefähr so isoliert wie es sein könnte, wenn die Daten, die es für die Eingabe benötigt, so rationalisiert werden.
Ich habe das Beispiel eines Dateiformats verwendet, aber es muss keine Datei sein, die die optimierten Daten von Interesse für die Eingabe bereitstellt. Es könnte sich um eine In-Memory-Datenbank handeln. In meinem Fall handelt es sich um ein Entity-Component-System, bei dem die Komponenten die interessierenden Daten speichern. Ich habe jedoch festgestellt, dass dieses Grundprinzip der Entkopplung zu optimierten Daten (wie auch immer Sie es tun) meine geistige Leistungsfähigkeit so viel weniger belastet als frühere Systeme, an denen ich gearbeitet habe und die sich um Abstraktionen und viele, viele, viele Interaktionen zwischen all diesen drehten abstrakte Schnittstellen, die es unmöglich machten, sich nur mit einer Sache hinzusetzen und nur darüber und über wenig anderes nachzudenken. Mein Gehirn füllte sich bis an den Rand mit diesen früheren Systemtypen und wollte explodieren, weil so viele Interaktionen zwischen so vielen Dingen stattfanden.
Entkopplung
Wenn Sie minimieren möchten, wie viel größere Codebasen Ihr Gehirn belasten, machen Sie es so, dass jeder wichtige Teil der Software (ein ganzes Rendering-System, ein ganzes Physik-System usw.) in einer möglichst isolierten Welt lebt. Minimieren Sie den Umfang der Kommunikation und Interaktion, der durch die optimiertesten Daten auf ein Minimum reduziert wird. Sie können sogar Redundanz akzeptieren (redundante Arbeit für den Prozessor oder sogar für sich selbst), wenn der Austausch ein weitaus isolierteres System ist, das nicht mit Dutzenden anderer Dinge sprechen muss, bevor es seine Arbeit erledigen kann.
Und wenn Sie damit beginnen, haben Sie das Gefühl, Sie verwalten ein Dutzend kleiner Anwendungen anstelle einer gigantischen. Und ich finde das auch viel lustiger. Sie können sich hinsetzen und nach Herzenslust an einem System arbeiten, ohne sich mit der Außenwelt zu befassen. Es wird nur die Eingabe der richtigen Daten und die Ausgabe der richtigen Daten am Ende an einen Ort, an dem andere Systeme darauf zugreifen können (an diesem Punkt könnte ein anderes System dies eingeben und seine Sache tun, aber das muss Sie nicht interessieren bei der Arbeit an Ihrem System). Natürlich müssen Sie zum Beispiel immer noch darüber nachdenken, wie alles in die Benutzeroberfläche integriert ist (ich muss immer noch über das gesamte Design für GUIs nachdenken), aber zumindest nicht, wenn Sie sich hinsetzen und an diesem vorhandenen System arbeiten oder beschließen Sie, eine neue hinzuzufügen.
Vielleicht beschreibe ich Menschen, die mit den neuesten technischen Methoden auf dem neuesten Stand sind, etwas Offensichtliches. Ich weiß es nicht. Aber es war mir nicht klar. Ich wollte mich dem Design von Software um Objekte nähern, die miteinander interagieren, und Funktionen, die für umfangreiche Software benötigt werden. Und die Bücher, die ich ursprünglich über umfangreiches Software-Design gelesen habe, konzentrierten sich auf Schnittstellendesigns über Dinge wie Implementierungen und Daten (das Mantra war damals, dass Implementierungen nicht so wichtig sind, sondern nur Schnittstellen, da erstere leicht ausgetauscht oder ersetzt werden können ). Anfangs war es für mich nicht intuitiv, die Interaktionen einer Software als bloße Eingabe und Ausgabe von Daten zwischen riesigen Subsystemen zu betrachten, die nur über diese optimierten Daten miteinander kommunizieren. Als ich mich jedoch darauf konzentrierte, dieses Konzept zu entwerfen, machte es die Dinge so viel einfacher. Ich könnte so viel mehr Code hinzufügen, ohne dass mein Gehirn explodiert. Es fühlte sich an, als würde ich ein Einkaufszentrum anstelle eines Turms bauen, der einstürzen könnte, wenn ich zu viel hinzufüge oder wenn in einem Teil ein Bruch vorliegt.
Komplexe Implementierungen vs. komplexe Interaktionen
Dies ist eine weitere, die ich erwähnen sollte, da ich einen Großteil meines frühen Teils meiner Karriere damit verbracht habe, nach den einfachsten Implementierungen zu suchen. Also zerlegte ich die Dinge in die kleinsten und einfachsten Teile und dachte, ich würde die Wartbarkeit verbessern.
Im Nachhinein merkte ich nicht, dass ich eine Art von Komplexität gegen eine andere austauschte. Indem alles auf das Einfachste reduziert wurde, wurden die Interaktionen zwischen diesen kleinen Teilen zu einem komplexesten Netz von Interaktionen mit Funktionsaufrufen, die manchmal 30 Ebenen tief in den Callstack gingen. Und wenn Sie sich eine Funktion ansehen, ist es natürlich so einfach und leicht zu wissen, was sie tut. Zu diesem Zeitpunkt erhalten Sie jedoch nicht viele nützliche Informationen, da jede Funktion so wenig leistet. Sie müssen dann alle Arten von Funktionen nachverfolgen und durch alle Arten von Reifen springen, um tatsächlich herauszufinden, was sie alle auf eine Weise bewirken, die Ihr Gehirn dazu bringen kann, mehr als eine größere zu explodieren.
Das soll nicht bedeuten, dass Gott Objekte oder ähnliches sind. Aber vielleicht müssen wir unsere Netzobjekte nicht in kleinste Dinge wie ein Scheitelpunktobjekt, ein Kantenobjekt oder ein Gesichtsobjekt zerlegen. Vielleicht könnten wir es einfach mit einer moderat komplexeren Implementierung im Netz halten, um radikal weniger Code-Interaktionen zu erhalten. Ich kann hier und da eine mäßig komplexe Implementierung durchführen. Ich kann nicht mit einer Unmenge von Wechselwirkungen mit Nebenwirkungen umgehen, die wer-weiß-wo und in welcher Reihenfolge auftreten.
Zumindest finde ich das Gehirn sehr viel weniger belastend, weil es die Interaktionen sind, die mein Gehirn in einer großen Codebasis verletzen. Keine bestimmte Sache.
Allgemeinheit vs. Spezifität
Vielleicht mit dem oben Gesagten verbunden, liebte ich die Allgemeinheit und die Wiederverwendung von Code und dachte immer, dass die größte Herausforderung beim Entwerfen einer guten Schnittstelle darin bestand, die unterschiedlichsten Anforderungen zu erfüllen, da die Schnittstelle von allen möglichen Dingen mit unterschiedlichen Anforderungen verwendet werden würde. Und wenn Sie das tun, müssen Sie unweigerlich über hundert Dinge gleichzeitig nachdenken, weil Sie versuchen, die Bedürfnisse von hundert Dingen gleichzeitig auszugleichen.
Das Verallgemeinern von Dingen braucht so viel Zeit. Schauen Sie sich einfach die Standardbibliotheken an, die unsere Sprachen begleiten. Die C ++ - Standardbibliothek enthält so wenig Funktionen, dass jedoch Teams von Personen erforderlich sind, um ganze Komitees von Personen zu pflegen und mit ihnen abzustimmen, die über ihr Design diskutieren und Vorschläge machen. Das liegt daran, dass diese kleine Funktionalität versucht, die Bedürfnisse der ganzen Welt zu erfüllen.
Vielleicht müssen wir die Dinge nicht so weit bringen. Vielleicht ist es in Ordnung, nur einen räumlichen Index zu haben, der nur zur Kollisionserkennung zwischen indizierten Netzen und sonst nichts verwendet wird. Vielleicht können wir eine andere für andere Arten von Oberflächen und eine andere zum Rendern verwenden. Früher habe ich mich so darauf konzentriert, diese Art von Redundanzen zu beseitigen, aber ein Grund dafür war, dass ich es mit sehr ineffizienten Datenstrukturen zu tun hatte, die von einer Vielzahl von Menschen implementiert wurden. Wenn Sie einen Octree haben, der 1 Gigabyte für ein 300k-Dreiecksnetz benötigt, möchten Sie natürlich keinen weiteren im Speicher haben.
Aber warum sind die Oktrees überhaupt so ineffizient? Ich kann Oktrees erstellen, die nur 4 Bytes pro Knoten und weniger als ein Megabyte benötigen, um dasselbe wie diese Gigabyte-Version zu tun, während ich in einem Bruchteil der Zeit baue und schnellere Suchabfragen durchführe. Zu diesem Zeitpunkt ist eine gewisse Redundanz völlig akzeptabel.
Effizienz
Dies ist also nur für leistungskritische Bereiche relevant. Je besser Sie jedoch mit der Speichereffizienz umgehen, desto mehr können Sie es sich leisten, ein bisschen mehr (möglicherweise etwas mehr Redundanz im Austausch für eine geringere Allgemeinheit oder Entkopplung) zugunsten der Produktivität zu verschwenden . Und dort ist es hilfreich, sich mit Ihren Profilern vertraut zu machen und sich mit der Computerarchitektur und der Speicherhierarchie vertraut zu machen, denn dann können Sie es sich leisten, im Austausch für Produktivität mehr Einbußen bei der Effizienz zu machen, weil Ihr Code bereits so effizient ist und es sich leisten kann selbst in den kritischen Bereichen etwas weniger effizient und dennoch besser als die Konkurrenz. Ich habe festgestellt, dass die Verbesserung in diesem Bereich es mir auch ermöglicht hat, mit immer einfacheren Implementierungen davonzukommen.
Verlässlichkeit
Dies ist offensichtlich, könnte es aber genauso gut erwähnen. Ihre zuverlässigsten Dinge erfordern den minimalen intellektuellen Aufwand. Sie müssen nicht viel über sie nachdenken. Sie arbeiten einfach. Je größer Ihre Liste an äußerst zuverlässigen Teilen ist, die durch gründliche Tests auch "stabil" sind (nicht geändert werden müssen), desto weniger müssen Sie darüber nachdenken.
Besonderheiten
All das oben Genannte behandelt einige allgemeine Dinge, die mir geholfen haben, aber lassen Sie uns zu spezifischeren Aspekten für Ihre Region übergehen:
In meinen kleineren Projekten fällt es mir leicht, mich an eine mentale Karte zu erinnern, wie jeder Teil des Programms funktioniert. Auf diese Weise kann ich genau wissen, wie sich Änderungen auf den Rest des Programms auswirken, Fehler sehr effektiv vermeiden und genau sehen, wie eine neue Funktion in die Codebasis passen sollte. Wenn ich jedoch versuche, größere Projekte zu erstellen, finde ich es unmöglich, eine gute mentale Karte zu führen, was zu sehr chaotischem Code und zahlreichen unbeabsichtigten Fehlern führt.
Für mich hängt dies tendenziell mit komplexen Nebenwirkungen und komplexen Kontrollabläufen zusammen. Das ist eine eher einfache Sicht der Dinge, aber all die am schönsten aussehenden Schnittstellen und die Entkopplung vom Konkreten zum Abstrakten können es nicht einfacher machen, über komplexe Nebenwirkungen in komplexen Kontrollabläufen nachzudenken.
Vereinfachen / reduzieren Sie die Nebenwirkungen und / oder vereinfachen Sie die Kontrollflüsse, idealerweise beides. und Sie werden es im Allgemeinen viel einfacher finden, darüber nachzudenken, was viel größere Systeme tun und was als Reaktion auf Ihre Änderungen passieren wird.
Zusätzlich zu diesem "Mental Map" -Problem fällt es mir schwer, meinen Code von anderen Teilen seiner selbst zu entkoppeln. Wenn es beispielsweise in einem Multiplayer-Spiel eine Klasse gibt, die sich mit der Physik der Spielerbewegung befasst, und eine andere, die sich mit dem Networking befasst, sehe ich keine Möglichkeit, dass sich eine dieser Klassen nicht auf die andere verlässt, um Spielerbewegungsdaten in das Netzwerksystem zu übertragen um es über das Netzwerk zu senden. Diese Kopplung ist eine wesentliche Quelle für die Komplexität, die eine gute mentale Karte beeinträchtigt.
Konzeptionell muss man eine Kopplung haben. Wenn Menschen über Entkopplung sprechen, meinen sie normalerweise, eine Art durch eine andere, wünschenswertere Art zu ersetzen (typischerweise in Richtung Abstraktionen). Angesichts meiner Domäne, der Funktionsweise meines Gehirns usw. sind für mich die oben diskutierten optimierten Daten die wünschenswerteste Art, die Anforderungen an die "mentale Karte" auf ein Minimum zu reduzieren. Eine Black Box spuckt Daten aus, die in eine andere Black Box eingespeist werden, und beide wissen nichts über die Existenz des anderen. Sie kennen nur einen zentralen Ort, an dem Daten gespeichert werden (z. B. ein zentrales Dateisystem oder eine zentrale Datenbank), über den sie ihre Eingaben abrufen, etwas tun und eine neue Ausgabe ausspucken, die dann möglicherweise von einer anderen Black Box eingegeben wird.
Wenn Sie dies auf diese Weise tun, würde das Physiksystem von der zentralen Datenbank und das Netzwerksystem von der zentralen Datenbank abhängen, aber sie würden nichts voneinander wissen. Sie müssten nicht einmal wissen, dass es sie gibt. Sie müssten nicht einmal wissen, dass abstrakte Schnittstellen für einander existieren.
Schließlich finde ich oft eine oder mehrere "Manager" -Klassen, die andere Klassen koordinieren. In einem Spiel würde beispielsweise eine Klasse die Haupt-Tick-Schleife handhaben und Aktualisierungsmethoden in den Netzwerk- und Spielerklassen aufrufen. Dies steht im Widerspruch zu der Philosophie, die ich in meiner Forschung festgestellt habe, dass jede Klasse unabhängig von anderen Einheitenprüfbar und verwendbar sein sollte, da jede solche Managerklasse aufgrund ihres Zwecks von den meisten anderen Klassen im Projekt abhängt. Darüber hinaus ist die Orchestrierung des Restes des Programms durch Manager-Klassen eine bedeutende Quelle für nicht mental abbildbare Komplexität.
Sie benötigen in der Regel etwas, um alle Systeme in Ihrem Spiel zu orchestrieren. Central ist vielleicht zumindest weniger komplex und überschaubarer als ein Physiksystem, das ein Rendering-System aufruft, nachdem es fertig ist. Aber hier müssen zwangsläufig einige Funktionen aufgerufen werden, und vorzugsweise sind sie abstrakt.
Sie können also eine abstrakte Schnittstelle für ein System mit einer abstrakten update
Funktion erstellen . Es kann sich dann bei der zentralen Engine registrieren und Ihr Netzwerksystem kann sagen: "Hey, ich bin ein System und hier ist meine Update-Funktion. Bitte rufen Sie mich von Zeit zu Zeit an." Und dann kann Ihre Engine alle diese Systeme durchlaufen und sie aktualisieren, ohne Funktionsaufrufe an bestimmte Systeme fest zu codieren.
Dadurch können Ihre Systeme mehr wie in ihrer eigenen isolierten Welt leben. Die Spiel-Engine muss sie nicht mehr speziell (auf konkrete Weise) kennen. Und dann wird möglicherweise die Aktualisierungsfunktion Ihres Physiksystems aufgerufen. Zu diesem Zeitpunkt gibt es die Daten ein, die es für die Bewegung aller Elemente aus der zentralen Datenbank benötigt, wendet die Physik an und gibt die resultierende Bewegung zurück.
Danach wird möglicherweise die Aktualisierungsfunktion Ihres Netzwerksystems aufgerufen. Zu diesem Zeitpunkt gibt es die benötigten Daten aus der zentralen Datenbank ein und gibt beispielsweise Socket-Daten an Clients aus. Aus meiner Sicht ist es wieder das Ziel, jedes System so weit wie möglich zu isolieren, damit es in seiner eigenen kleinen Welt mit minimalem Wissen über die Außenwelt leben kann. Dies ist im Grunde der in ECS verfolgte Ansatz, der bei Game-Engines beliebt ist.
ECS
Ich denke, ich sollte ECS ein wenig behandeln, da sich viele meiner obigen Gedanken um ECS drehen und versuchen zu rationalisieren, warum dieser datenorientierte Ansatz zur Entkopplung die Wartung so viel einfacher gemacht hat als die objektorientierten und COM-basierten Systeme, die ich gewartet habe in der Vergangenheit trotz der Verletzung von fast allem, was ich ursprünglich für heilig hielt, was ich über SE gelernt habe. Es kann auch sehr sinnvoll für Sie sein, wenn Sie versuchen, größere Spiele zu entwickeln. ECS funktioniert also folgendermaßen:
Und wie im obigen Diagramm MovementSystem
könnte die update
Funktion des aufgerufen werden. Zu diesem Zeitpunkt wird möglicherweise die zentrale Datenbank nach PosAndVelocity
Komponenten als einzugebende Daten abgefragt (Komponenten sind nur Daten, keine Funktionalität). Dann kann es diese durchlaufen, die Positionen / Geschwindigkeiten ändern und die neuen Ergebnisse effektiv ausgeben. Dann wird RenderingSystem
möglicherweise die Aktualisierungsfunktion aufgerufen. Zu diesem Zeitpunkt fragt sie die Datenbank nach PosAndVelocity
und nach Sprite
Komponenten ab und gibt basierend auf diesen Daten Bilder auf dem Bildschirm aus.
Alle Systeme sind sich der Existenz des anderen überhaupt nicht bewusst und müssen nicht einmal verstehen, was a Car
ist. Sie müssen nur bestimmte Komponenten des Interesses jedes Systems kennen, aus denen die Daten bestehen, die für die Darstellung einer Komponente erforderlich sind. Jedes System ist wie eine Black Box. Es gibt Daten ein und gibt Daten mit minimalem Wissen über die Außenwelt aus, und die Außenwelt hat auch nur minimales Wissen darüber. Es kann vorkommen, dass ein Ereignis von einem System aus verschoben und von einem anderen entfernt wird, sodass beispielsweise bei der Kollision zweier Entitäten im Physiksystem im Audio ein Kollisionsereignis angezeigt wird, bei dem Ton abgespielt wird, die Systeme jedoch möglicherweise nicht wahrgenommen werden über einander. Und ich fand solche Systeme so viel einfacher zu überlegen. Sie lassen mein Gehirn nicht explodieren, selbst wenn Sie Dutzende von Systemen haben, weil jedes so isoliert ist. Du ziehst nicht Sie müssen nicht über die Komplexität von allem als Ganzes nachdenken, wenn Sie hineinzoomen und an einem bestimmten arbeiten. Aus diesem Grund ist es auch sehr einfach, die Ergebnisse Ihrer Änderungen vorherzusagen.