Verschiedene Body-Klassen (Physik) - Mit Policies oder so?



  • Hallo zusammen,

    ich benutze eine Physics engine und schreibe mir dazu ein paar Wrapper um die Benutzung zu vereinfachen.
    Ich benötige allerdings eine relativ hohe Anzahl an verschiedenen bodies mit jeweils verschiedenen Kombinationen aus ein paar Merkmalen, die ich gleich nenne. Aber trotzdem will ich wenn möglich für jede Kombination eine eigene Klasse haben, da ich jede Kombination recht häudig anwenden will.
    Allerdings finde ich es blöd, wenn ich für jede Kombination eine eigene Klasse schreibe, weil das zu ziemlicher Code-Duplikation führen würde.
    Bei C++ gibt es die Möglichkeit templates als Policies zu benutzen. Bei C# könnte ich mir das wenn dann mithilfe von Interfaces vorstellen, bin mir aber nicht sicher, wie gut das funktionieren würde.

    Hier sind die Merkmale, die ich brauche (von jedem Merkmal gibt es zwei Ausprägungen)
    - Mit oder ohne Beschleunigung (ohne heißt man beschleunigt sofort auf max speed)
    - Mit oder ohne Fähigkeit zum springen
    - Mit oder ohne einem "Speed-Modus" (anweungsfall: wenn man strg drück, soll sich der max speed erhöhen)
    - evtl noch mehr

    Jetzt könnte man sagen - einfach immer die Klasse mit allem nehmen, aber der Nachteil ist das beim Serialisieren dann einiges überflüssiges geschrieben wird und ich muss da sparsam sein, weil das übers internet geht und die bandbreite da sehr knapp ist.
    Wie würdet ihr das machen?

    Vielen Dank schonmal für eure Hilfe!



  • Die Frage ist schon ein paar Tage alt aber niemand hat geantwortet und mir ist sie auch nicht aufgefallen. Deshalb...

    Q schrieb:

    Ich benötige allerdings eine relativ hohe Anzahl an verschiedenen bodies mit jeweils verschiedenen Kombinationen aus ein paar Merkmalen

    Allerdings finde ich es blöd, wenn ich für jede Kombination eine eigene Klasse schreibe, weil das zu ziemlicher Code-Duplikation führen würde.

    Das schreit nach dem Strategy-Pattern.

    Q schrieb:

    - Mit oder ohne Beschleunigung (ohne heißt man beschleunigt sofort auf max speed)

    Passt: Beliebige Strategien für jeden Fall einer Beschleunigung, auch den Sprung auf max-speed.

    Q schrieb:

    - Mit oder ohne ...

    Passt ebenfalls. Es tut nicht sehr weh, "leere" Strategien an eine Klasse zu übergeben.
    Die Alternative mit Vererbung und Überschreibung birgt viel mehr Gefahr für unsaubere Auswüchse.

    Komposition, Dependency-Injection und das Strategy-Pattern sind bei diesem Problem in C# imo am sinnvollsten.

    Q schrieb:

    Jetzt könnte man sagen - einfach immer die Klasse mit allem nehmen, aber der Nachteil ist das beim Serialisieren dann einiges überflüssiges geschrieben wird und ich muss da sparsam sein, weil das übers internet geht und die bandbreite da sehr knapp ist.

    Der Zustand wird also in einem Multiplayer-Spiel über das Internet übertragen?
    Ich vermute mal mit .NET-Remoting?!



  • Ich habe gerade gesheen, dass es hier ja doch eine Antwort gab 🙂

    Ja, das mit dem strategy-pattern ist eine gute Idee, im Prinzip ist es auch das gleiche wie mit policies, nur das ich das mit policies eher statisch (mit templates) kenne.

    Soll ich dann eine allgemeine Klasse machen, die im Konstruktor ein paar policies übergeben bekommt? Dann könnte ich falls ich noch mehr ändern muss von der klasse erben und ansonsten statische methoden zum konstruieren verwenden.

    Ein paar sachen stören mich dabei allerdings noch. falls ich dann etwas spezielles von der konkreten policy benutzen will, dann muss ich erst downcasten, obwohl ich eigentlich weiß, um was für einen typ es sich handelt.
    Ich weiß nicht in wieweit C# generics mir da helfen können.
    Geht sowas wie

    public class Body<AccelerationPolicy, JumpPolicy>
    {
       protected AccelerationPolicy acc; //statt protected würde auch ne property gehen mit protected getter gehen
       protected JumpPolicy jmp;
       public Body(AccelerationPolicy acc, JumpPolicy jump)
       {
          this.acc = acc;
          this.jump = jump;
       }
    }
    

    Also das die Typinformationen erhalten blieben und keine referenzen auf basisklassen gespeichert werden.

    µ schrieb:

    Der Zustand wird also in einem Multiplayer-Spiel über das Internet übertragen?
    Ich vermute mal mit .NET-Remoting?!

    Ja, es ist ein Multiplayerspiel und es wird übers internet übertragen. Ich verwende dafür die lidgren netzwerk bibliothek.

    Die serialisierung mache ich "manuell", also ich schreibe alles was ich wirklich brauche selbst in ein paket rein und benutze keine automatischen serialisierungssachen von .NET
    .Net remoting kenn ich gar nicht.



  • Hi Q

    Q schrieb:

    Soll ich dann eine allgemeine Klasse machen, die im Konstruktor ein paar policies übergeben bekommt? Dann könnte ich falls ich noch mehr ändern muss von der klasse erben und ansonsten statische methoden zum konstruieren verwenden.

    Der Konstruktor (Nennen wir die Klasse mit diesem Konstruktor: Workerclass) nimmt AccelerationStrategy (AccelerationPolicy) entgegen. AccelerationStrategy ist ein interface oder eine abstrakte Basisklasse. Alle verschiedenen Arten von Beschleunigungen stellen verschiedene Klassen dar, die jeweils das interface / die Basisklasse implementieren. Damit ist es kein Problem Code wiederzuverwenden und beliebige Kombinationen zu realisieren.
    Das Strategy-Pattern ist sehr sehr nützlich. Du schreibst "Dann könnte ich falls ich noch mehr ändern muss von der klasse erben". Falls Du damit das, was ich Workerclass nenne, meinst, geht es in Richtung Bridge-Pattern.

    Die Konstruktion sollte auf keinen Fall auf direkte Art innerhalb der WorkerClass stattfinden. Warum auch, wir haben ja schon Dependeny-Injection mit dem obigen Ansatz. Falls die WorkerClass ihrer Helferlein DOCH konstruieren muss, gibt es kluge Lösungen wie Factory-Method oder Abstract-Factory. Deine statische Methode zur Konstruktion nennt man "einfache Fabrikmethode". (Viele Stichwörter zum nachlesen)

    Q schrieb:

    Ein paar sachen stören mich dabei allerdings noch. falls ich dann etwas spezielles von der konkreten policy benutzen will, dann muss ich erst downcasten, obwohl ich eigentlich weiß, um was für einen typ es sich handelt.
    Ich weiß nicht in wieweit C# generics mir da helfen können.

    Generics sind keine Templates. Ein Template wird statisch instanziiert in Abhängigkeit seiner Parameter. Der Compiler schaut dabei ob die Typen hinter den Templateparametern alle Operationen, Methoden, etc. anbieten, die im Code tatsächlich verwendet werden und winkt entsprechend durch.
    Generics sind leider sehr viel restriktiver. Hierbei muss jeder Typ erlaubt sein. D.h. mit einem gewöhnlichen Generic-Parameter kannst Du in der Klasse nicht mehr anstellen als mit einem object weil das nunmal die gemeinsame Basis aller Typen ist.
    ´
    Es gibt Möglichkeiten die Generic-Parameter einzuschränken (Constraining Type Parameters):

    class Body<AccelerationPolicy> where AccelerationPolicy : myInterfaceName
    {
    }
    

    Das würde z.B. ausdrücken, dass jedes AccelerationPolicy das interface myInterfaceName implementieren muss. Weitere Möglichkeiten sind:

    1. where T : myClassName	// T muss myClassName oder davon abgeleitet sein
    2. where T : new()	// T muss Defaultkonstruktor anbieten
    3. where T : class	// T ist Referenztyp
    4. where T : struct	// T ist Wertetyp
    

    Damit kann man viel machen, aber was Dein Problem betrifft, wirst Du Dich solange im Kreis drehen, bis Du wieder an dem Punkt bist wo ein downcast notwendig wird. (Falls jemand doch eine Lösung kennt, her damit. Mir ist keine bekannt)

    Die Philosophie in C# ist ein wenig anders. Dein Argument "dann muss ich erst downcasten, obwohl ich eigentlich weiß, um was für einen typ es sich handelt" würde man i.d.R. so beantworten: Wenn Du den konkreten Typen kennst und benötigst, dann arbeite auch mit dem konkreten Typen. Oder: Erweitere die Abstraktion (interface oder Basisklasse) und damit auch alle Subtypen.

    Downcasting ist jedenfalls keine saubere Lösung. If-else-Kaskaden auch nicht. Letzteres lässt sich unter Umständen aber durch einen Double-Dispatch-Mechanismus wie im Visitor-Pattern umgehen. Evtl. solltest Du Dir das mal näher anschauen.

    Q schrieb:

    Die serialisierung mache ich "manuell", also ich schreibe alles was ich wirklich brauche selbst in ein paket rein und benutze keine automatischen serialisierungssachen von .NET

    Ein Blick wären Remoting und die hauseigene Serialisierung wert.



  • Hi µ, danke für deine Hilfe!

    Ich glaube du hast mich teilweise etwas missverstanden. Ich weiß wie das Strategiemuster funktioniert, ich bezog mich mit basisklasse und ableiten davon nicht auf die policies/strategien selbst, sondern auf die Body-Klassen, die die policies verwenden.

    Im moment sieht es so aus:

    Body ist die abstrakte basisklasse aller bodies
    davon leitet die konkrete klasse StaticBody ab und die abstrakte Klasse DynamicBody.

    DynamicBody hat zwei abstrakte properties:

    protected abstract JumpPolicy JumpPolicy {get; }
    protected abstract AccelerationPolicy AccelerationPolicy {get; }
    

    , über die es auf die policies zugreift, die von abgeleiteten klassen überschrieben werden müssen.

    Ich hatte dort vorher einen konstruktor, der 2 policies übergeben bekommt und diese dann in protected referenzen auf die basisklassen der policies abgespeichert hatte, aber das aufrufen des basisklassenkonstruktors wurde dadurch sehr abenteuerlich und die Typinformationen gingen verloren (das war das was ich mit downcast angesprochen hatte).
    Das sah dann ungefähr so aus:

    protected JumpPolicy jumpPolicy;
    protected AccelerationPolicy accelerationPolicy;
    protected FarseerBody body;
    public DynamicBody(FarseerBody body, JumpPolicy jumpPolicy, AccelerationPolicy accelPolicy)
    {  
       this.body = body;
       this.jumpPolicy = jumpPolicy;
       this. accelerationPolicy = accelPolicy;
    }
    

    Das mit den abenteuerlichen Konstruktoren lag daran, dass ich erst einen FarseerBody (body von der physicsengine) erstellen musste, dann 2 policies, die auch eine referenz auf den farseerBody brauchen und dann alle 3 sachen dem basisklassenkonstruktor übergeben werden musste.
    Ich hatte einige private konstruktoren gebraucht über die ich umleiten konnte um überhaupt die argumente für die basisklasse zu initialisieren.
    Deswegen habe ich es jetzt über abstrakte properties gelöst.

    Das mit dem Downcast hat sich damit auch erledigt.

    Vorher war es so, dass die basisklasse die policies abspeichert (mit referenz auf basisklassen der policies). Jetzt speichert die abgeleitete klasse selbst seine policies (mit referenz auf konkreten typ) und überschreibt die abstrakten properties, um der basisklasse zugriff zu gewähren.

    Bei der Lösung bleibt es allerdings dabei, dass ich für jeden Möglichen body eine neue Klasse ableiten muss um die abstrakten properties zu überschreiben.
    z.B. brauche ich dann LinearAcceleratedJumpingBody und viele weitere ausprägungen.
    Allerdings finde ich das nicht besonders schlimm, da diese klassen sehr schnell geschrieben sind und ich dann dort auch noch weitere anpassungen vornehmen kann.

    Die Beschränktheit der Generics ist sehr schade, das ist fast genauso wie bei java... Templates sind da wirklich wesentlich mächtiger.

    Ich kenne .Net Remoting zwar nicht, aber ich habe meine Zweifel, dass es mir was hilft. Erstens wird das vermutlich einen ziemlichen overhead haben (für metainformationen usw. und ich kann dann vermutlich nicht mehr so selektiv serialisieren wie bisher) und außerdem wird vermutlich alles über tcp gehen. Mit Lidgren bin ich da meiner meinung nach wesentlich besser bedient, das ist extra auf spiele ausgelegt.


Anmelden zum Antworten