octoawesome icon indicating copy to clipboard operation
octoawesome copied to clipboard

Änderung des Komponentensystems: Einführung von Interfaces

Open Gallimathias opened this issue 6 years ago • 22 comments

Wie in PR #249 bereits erwähnt hatt @HierGibtEsDrachen vorgeschlagen das Komponentensystem zu ändern und Interfaces anstelle des bisherigen system zu verwenden.

Am besten erläutert er das mal selber. Ich denke wir können durchaus dieses Thema mal besprechen.

Gallimathias avatar Apr 06 '18 06:04 Gallimathias

Es steht eigentlich soweit hier Docu.pdf drin wohin ich mit dem Komponentensystem bzw. Entitäten möchte.

Im Grunde werden den Entitäten durch Interfaces Eigenschaften gegeben so dass sie gefiltert und über die Komponenten der Entitäten berechnet werden bzw. gezeichnet oder mit dem Player interagieren können. Dadurch könnten unter anderem die Simulationskomponenten wegfallen. Abhängig von der letztendlichen Umsetzung des Multiplayers könnte das auch für Remotekontroller hergenommen werden.

HierGibtEsDrachen avatar Apr 11 '18 14:04 HierGibtEsDrachen

Wir hatten uns eigentlich, als wir die Komponenten eingeführt haben, bewusst gegen interfaces entschieden. Und damit gegen ein system wie z.b. bei Unity.

Interfaces haben den entscheidenden Nachteil das sie nicht Dynamisch verwendet werden können.

Wie hast du dir das mit Mod's gedacht ?

Beispiel ein Mod fügt die Eigentschaft "blau" hinzu. Wie bekommst du das Interface in den Player, was ist deine idee dazu? Und was ist wenn morgen ich den mod nicht mehr will was passiert dann mit der Eigenschaft?

Außerdem wird bei Interfaces immer die Funktionalitäten aus Reflections benötigt wohingegen Objekte auf herkömmliche Weise durchsucht werden können.

Gallimathias avatar Apr 15 '18 13:04 Gallimathias

