Design-Frage: Zugriff verwaltender Klasse auf verwaltetes Objekt
-
@ Progchild:
Ist zwar Off-Topic, aber du hast ein Schreibfehler in deinem Programm.
Dort steht etwas von "Receave". Es heißt aber "receive"
Siehe:
http://www.progchild.de/software/netwalker/images/shot_01.jpg
http://www.progchild.de/software/netwalker/images/shot_02.jpg
-
Blubbb schrieb:
Ist zwar Off-Topic, aber du hast ein Schreibfehler in deinem Programm.
Dort steht etwas von "Receave". Es heißt aber "receive"
Ich weiß. Ich hab das auch schon seit Jahren geändert. Nur erstelle ich für sowas kein neues Build. Es liegt hier also weiterhin korrigiert auf meiner Festplatte. Wenn es dich stört, kannst du dir ja gerne den Quelltext runter laden und es ändern.
Btw. es heißt einen Schreibfehler nicht ein.
-
Darf ich fragen, wieso du direkt auf den Variablen des Modells rumrechnen möchtest? Das leuchtet mir nicht so ein.
Schon nur die Tatsache, falls in den Berechnungen ein Fehler passiert, der Fehler in den temporären Variablen haften bleibt und nicht im Objekt selbst, erscheint mir von Vorteil
Grundsätzlich kenne ich es so, dass man den Status eines Modells liest und kopiert, den Status neu berechnet und ihn wieder ins Modell zurückschreibt. Dem zuständigen Controller wird entweder automatisch oder manuell mitgeteilt, dass sich sein Objekt oder eines seiner Objekte verändert hat.
Falls du oft mehrere Daten zugleich brauchst, kannst du auch erweiterte Getter schreiben:
class Foo { private: int m_a; int m_b; int m_c; public: void get_abc(int& a, int& b, int& c) { a = m_a; b = m_b; c = m_c; } };Man könnte es sogar mit Zeiger erledigen, was einem die Möglichkeit gibt, nur einzelne Werte zu lesen:
void get_abc(int* a, int* b, int* c) { if(a) { *a = m_a; } if(b) { *b = m_b; } if(c) { *c = m_c; } }Gleiches gilt dann natürlich auch für den Setter. Es muss ja schliesslich nicht immer nur eine Variable gesetzt werden über einen Setter

Die Frage sei aber auch in den Raum gestellt, ob du richtig gekapselt hast

Grundsätzlich gefällt mir schon nur der Name von PhysicManager nicht. Erscheint mir irgendwie zu zentral.
Wenn ich nur zum Beispiel an eines meiner aktuelle Projekte denke, welches zwar derzeit etwas nebenher lahmt, da andere Projekte eine höhere Priorität haben, so habe ich dort folgende Klassen:
AreaMapping -> Aufteilung der Spielfläche in Sektionen.
CollisionDetection -> Die Objekte in gleichen Sektionen werden untereinander geprüft.
CollisionHandler -> Behandlen einer allfälligen Kollision.
GravitySystem -> Gravitation auf die Objekte anwenden.
usw.Viele davon sind teilweise nur Basisklassen, oft sogar abstrakt. Ableitungen geben dann spezielle Eigenschaften.
Ohne Getter und Setter in den Modellen der Objekte, gäbe es da gar keine Möglichkeit. Ich kann ja nicht all diese Klassen zufrienddeklarieren
Nexus schrieb:
Naheliegend sind Getter und Setter, allerdings müsste ich relativ viele Methoden schreiben.
Diese Aussage verwundert mich allerdings auch noch ein wenig. Von wievielen Methoden reden wir hier? Und suchst du eine andere Lösung, nur um deine Schreibfaulheit zu umgehen?

Wieviele Eigenschaften stecken bei dir denn in einem Object? Womöglich zuviele?Grüssli
-
Dravere schrieb:
Darf ich fragen, wieso du direkt auf den Variablen des Modells rumrechnen möchtest? Das leuchtet mir nicht so ein.
Das ist nur gerade wie es aktuell aussieht, ich frage ja nach einer besseren Lösung.
Dravere schrieb:
Schon nur die Tatsache, falls in den Berechnungen ein Fehler passiert, der Fehler in den temporären Variablen haften bleibt und nicht im Objekt selbst, erscheint mir von Vorteil

In den Berechnungen passieren aber keine Fehler. Falls es doch der Fall sein sollte (Bugs gibts immer, ich hab noch nicht gross getestet), werde ich die kaum zur Laufzeit abfragen, sondern gleich im Quellcode beheben.

