Ich erstelle oft Programme für mich und andere mit verschiedenen objektorientierten Programmiersprachen. Dabei sind sie normalerweise relativ klein (höchstens einige tausend Zeilen). In letzter Zeit habe ich jedoch versucht, größere Projekte wie komplette Spiel-Engines zu realisieren. Dabei stoße ich oft auf eine Straßensperre: Komplexität.
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.
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 bedeutende Quelle für die Komplexität, die eine gute mentale Karte beeinträchtigt.
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.
Zusammengenommen hindert mich dies daran, qualitativ hochwertige fehlerfreie Software von beträchtlicher Größe zu schreiben. Was tun professionelle Entwickler, um dieses Problem effektiv zu lösen? Ich bin besonders an OOP-Antworten interessiert, die auf Java und C ++ abzielen, aber diese Art von Rat ist wahrscheinlich sehr allgemein.
Anmerkungen:
- Ich habe versucht, UML-Diagramme zu verwenden, aber das scheint nur beim ersten Problem zu helfen, und selbst dann nur, wenn es um die Klassenstruktur geht und nicht (zum Beispiel) um die Reihenfolge der Methodenaufrufe in Bezug auf das, was zuerst initialisiert wird.
Antworten:
Es gibt ganze Bücher zu diesem Thema. Hier ist ein Zitat aus einem der wichtigsten Bücher, die jemals über Softwareentwicklung geschrieben wurden, Steve McConnells Code Complete :
Abgesehen davon würde ich das Lesen des Buches sehr empfehlen, wenn Sie überhaupt Interesse an Softwareentwicklung haben (was ich vermute, da Sie diese Frage gestellt haben). Klicken Sie mindestens auf den obigen Link und lesen Sie den Auszug über Designkonzepte.
In diesem speziellen Fall würde ich Ihre
PlayerMovementCalculator
Klasse und IhreNetworkSystem
Klasse als völlig unabhängig voneinander betrachten. Eine Klasse ist für die Berechnung der Spielerbewegung verantwortlich, die andere für die Netzwerk-E / A. Vielleicht sogar in separaten unabhängigen Modulen.Ich würde jedoch sicherlich erwarten, dass es irgendwo außerhalb dieser Module zumindest ein zusätzliches Stück Verkabelung oder Kleber gibt , das Daten und / oder Ereignisse / Nachrichten zwischen ihnen vermittelt. Beispielsweise können Sie eine
PlayerNetworkMediator
Klasse mithilfe des Mediator-Musters schreiben .Ein anderer möglicher Ansatz könnte darin bestehen, Ihre Module mithilfe eines Ereignisaggregators zu entkoppeln .
Im Fall von asynchroner Programmierung, wie z. B. der Art der Logik, die mit Netzwerksockets verbunden ist, können Sie Expose Observables verwenden , um den Code aufzuräumen, der diese Benachrichtigungen abhört.
Asynchrone Programmierung bedeutet nicht unbedingt auch Multithreading. Es geht mehr um Programmstruktur und Flusskontrolle (obwohl Multithreading der offensichtliche Anwendungsfall für Asynchronität ist). Observables können in einem oder beiden dieser Module nützlich sein, damit nicht verwandte Klassen Änderungsbenachrichtigungen abonnieren können.
Zum Beispiel:
usw.
Während einiges davon sicherlich auf Erfahrung zurückzuführen ist; Der Name
Manager
in einer Klasse weist häufig auf einen Designgeruch hin .Wenn Klassen zu benennen, sollten Sie die Funktionalität dieser Klasse für, verantwortlich ist und lassen Sie Ihre Klassennamen reflektieren , was sie tut .
Das Problem mit Managern im Code ist ein bisschen wie das Problem mit Managern am Arbeitsplatz. Ihr Zweck ist in der Regel vage und selbst für sich selbst schlecht verstanden. Die meiste Zeit sind wir ohne sie einfach besser dran.
Bei der objektorientierten Programmierung geht es hauptsächlich um Verhalten . Eine Klasse ist keine Datenentität, sondern eine Darstellung einiger funktionaler Anforderungen in Ihrem Code.
Wenn Sie eine Klasse basierend auf den funktionalen Anforderungen benennen können, die sie erfüllt, verringern Sie die Wahrscheinlichkeit, dass Sie ein aufgeblähtes Gottobjekt erhalten , und es ist wahrscheinlicher, dass Sie eine Klasse haben, deren Identität und Zweck in Ihrem Programm klar ist.
Darüber hinaus sollte es offensichtlicher sein, wenn sich zusätzliche Methoden und Verhaltensweisen einschleichen, wenn sie wirklich nicht dazu gehören, da der Name falsch aussieht - dh Sie haben eine Klasse, die eine ganze Reihe von Dingen erledigt, die nicht funktionieren. t spiegelt sich in seinem Namen wider
Vermeiden Sie schließlich die Versuchung, Klassen zu schreiben, deren Namen so aussehen, als gehörten sie zu einem Entitätsbeziehungsmodell. Das Problem mit dem Klassennamen wie
Player
,Monster
,Car
,Dog
usw. ist , dass das nichts über ihr Verhalten impliziert, und scheint nur eine Sammlung von logisch verknüpften Daten oder Attributen zu beschreiben. Objektorientiertes Design ist keine Datenmodellierung, sondern eine Verhaltensmodellierung.Betrachten Sie beispielsweise zwei verschiedene Methoden zur Modellierung eines Schadens
Monster
und zurPlayer
Berechnung des Schadens:Das Problem hierbei ist, dass Sie vernünftigerweise eine ganze Reihe anderer Methoden erwarten
Player
undMonster
haben können, die wahrscheinlich völlig unabhängig von der Höhe des Schadens sind, den diese Entitäten anrichten könnten (z. B. Bewegung). Du bist auf dem Weg zum oben erwähnten Gottobjekt.Ein natürlicherer objektorientierter Ansatz besteht darin, den Namen der Klasse anhand ihres Verhaltens zu identifizieren, zum Beispiel:
Bei dieser Art von Design sind Ihren
Player
undMonster
Objekten wahrscheinlich keine Methoden zugeordnet, da diese Objekte die Daten enthalten, die von Ihrer gesamten Anwendung benötigt werden. Es handelt sich wahrscheinlich nur um einfache Datenentitäten, die sich in einem Repository befinden und nur Felder / Eigenschaften enthalten.Dieser Ansatz wird normalerweise als anämisches Domänenmodell bezeichnet , das als Anti-Pattern für Domain-Driven-Design (DDD) angesehen wird. Die SOLID-Prinzipien führen Sie jedoch natürlich zu einer sauberen Trennung zwischen "gemeinsam genutzten" Datenentitäten (möglicherweise in einem Repository). und modulare (vorzugsweise zustandslose) Verhaltensklassen im Objektdiagramm Ihrer Anwendung.
SOLID und DDD sind zwei verschiedene Ansätze für das OO-Design. Während sie sich in vielerlei Hinsicht überschneiden, tendieren sie dazu, in Bezug auf die Klassenidentität und die Trennung von Daten und Verhalten in entgegengesetzte Richtungen zu ziehen.
Zurück zu McConnells obigem Zitat: Das Management von Komplexität ist der Grund, warum Softwareentwicklung eher ein qualifizierter Beruf als eine alltägliche Büroarbeit ist. Bevor McConnell sein Buch schrieb, schrieb Fred Brooks einen Artikel zu diesem Thema, der die Antwort auf Ihre Frage genau zusammenfasst: Es gibt keine Silberkugel für den Umgang mit Komplexität.
Obwohl es keine einzige Antwort gibt, können Sie sich das Leben leichter oder schwerer machen, je nachdem, wie Sie es angehen:
quelle
Ich denke, es geht um die Verwaltung von Komplexität, die immer dann abnimmt, wenn die Dinge um Sie herum zu komplex werden, um sie noch mit dem zu verwalten, was Sie derzeit haben, und wahrscheinlich ein paar Mal auftritt, wenn Sie von einem 1-Mann-Unternehmen zu einem globalen Unternehmen mit einer halben Million Personen heranwachsen .
Im Allgemeinen wächst die Komplexität, wenn Ihr Projekt wächst. Und im Allgemeinen benötigen Sie in dem Moment, in dem die Komplexität zu groß wird (unabhängig davon, ob es sich um die IT oder die Verwaltung des Programms, der Programme oder des gesamten Unternehmens handelt), mehr Tools oder ersetzen Tools durch Tools, die die Komplexität besser handhaben, als es die Menschheit seit Ewigkeiten getan hat. Und Tools gehen Hand in Hand mit komplexeren Prozessen, da jemand etwas ausgeben muss, an dem jemand anderes arbeiten muss. Und diese gehen Hand in Hand mit "zusätzlichen Personen", die bestimmte Rollen haben, z. B. einem Enterprise Architect, der bei einem 1-Personen-Projekt zu Hause nicht benötigt wird.
Gleichzeitig muss Ihre dahinter stehende Taxonomie hinter Ihrer Komplexität wachsen, bis die Taxonomie zu komplex ist und Sie auf unstrukturierte Daten umsteigen. Sie können beispielsweise eine Liste von Websites erstellen und diese kategorisieren. Wenn Sie jedoch eine Liste mit 1 Milliarde Websites benötigen, können Sie dies nicht innerhalb einer angemessenen Zeit tun, sodass Sie zu intelligenteren Algorithmen wechseln oder einfach nur Daten durchsuchen.
In einem kleinen Projekt können Sie an einem freigegebenen Laufwerk arbeiten. Bei einem etwas großen Projekt installieren Sie .git oder .svn oder was auch immer. In einem komplexen Programm mit mehreren Projekten, die alle unterschiedliche Releases haben, die alle live sind und unterschiedliche Release-Daten haben und alle von anderen Systemen abhängig sind, müssen Sie möglicherweise zu Clearcase wechseln, wenn Sie für das Konfigurationsmanagement verantwortlich sind, anstatt nur die Versionierung einige Zweige eines Projekts.
Daher denke ich, dass die beste Anstrengung des Menschen bis jetzt eine Kombination von Werkzeugen, spezifischen Rollen und Prozessen ist, um die zunehmende Komplexität von fast allem zu bewältigen.
Glücklicherweise gibt es Unternehmen, die Geschäftsrahmen, Prozessrahmen, Taxonomien, Datenmodelle usw. verkaufen. Je nach Branche können Sie also kaufen, wenn die Komplexität so groß geworden ist, dass jeder anerkennt, dass es keinen anderen Weg gibt, Daten. Modell und Prozess sofort einsatzbereit, z. B. für ein globales Versicherungsunternehmen. Von dort aus arbeiten Sie sich nach unten, um Ihre vorhandenen Anwendungen zu überarbeiten und alles wieder mit dem allgemein bewährten Framework in Einklang zu bringen, das bereits ein bewährtes Datenmodell und Prozesse enthält.
Wenn es für Ihre Branche keine auf dem Markt gibt, gibt es vielleicht eine Chance :)
quelle
Hören Sie auf, alles OOP zu schreiben, und fügen Sie stattdessen einige Dienste hinzu.
Zum Beispiel habe ich kürzlich eine GOAP für ein Spiel geschrieben. Sie werden die Beispiele im Internet sehen, die ihre Aktionen mit der Spiel-Engine im OOP-Stil verbinden:
In meinem Fall musste ich Aktionen sowohl in der Engine als auch außerhalb der Engine verarbeiten und Aktionen auf weniger detaillierte Weise simulieren, wenn der Spieler nicht anwesend ist. Also habe ich stattdessen verwendet:
Dadurch kann ich die Logik entkoppeln und zwei Klassen haben.
Es ist nicht OOP, aber es bedeutet, dass die Aktionen jetzt keine Verbindung zur Engine haben. Somit ist mein GOAP-Modul völlig vom Rest des Spiels getrennt.
Sobald es fertig ist, benutze ich einfach die DLL und denke nicht an die interne Implementierung.
quelle
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:
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.
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.
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 dieupdate
Funktion des aufgerufen werden. Zu diesem Zeitpunkt wird möglicherweise die zentrale Datenbank nachPosAndVelocity
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 wirdRenderingSystem
möglicherweise die Aktualisierungsfunktion aufgerufen. Zu diesem Zeitpunkt fragt sie die Datenbank nachPosAndVelocity
und nachSprite
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.quelle