Geanu das ist das Problem. Mit Interfaces können Extensions nur eigene Entities erstellen und erweitern, aber nur "Eigenschaften" aus anderen Extensions konsumieren. Bereits bestehenden Entities aus anderen Erweiterungen können nachträglich nicht mehr erweitert werden (da die implemntierten Interfaces zur Compilezeit bekannt sein müssen.

Diese Beschränkung besteht bei dem derzeit verwendeten System aus Komponenten und EntityExtendern nicht, dieses ist damit flexibler. Was stört dich denn geanu am bisherigen System, was bei deinem Interface-System besser ist, @HierGibtEsDrachen?

ManuelHu avatar Apr 15 '18 14:04 ManuelHu

Außerdem wird bei Interfaces immer die Funktionalitäten aus Reflections benötigt wohingegen Objekte auf herkömmliche Weise durchsucht werden können.

Versteh ich jetzt nicht ganz ?? Und das derzeitige System benötigt keine Funktionalitäten von Reflection?

Die Interfaces sollen Basisfunktionalitäten bereitstellen und Eigenschaften vermitteln die eine Vereinheitlichung ermöglichen. Bildlich ausgedrückt: Client und Entitäten sollen wie Zahnräder ineinander greifen. An dieser Stelle muss aber noch gesagt werden das Beliebige Erweiterungen von fremden Entitäten nur bedingt wünschenswert sind, wenn Wauzi anfängt mit mir zu Handeln suche ich die nächste Telefonzelle^^ Außerdem sollte grundsätzlich zwischen den Arten der Erweiterung unterschieden werden bzw. Redundanzen vermieden werden. Doppelte Physik macht einfach keinen Sinn. Die Physik könnte als globale Erweiterung betrachtet werden (anderes Universum andere Physik). Ebenso ist eine zu feine Granulierung nicht wünschenswert -- Head-, Body-, FootComponent ? wie wäre es mit ButtComponent?

Hier muss noch geklärt werden wie die Komponenten selbst organisiert werden. Ein Modder will eventuell sein eigenes Inventar für den Spieler implementieren mit eigener UI weil im der Standart nicht gefällt. Das würde ich dann auch über Interfaces oder Basisklassen machen. Allein schon weil der IGameService die Kenntnisse benötigt, wenn er Methoden bereitstellen will wie z.B. TakeBlock oder ähnliches.

Entitäten können: -Interagieren -Gezeichnet werden (Rendern) -Kontrolliert werden

Die Interaktion Es ist nicht das Ziel die Interaktion der Entität an sich mit dem Player zu definieren sondern die Interaktion einzuleiten. Die Interaktion kann eine Auswirkung auf das UserInterface haben zum Beispiel einen Dialog (Händler) oder auch nicht (Hebel) und genau diesen zugriff bzw. Auswirkung soll das Interface regeln. Der NPC könnte auch beschäftigt sein weil er mit einem anderen Spieler interagiert.

Das Rendern Dazu muss die EntityComponenten (Client, Renderer) wissen welche Entitäten überhaupt gezeichnet werden sollen (ein GM will eventuell mit einem Avatar herumlaufe der unsichtbar ist) über ein entsprechendes Interfaces können somit die Entitäten gefiltert werden und über eine Methode die den gegepart IGraphicsDevice (oder so) annimmt vom Modder beliebig gezeichnet werden. Das geschieht momentan über die RenderComponent die OctoAwesome.dll ist. Wenn die gegenwertige umsetzung betrachtet wird, wird alles fein säuberlich in entsprechende Komponenten zerlegt, sodass bei jeder Iteration die Komponenten (RenderComponent, BodyComponent, PositionComponent) immer wieder rausgesucht werden müssen (und das ist nicht nur hier so). Meiner Meinung nach sollte eben die EntityComponent (Client, Renderer) diese Komponenten gar nicht kennen da diese in der Extension selbst liegen sollten. Außerdem kann das Interface bzw. in der Methode in den Komponenten gesucht werden ob RenderComponenten (Basisklassen) vorhanden sind und alle oder nur eine Zeichnen, dass liegt dann in der Verantwortung des Modders.

Die Bewegung (Kontrollierbarkeit) Ich hab mal das Argument gehört der unterschied zwischen Wauzi und dem Player ist die HeadComponent. Ich muss aber sagen das diese dem Wauzi nicht schadet^^ Und auch hier mein allgemeiner Ansatz über den EntityController (siehe Docu), es werden alle möglichen Eingaben übergeben diese müssen allerdings nicht benutzt werden. Und es ist nicht überladen sowie Lassi gesagt hat es ist nur vollständig (bzw. noch nicht ganz)! Hier kann unterschieden werden zwischen Player, Wauzi oder einem Zelt bzw. Blume, denn letzteres benötigt keine Berechnung der Bewegung. KI´s von Entitäten werden einfach als Kontroller Programmiert und gesetzt. Ich will nicht das mein Zelt bewegbar wird :) bzw. wegläuft.

HierGibtEsDrachen avatar Apr 15 '18 18:04 HierGibtEsDrachen

solche Dinge würde ich trotzdem ungern mit Interfaces verallgemeinern, sondern auch mit entsprechenden Flags oder gar FlagComponenten, denn mit Interfaces verbaut man sich die Inlinebarkeit noch schlimmer als bei C++(welches immerhin zur Compilezeit evtl. erkennt ob es immer dasselbe Objekt ist und dann inlinen kann), C# inlined Dinge aus einem Interface Grundsätzlich gar nicht.

jvbsl avatar Apr 17 '18 20:04 jvbsl

Also, hier muss ich julian wieder recht geben, zum einen was Julian sagt zum anderen

Das Komponentensystem bedarf ein Redesign bzw. ein neues Konzept überarbeitung. Und Interfaces will man ja nicht Pauschal verbieten aber ich weis nicht ob wir hier nicht gerade aneinander vorbeireden.

Der Vorteil an den Komponenten ist aktuell die einfachheit und Flexibilität. Ich mache dir einfach mal ein Beispiel:

Wir haben 3 Mods Mod A fügt ein Schwert hinzu Mod B ergänzt das Schwert um einen Flammen effekt Mod C erweitert den Flammeneffekt um Mana