Dravere schrieb:
Die Frage sei aber auch in den Raum gestellt, ob du richtig gekapselt hast

Grundsätzlich gefällt mir schon nur der Name von PhysicManager nicht. Erscheint mir irgendwie zu zentral.
Wenn ich nur zum Beispiel an eines meiner aktuelle Projekte denke, welches zwar derzeit etwas nebenher lahmt, da andere Projekte eine höhere Priorität haben, so habe ich dort folgende Klassen:
AreaMapping -> Aufteilung der Spielfläche in Sektionen.
CollisionDetection -> Die Objekte in gleichen Sektionen werden untereinander geprüft.
CollisionHandler -> Behandlen einer allfälligen Kollision.
GravitySystem -> Gravitation auf die Objekte anwenden.
usw.Momentan besteht die Physik eigentlich nicht aus viel mehr als Kollisionsabfrage/-reaktion und Gravitation/Bewegung. Diese beiden Aufgaben möglicherweise noch in einzelne Klassen aufspalten, aber das lässt sich auch leicht in Zukunft durchführen. Ich muss mir sowieso noch überlegen, wie ich das Ganze mache, wenn noch mehr Spielelemente dazu kommen. Kollisionsabfrage und -reaktion sind momentan aber so eng verbunden, dass ich die unmöglich auftrennen kann. Dazu sehe ich aber auch keinen Anlass.
Dravere schrieb:
Diese Aussage verwundert mich allerdings auch noch ein wenig. Von wievielen Methoden reden wir hier? Und suchst du eine andere Lösung, nur um deine Schreibfaulheit zu umgehen?

Ich hab bereits beim Schreiben gedacht, dass man dies falsch interpretieren könnte. Die Faulheit ist nicht das Problem, sondern dass nachher zu viele Methoden in der Klasse stecken. Wobei das eigentlich nicht sehr schlimm ist.
Dravere schrieb:
Wieviele Eigenschaften stecken bei dir denn in einem Object? Womöglich zuviele?
Aktuell sind es nur 6 Membervariablen, das sollte eigentlich legitim sein. Das Problem ist, dass drei davon zweidimensionale Vektoren sind, und man bei denen teilweise nur eine Komponente ändern muss. Dann müsste ich entweder immer hässliche Konstrukte bauen:
Object.SetVector(NewX, Object.GetVector().y);Oder ich überleg mir deine Variante mit den optionalen Angaben. Das tönt wirklich interessant.
-
Nexus schrieb:
In den Berechnungen passieren aber keine Fehler. Falls es doch der Fall sein sollte (Bugs gibts immer, ich hab noch nicht gross getestet), werde ich die kaum zur Laufzeit abfragen, sondern gleich im Quellcode beheben.

Ich meinte nicht nur solche Fehler, sondern auch logische Spielfehler.
Es ist manchmal deutlich einfacher das Resultat auf Gültigkeit zu prüfen, als im voraus probieren die Gültigkeit des Resultates abzuschätzen. Wenn du nun direkt auf dem Modell rechnest, dann musst du am Ende die Sache womöglich zurücksetzen. Bei meinem Fall verwerfe ich einfach das Resultat.
Aber es kommt natürlich darauf an, wie deine Implementation aussieht. Das sind alles sehr theoretische und allgemein gehaltene Konstrukte, welche ich da aufführe
Nexus schrieb:
Kollisionsabfrage und -reaktion sind momentan aber so eng verbunden, dass ich die unmöglich auftrennen kann.
Ich kann da natürlich auch nur theoretisch antworten. Aber die Idee dahinter ist, dass die Kollision und Reaktion unterschiedliche Dinge sind. Zum einen hat man Kollisionen, welche womöglich auf unterschiedlichen Ebenen stattfinden, zum anderen hat man Reaktionen, welche je nach Umgebung und anderen Einflüssen unterschiedlich sein können (abgesehen vom Material der kollidierenden Objekte).
Dem Kollisionsabfrage-Objekt kann man nun verschiedene Kollisionsreaktion-Objekte übergeben. Bei der Kollision wird dann das entsprechend korrekte Kollisionsreaktion-Objekt aufgerufen.
Als theoretisches Beispiel:
Sagen wir du hast unterschiedliche Levels und in einem Level sollen Kollisionen deutlich schwächere Reaktionen haben, als in einem anderen. Du speicherst einfach ein entsprechendes Kollisionsreaktion-Objekt ab. Sobald das Level betreten wird, wechselst du das Kollisionsreaktion-Objekt im Kollionsionsabfrage-Objekt aus. Das kann womöglich nur ein Zeiger sein.
Das ist eine äusserst kurze Anweisung und schon läuft alles, wie man es möchte (sofern keine Programmierfehler vorhanden sind ;)). Am Ende des Levels kann man mit der gleichen kurzen Anweisung, die Sache wieder zurück tauschen.Nexus schrieb:
Aktuell sind es nur 6 Membervariablen, das sollte eigentlich legitim sein. Das Problem ist, dass drei davon zweidimensionale Vektoren sind, und man bei denen teilweise nur eine Komponente ändern muss. Dann müsste ich entweder immer hässliche Konstrukte bauen:
Object.SetVector(NewX, Object.GetVector().y);Oder ich überleg mir deine Variante mit den optionalen Angaben. Das tönt wirklich interessant.
Oder du überlegst dir zum Beispiel sowas:
template<typename ValueT, typename ObjectT> void set_vector_x(ObjectT& object, ValueT const& value) { object.set_vector(value, object.get_vector().y); } template<typename ValueT, typename ObjectT> void set_vector_y(ObjectT& object, ValueT const& value) { object.set_vector(object.get_vector().x, value); }Man könnte auch in der Klasse selbst eine
set_xundset_yMethode einbauen. Mit den Templates, und wenn man ein einheitliches Interface hat, kann man sich natürlich Schreibarbeit sparen.
Du könntest aber natürlich auch immer den ganzen Vektor kopieren und dann auf der Variable des kopierten Vektors rumrechnen. Am Ende einfach den ganzen Vektor zurückschreiben. Würde mich nicht überraschen, wenn der Compiler erkennt, dass die andere Variable nicht berührt wird und es daher wegoptimiert. Aber naja, bereits der Gedanke an Optimierung, vor allem in so einem Fall, stösst in die Richtung von PMO
Grüssli
-
Danke für deine Antworten.
Dravere schrieb:
Ich kann da natürlich auch nur theoretisch antworten. Aber die Idee dahinter ist, dass die Kollision und Reaktion unterschiedliche Dinge sind. Zum einen hat man Kollisionen, welche womöglich auf unterschiedlichen Ebenen stattfinden, zum anderen hat man Reaktionen, welche je nach Umgebung und anderen Einflüssen unterschiedlich sein können (abgesehen vom Material der kollidierenden Objekte).
Ja, grundsätzlich hast du hier schon Recht. Allerdings gibt es bei zwei Dinge, die dagegen sprechen: Erstens ist die Kollision überall gleich, also allgemeingültig (spricht zwar nicht dagegen, aber die Abstraktion bringt nicht sehr viel). Das zweite, und viel kritischere besteht in der Implementierung. Diese ist nämlich ziemliches Frickelwerk, da eine eigene Kollisionsbehandlung habe und sehr viele Spezialfälle abdecken muss. Vieles ist läuft nach dem Schema
if (Kollision) { Behandlung; }. Und ich bin froh, dass das Ganze nun läuft, da habe ich keine Lust, das alles umzukrempeln (kannst du sicher nachvollziehen).

Sowieso stelle ich es mir schwieriger vor, wenn die Fehler nicht gleich lokal behandelt werden können. Dann müssen womöglich noch diverse Statusflags übergeben werden, was die Sache auch nicht gerade vereinfacht.Die Sache mit den Templates ist natürlich auch eine Möglichkeit. Aber momentan sollte es nicht zu schlimm sein wegen der Memberfunktionen. Ich habe bei mir auch nicht mehrere Klassen mit gleicher Schnittstelle, die als Typ-Argument
ObjectTdienen könnten.Also würdest du die Variablen nicht direkt ändern, sondern zuerst kopieren und dann wieder setzen:
void PhysicManager::HandleCollision(Object& CurObj) { float vx = CurObj.GetVelocity().x; float vy = CurObj.GetVelocity().y; // hier eigentlicher Algorithmus CurObj.SetVelocity(Vector2f(vx, vy)); }Oder ich biete spezifischere Funktionen wie
void SetVelocity(float, float)an...
-
Nexus schrieb:
Diese ist nämlich ziemliches Frickelwerk, da eine eigene Kollisionsbehandlung habe und sehr viele Spezialfälle abdecken muss.
Das ist aber gar nicht gut

Aber naja ...Nexus schrieb:
Also würdest du die Variablen nicht direkt ändern, sondern zuerst kopieren und dann wieder setzen:
Oder ich biete spezifischere Funktionen wie
void SetVelocity(float, float)an...Es kommt natürlich teilweise darauf an, was ich mache, aber ich probiere mich an so ein Prinzip zu halten.
Abgesehen vom praktischen Nutzen, welchen ich dahinter sehe, empfinde ich auch diese Schreibweise als lesbarer. Es entspricht dem Gedankengang einer Funktionsweise.Gedankengang bei deiner Version:
-> Attribute als Referenz gelesen, ok, die Geschwindigkeit ist also in vx und vy drin.
-> Komplizierte Berechnung. Wie geht das, ach so. Ja ... ok ... hmmm ... gut ... (in der Zwischenzeit vernichtet das Gehirn die Erinnerung daran, dass vx und vy Referenzen sind, da es die Ressourcen für anderes braucht) ... ok, interessant, gut ... das funktioniert ... verstanden.
-> Funktionsende? Und das Ergebnis? Wo, wie, was?
-> Raufscrollen, rückwärts lesen des Codes, bzw. überfliegen. Irgendwas übersehen?
-> Ah, es waren Referenzen ...
-> Je nachdem nochmals runterscrollen, um sich dies nochmals klar zu machen, was das nun genau bedeutet. Die Zuweisung findet im Algorithmus statt.Gedankengang bei meiner Version:
-> Attribute gelesen, ok, die Geschwindigkeit ist also in vx und vy drin.
-> Komplizierte Berechnung (ich wiederhole nicht nochmals all diese kleinen Wörter :))
-> Setzen der Attribute, gut.
-> Funktionsende, verstanden.Grüssli
-
Dravere schrieb:
Das ist aber gar nicht gut