Bei einem Komponenten System wie es aktuell ansätze hatt würde jeder Mod einfach eine Komponente zu einer grund Entität hinzufügen. Bei einem Update kann man einfach einmal überalle Komponenten gehen. Bzw. klassische vergleichsoperationen verwenden um eine bestimmte Komponente zu suchen.

Möchte ich einen Mod nicht mehr in dem Fall B dann wird die Komponente einfach nicht mehr hinzugefügt die Sache ist durch.

Weiterer Vorteil, wir können z.b. Komponenten nach reihenfolge sortieren vor der Ausführung so das eine Herachie entsteht. Und wie julian als Idee einbrachte liese sich jede komponente mit einer uniqe ID versehen auf die z.b. referenziert werden könnte in dem man in einem Objekt nur die ID hält.

Im falle von Interfaces sehen die oben genannten Funktionalitäten schwieriger aus, ich sage nicht unmöglich. Sicher bequemer zu Programmieren sind Interfaces, flexiebler aber Komponenten.

Gallimathias avatar Apr 19 '18 08:04 Gallimathias

Ahja meine mal gelesen zu haben das Aufrufe durch Interfaces immer (oder meistens) langsamer sind als virtual Calls. Ansonsten gebe ich dir vollkommen Recht mit deiner Aussage.

Dein Beispiel steht mit dem was ich machen will nicht in Konflikt. :) Die Sache ist, woher weiß Mod C das Mod B ein Flammeneffekt ist und Mod B das Mod A ein Schwert ist? Ok jetzt könnte einfach Mod C den Mod B und Mod A (falls A nötig ist) und Mod B den Mod A referenzieren, kann gemacht werden, kein Thema. Aber der Client bzw. der Server kann das nicht ohne weiteres und dennoch muss er den Effekt rendern. Dann sucht der Client mal eben nach ???? ? und genau an der Stelle ???? kommen meine Interfaces oder Basis Klassen ins "Spiel" (das ist mir Latte) :D wobei ich Interfaces bevorzugen würde wegen https://stackoverflow.com/questions/49002/prefer-composition-over-inheritance

Dein Beispiel ist nach meiner Meinung eine legetime Verwendung weil es tatsächlich eine Erweiterung ist, dennoch bin ich der Meinung das zu hohe Granularität (siehe ButtComponent) nicht immer hilfreich ist!

Und damit alles nicht von jedem Modder 1000000 mal selber geschrieben werden muss kommen noch die IGameService´s hinzu die Standartimplementierungen anbieten etc...

Ich will die Komponenten nicht abschaffen, Ich will es verbessern :P

Bei meinem dritten PR gab es die Komponenten ja auch noch, halt nur anders :)

HierGibtEsDrachen avatar Apr 19 '18 13:04 HierGibtEsDrachen

With composition, it's easy to change behavior on the fly with Dependency Injection / Setters. Inheritance is more rigid as most languages do not allow you to derive from more than one type.

das wollen wir aber

und virtual calls sind auch langsam können auch nicht inlined werden, das ist genau dasselbe. Deshalb weg von dem StandardOOP zeugs

verändern würde ich das ganze System später sowieso,

Was deine IGameService anbelangt kann ich noch nicht viel sagen ist noch in zu kleinem Rahmen, aber dagegen wehr ich mich erstmal nicht. Ansonsten glaub ich fast, dass die meisten immer noch nicht ganz verstehen(mich eingeschlossen) wo du denn jetzt überall deine Interfaces einbringen willst. Deshalb wäre mal ne mündliche Erklärung mit Bild(TeamViewer+TeamSpeak) denke ich mal ziemlich sinnvoll

jvbsl avatar Apr 19 '18 20:04 jvbsl

der satz mit den 4 fragezeichen in meinem vorherigen beitrag sollte das glaub ich klar machen :) versteh echt nicht wie ihr das sonst machen wollt über reflection ?

HierGibtEsDrachen avatar Apr 19 '18 21:04 HierGibtEsDrachen