Aber naja ...Hätte mich interessiert, ob du das sauber hingekriegt hättest. Und denk dran, es ist nicht umso besser, je mehr Klassen und Dateien das Ganze benötigt. :p
Ok, das mit dem Gedankengang ist auch sehr subjektiv. Ich weiss ja, dass die Werte als Referenzen genommen werden, beim Anschauen hab ich das noch nicht vergessen.

Aber mit der grundsätzlichen Funktionsweise stimmt es schon. Ausserdem ist dann die Schnittstelle von
Objectnicht hässlich (entweder direkter Zugriff auf Member oder Referenzrückgabe). Wahrscheinlich werde ich es mit herkömmlichen Gettern und Settern machen.
-
Nexus schrieb:
Hätte mich interessiert, ob du das sauber hingekriegt hättest. Und denk dran, es ist nicht umso besser, je mehr Klassen und Dateien das Ganze benötigt. :p

In meiner Theorie sicher *ist schliesslich ein grosser Theoretiker* :pNexus schrieb:
Ok, das mit dem Gedankengang ist auch sehr subjektiv. Ich weiss ja, dass die Werte als Referenzen genommen werden, beim Anschauen hab ich das noch nicht vergessen.

Du weisst es jetzt, aber was ist in einem Jahr? Oder wenn jemand anderes den Code anschauen soll.
Ich kann Spaghetti Code schreiben, wenn ich mich gerade mit dem Code beschäftige, dann verstehe ich auch alle gotos. Aber wenn ich mich nur einen Monat mit etwas anderem beschäftige und dann zurückkehre, dann habe ich unglaubliche Mühe, mich wieder einzuarbeiten
Aber es hat natürlich schon auch eine gewisse Subjektivität drin. Es geht schliesslich in Richtung von m_ oder nicht m_.

Grüssli
-
Also ich bevorzuge auch die Version, dass ich zuerst lese, bearbeite und dann zurückschreibe. Dann kann ich auch kürzere, lokale Variablen nehmen.
-
Dravere schrieb:
Du weisst es jetzt, aber was ist in einem Jahr? Oder wenn jemand anderes den Code anschauen soll.
Nein, das würde garantiert kein Problem darstellen. Die ersten beiden Anweisungen in der Funktion sind die Referenzinitialisierungen. Wenn man die übersieht (gerade wenn da noch ein Kommentar steht)...
So, ich habe es nun mit Gettern und Settern gemacht, aber aus dem Grund, dass zuerst gelesen wird, dann bearbeitet und anschliessend wieder geschrieben (wie drakon sagte). Allerdings ist das immer noch nicht ganz konsequent, da zwischendurch auch geschrieben wird (über Setter). Aber wenn man das wirklich durchziehen wollte, müsste man das Objekt kopieren.
Momentan sind es zwar etwa 25 Memberfunktionen in der
Object-Klasse, aber das finde ich nicht so tragisch. Viele wären eigentlich nicht direkt nötig, da sie andere aufrufen und zur vereinfachten und intuitiveren Nutzung der Klasse beitragen. Eventuell könnte ich mir überlegen, diese als freie Funktionen zu implementieren, aber momentan werde ich es wahrscheinlich so lassen...