Die Mods müssen sich in diesem Fall sowieso gegenseitig referenzieren. Sonst müsste in der OctoAwesonme.dll ein Interface für jedes erdenkliche Werkzeug befinden. Dann kommt ein Modder und will einen SuperDuperSpaceBagger2000 integrieren und es gibt keine Interface dafür, nach dem andere Mods filtern können. Abhängigkeiten sind ja geplant (#207)

Mod A: Klasse Schwert, implementiert IEntity und wird dem Spiel registriert. Registriert auch noch gleich eine RenderComponent mit. Mod B: Refereziert A, fügt eine EffectComponent hinzu (bzw. eine Komponente, die davon erbt. Alle EffectComponets müssen wie RenderComponents im Client in den Zeichenprozess integriert werden) Mod C: vergleichbar

Geht also auch mit Komponenten. Natürlich haben die Komponenten auch Basisklassen bzw. Interfaces, die der Client kennen muss (sonst geht echts nichts).

ManuelHu avatar Apr 22 '18 08:04 ManuelHu

aber eine Component kann auch von Mod XYZ kommen und von Mod B hinzugefügt werden um dann von Mod C manipuliert/verwendet zu werden, ohne dass Mod C etwas von Mod B weiß, also nicht unbedingt eine Referenz. Basis Interface wäre mMn außerdem ein leeres Interface, bzw. ein Interface mit nur Dingen, die sehr wenig aufgerufen werden... Und ja das sollte dann trotzdem möglich sein(Laufzeitcodegenerierung für die paar Sachen, natürlich direkt beim laden - wobei natürlich alles nur in Theorie bei mir im Kopf besteht^^)

jvbsl avatar Apr 22 '18 10:04 jvbsl

@ManuelHu genau davon rede ich (hab nie gesagt ich will eure Komponenten Entfernern siehe weiter oben fett gedruckt :P)

Ich kann einfach nicht verstehen wie die Aussage:

Die Interfaces sollen Basisfunktionalitäten bereitstellen und Eigenschaften vermitteln die eine Vereinheitlichung ermöglichen.

Aber der Client bzw. der Server kann das nicht ohne weiteres und dennoch muss er den Effekt rendern. Dann sucht der Client mal eben nach ???? ? und genau an der Stelle ???? kommen meine Interfaces oder Basis Klassen ins "Spiel"

so falsch verstanden werden kann :(

Dann kommt ein Modder und will einen SuperDuperSpaceBagger2000 integrieren und es gibt keine Interface dafür, nach dem andere Mods filtern können.

Und die Finale Frage: Ist das, dass du beschrieben hast @ManuelHu (zweiter Absatz) gegenwärtig (vernünftig) umgesetzt?

Und das Referenzieren von Mods muss NICHT implementiert werden die Reference im Projekt schafft der Modder auch alleine. Es wäre eine Sache wenn wir sagen würde: Die Extension Assemblies dürfen NICHT referenziert werden. Dann müsste etwas aktive getan werden!

code zur laufzeit generieren und compilieren ? habe ich keine erfahrung mit aber noch generischer geht es glaub ich wirklich nicht mehr. vllt. mit einer AI die den Code für einen schreibt ^^ https://stackoverflow.com/questions/826398/is-it-possible-to-dynamically-compile-and-execute-c-sharp-code-fragments

HierGibtEsDrachen avatar Apr 22 '18 17:04 HierGibtEsDrachen

Also um genauzu sein hätte ich gerne etwas, was viel mehr eine Code Injection macht vmtl. mittels Mono.Cecil, wodurch wir dann eben verhindern können dass er über den interface weg gehen muss und somit dafür sorgen, dass das ganze mit bestmöglicher Performance funktioniert.

Basisfunktionalität, was braucht eine Komponente für eine Funktionalität, die für alle gilt, oder eine Entität? Und wenn dann wie gesagt bitte ohne Interfaces, ich bin vlt. aber am Überlegen vlt. doch Interfaces zu nehmen und das Mono.Cecil die Interfaces dann entfernt, so dass man noch den Programmierkomfort von Interfaces hat?!^^

Und das Filtern geht eben nicht nach Interfaces, sondern nach Komponenten. Du suchst nach Komponenten nicht die eine Entität hat oder eben auch nicht und nicht nach Interfaces?

jvbsl avatar Apr 22 '18 18:04 jvbsl

Woher weiß der Client den das Komponente xyz eine RenderKomponenten ist. Und wenn es halt keine gibt oder die Entity das Interface nicht implementiert (kann auch bei der Entity direkt implementiert werden braucht man keine Komponenten fürs Rendern, höchstens als Statespace) dann wird sie halt nicht gerendert. Oder woher weiß die Simulation das sich die Entität bewegen kann? Oder das mit der Entität interagiert werden kann? Oder das die FlagKomponent eine Flags hat die Informationen bereitstellt? usw. usw. usw.

foreach(EntityComponent comp in Components)
 {
      Type comptype = comp,GetType();
      foreach(MethodInfo method in comptype.GetMethods())
           if(method.Name == "Render")
               mothod.Invoke(comp, GraphicsDevice);
}

(oder so kann das zeug nicht auswendig :P) Reflection will do. Und meine Vorschlag ist:

Type rendtype = typeof(IRendable);
IEnumerable<IRendable > rendable = entities.Select(e => 
    e.Components.Get(rendtype)).Where(c => c  != null);

foreach(IRendable rend in rendable)
   rend.Render(GraphicsDevice);

Und wenn ihr das so nicht haben wollt, dann sagt einfach die Idee ist Mist und damit hat es sich :P

Gegen deinen Vorschlag hab ich nichts einzuwenden aber dann könnte auch hergegangen werden und die Simulation dynamisch bevor der Server startet zugeschnitten und optimiert aus den Extensions (die für den Server gelten und für den Server vorhanden sein müssen und zwar bis ein Admin das gegenteil sagt und den Server neu startet) generiert werden. Es werden quasi Entity Klassen gebaut die alle Methoden und Properties der Componenten inne haben. Eine Anpassung der Populator usw. ist dann eben auch erforderlich.

Dann schiebt der Server anstelle von X Assemblies eine Assembly über die Leitung zum Client. An der stelle kann auch überlegt werden ob es GraphicExtensions geben kann soll die der Server nicht kennen muss um eben den Look des Spiels lokal aufzupimpen. Dann läuft die Generierung wieder Lokal und die SymulationComponente im Client ist dann auch mehr als ein Wrapper für Simulation. :D

HierGibtEsDrachen avatar Apr 22 '18 19:04 HierGibtEsDrachen

Beim starten werden erstmal aus allen Assemblies alle Komponententypen zusammengesucht, das kann entweder über ein Attribut passieren oder von mir aus auch über ein leeres IComponent interface(das nur zum erkennen der Komponenten ist)...

Wenn man dann alle hat, dann sortiert man diese anhand einer GUID z.b. sodass bei Server und Client diesselben IDs herauskommen(Client-Only-Components werden ganz ans ende sortiert).

Jede Entity bekommt dann ein Array IComponent[] Components; evtl. dann auch über object muss ich gucken ob man mit TypedReference mehr Perf herausbekommt.

class ComponentIdFromType<T> where T : IComponent{
{
  public static int ComponentIndex = -1;//entweder später über Mono.Cecil und dann sogar als const, oder einmal beim Start
}
ComponentIdFromType<PositionComponent>.ComponentIndex = index++;//dieser Code wird natürlich automatisiert erzeugt, bzw über Reflection - kein Problem, da nur beim Starten ausgeführt.
ComponentIdFromType<RenderComponent>.ComponentIndex = index++;

//Component nach typ bekommen ist dann einfach:
Entity.Components[ComponentIdFromType<PositionComponent>.ComponentIndex]....;

wenn ne Component null ist, dann ist die natürlich einfach nicht teil dieser Entität. Man hat zwar etwas speicher overhead, aber das ist nicht allzu viel, jede Component ist einfach nur ne Referenz. Perf ist da wichtiger...

Wer Rendern will muss auf eine Assembly verweisen die Rendern kann, und diese hat dann eine RenderComponent, davon kann es dann natürlich entsprechend unterschiedliche Komponenten für unterschiedliche Render arten geben...

Wichtig ist dabei die Components kommen aus einem Pool, und dabei gäbe es zwei Ideen: Pools enthalten jeweils jeden Komponententypen einmal, falls für jede Entität oft die unterschiedlichen Komponenten direkt nacheinander aufgerufen werden(Caching ausnutzung). Oder was ich denke ich für sinnvoller halte, gepoolt werden alle Componenten von selben Typ direkt im Memory hintereinander besser für caching, wenn Componenten eines typs oft hintereinander aufgerufen werden. Auch kann man dies ausnutzen um eben alle RenderComponents durchzugehen ohne dafür eine Auflistung zu durchsuchen, bzw. alle Entitäten zu durchlaufen...

jvbsl avatar Apr 22 '18 21:04 jvbsl

Und das Referenzieren von Mods muss NICHT implementiert werden die Reference im Projekt schafft der Modder auch alleine. Es wäre eine Sache wenn wir sagen würde: Die Extension Assemblies dürfen NICHT referenziert werden. Dann müsste etwas aktive getan werden!

Klar, das stimmt schon. Wenn aber die referenzierte Assembly fehlt gibts ne Exception zur Laufzweit, wenn zum ersten Mal ein Typ daraus verwendet wird. Wenn man zusätzlich deklariert (z.B. via Attribute an der Extension-Klasse), was für Extensions referenziert werden, kann man solche Extensions gleich beim Laden ausschließen.

ManuelHu avatar Apr 23 '18 19:04 ManuelHu

Ich persönlich könnte mir auch noch Vorstellen einfach gewisse dinge den Komponenten selbst zu überlassen. So dass einfach bei einem Update jede komponente zyklisch aufgerufen wird.

Für den Build prozess den jubsel vorschlägt muss man aber nicht auf Interfaces setzen da wären Attributes eigentlich bequemer von der Programmierung her. Bei der Variante die mir im Kopfschwebte wäre die Mod referenzierung auch weich gewesen.

@jvbsl hast du so ein Komponentensystem schon mal ausprobiert?

Gallimathias avatar Apr 24 '18 04:04 Gallimathias

@ManuelHu sorry wenns ein bisschen hart war. Mit jvbls Ansatz würde das spätestens vor dem Spielstart geschehen. die Exception kann abgefangen werden und es gibt dann ein popup Assembly XYZ wurde nicht gefunden, dass Spiel kann nicht gestartet werden. Da wir gerade beim Thema sind würde den derzeitigen Algorithmus bisschen ändern und über eine zwischen Domain gehen und ein Assembly - OctoExtension - Attribute einführen. @Gallimathias jo UpdatableComponent :D 4 basis klassen ^^ edit: wenn wir seine idee umsetzen muss ohnehin umgedacht werden

HierGibtEsDrachen avatar Apr 24 '18 04:04 HierGibtEsDrachen

Ich habs noch nicht umgesetzt, aber die Grundidee ist von sebastian gestohlen mit nen paar Änderungen eben dann. Und bei Sebastian hats ziemlich gut funktioniert...

Großartig umdenken muss man eigentlich denke nicht, oder überseh ich da etwas?

jvbsl avatar Apr 24 '18 23:04 jvbsl

warum du das wollte ja ich machen ^^ doch umdenken muss man da schon ein bisschen, weil dann auch die IExtension interfaces und so umgeschrieben werden müssen.

hätte noch eine frage: ist zwar offtopic aber hier geht es ja auch darum etwas zu lernen :) kann festgestellt werden ohne den original code zu kennen ob eine code injection stattgefunden hat?

HierGibtEsDrachen avatar Apr 25 '18 05:04 HierGibtEsDrachen

bevor ich es vergessen, hier noch ein kleiner nachtrag @ManuelHu https://msdn.microsoft.com/de-de/library/system.reflection.assembly.getreferencedassemblies(v=vs.110).aspx

HierGibtEsDrachen avatar Jun 07 '18 20:06 HierGibtEsDrachen

Ja ja klar, die gibts. Aber da eine Assembly theoretisch mehrere Extension-Klassen beinhalten kann und diese einzeln aktiviert/daktiviert werden können, kann es auch zu SItuationen kommen, dass eine Extension die Funktionalität einer anderen Extensions braucht, deren Assembly zwar existiert und geladen ist, aber die selbst nicht instanziert ist.

ManuelHu avatar Jun 08 '18 17:06 ManuelHu