Objekte im Level - Designproblem

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Objekte im Level - Designproblem

Beitrag von Psycho »

Hallo,

ich habe eine Klasse Level, in der es verschiedene Arten von Objekten gibt.
Zum Beispiel Ship, Projectile und Building. Die erben alle von einer Klasse Drawable, welche aus einer Position, einem Sprite und einer Methode zum Zeichnen besteht.
Wenn ein Ship oder ein Building kaputt geschossen wird, soll eine Explosion erscheinen (ebenfalls Drawable). Die wird über Level::addExplosion(position) hinzugefügt.

Nun die Frage: Sollte ich in der Klasse Level alle Spielobjekte in einem gemeinsamen boost::ptr_vector<Drawable> speichern oder lieber getrennte Listen für Ship, Building, Projectile...

Vorteil bei einer gemeinsamen Liste:
Im Destruktor von Shootable (also alles, was abgeschossen werden kann: Ship, Building) könnte ich level.addExplosion(position) aufrufen. Dann steht es nur ein einziges Mal im Code da.
Nachteil bei einer gemeinsamen Liste:
Wenn ich zum Beispiel prüfen möchte, ob der Spieler mit einem Schiff kollidiert, kann ich ja nicht einfach gegen alle Ships prüfen, da ich ja gar nicht weiß, welche Drawable's überhaupt Schiffe sind. Meine Lösung dafür war, dass ich bereits in der Klasse Drawable eine virtuelle Methode collidesWithLocalPlayer(localPlayer) bräuchte, die dann Ship, Building usw selber implementieren müssten. Auch nicht super schön finde ich.

Nachteil bei getrennten Listen:
Ich kann nicht einfach aufrufen: level.removeObject(Drawable &)
Denn ich müsste erst prüfen, was für ein Drawable es überhaupt ist und aus der entsprechenden Liste rausnehmen.
Hier würde auch eine virtuelle Methode remove() in Drawable helfen, sodass ich zB mache:
Ship::remove() { level.removeShip(this); }

Die Klasse Level bräuchte dann auch für jede einzelne Liste eine add- und remove-Methode.

Also die Alternativen:

Code: Alles auswählen

class Level
{
private:
  boost::ptr_vector<Drawable> allObjects;
public:
  add(Drawable *newObject);
  remove(Drawable &object);
};
oder aber

Code: Alles auswählen

class Level
{
private:
  boost:ptr_vector<Ship> ships;
  boost:ptr_vector<Building> buildings;
  boost:ptr_vector<Projectiles> projectiles;
  boost:ptr_vector<Explosions> explosions;
public:
  addShip(Ship *newShip);
  removeShip(Ship &ship);
  addBuilding(Building *newBuilding);
  // [...]
};
Beides nicht perfekt, habt ihr bessere Ideen?
Despotist
Establishment
Beiträge: 394
Registriert: 19.02.2008, 16:33

Re: Objekte im Level - Designproblem

Beitrag von Despotist »

Psycho hat geschrieben: Wenn ich zum Beispiel prüfen möchte, ob der Spieler mit einem Schiff kollidiert, kann ich ja nicht einfach gegen alle Ships prüfen, da ich ja gar nicht weiß, welche Drawable's überhaupt Schiffe sind.
Warum diese Einschränkung auf Schiff-Schiff? Normalerweise willst du doch wissen ob ein Objekt mit einem anderen kollidiert. Also auch Schiff-Struktur und Schiff-Projectile usw.. Da würde ich diese Variante vorziehen.
Bei mir haben alle Elemente in der Liste eine int-Variable die ihren Typ angibt der gleichzeitig auch zum auswählen der Textur oder des Modells genommen wird (Index in Texturlist zb.). Auf diese Art kannst du immer besondere Fälle prüfen (zb Andockreichweite).
Es ist natürlich nicht besonders "elegant" die Drawable Klasse für Kollisionen zu mißbrauchen. Normalerweise würde man da eine eigene Abstraktion (Interface) dahinter stellen. Aber bei einem kleinen Projekt sollte es zu vertreten sein.
Wenn du alles auf Kollision prüfst noch zwei Tips (die du aber vielleicht schon kennst).
1. du musst nicht von i=1-n von j=1-n checken sondern i=1-n j=i-n und natürlich nur elemente i!=j testen (nicht mit sich selber)
2. als erste Näherung ob überhaupt eine Kollision möglich ist testen ob der Kollisionsabstand² < dx² + dy² ist. Da kannst du dir das häufige Wurzelziehen sparen indem du den Kollisionsbstand einmal quadrierst.

Ein weiterer Nachteil von getrennten Listen wäre, wenn du später noch 5 andere Typen an die du jetzt noch nicht denkst einfügen willst. Mit einer Liste ist das einfach wenn du von der Basisklasse ableitest. Genau dafür ist Vererbung da. Da die Objekte vom Typ Drawable auch die fürs zeichnen notwendige Funktionlität teilen (Position, Ausrichtung, Geschwindigkeit) sollten sie auch zusammen verwaltet werden. Die Logik wird ja dann woanders behandelt (z.B. ob Stationen shießen können oder das Andocken).

Du kannst ja auch für verschiedene Zwecke verschiedene Verweise auf dieselben Daten haben. Z.B. hat die Szene eine Liste mit Pointern auf alle Drawable Objekte. Jeder (KI)Spieler hat Pointer auf seine Schiffe und Stationen. Der Schussmanager verwaltet die Projektile. Die Daten werden also bei einem Besitzer gehalten und von vielen spezialisierten Listen referenziert.

Das ist natürlich nur meine "unfundierte" Meinung die auch etwas von Faulheit geprägt ist. Vielleicht denken die Profis anders.
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Vielen Dank für deine Antwort.
Du kannst ja auch für verschiedene Zwecke verschiedene Verweise auf dieselben Daten haben. Z.B. hat die Szene eine Liste mit Pointern auf alle Drawable Objekte. Jeder (KI)Spieler hat Pointer auf seine Schiffe und Stationen. Der Schussmanager verwaltet die Projektile. Die Daten werden also bei einem Besitzer gehalten und von vielen spezialisierten Listen referenziert.
Das gefällt mir sehr gut.
Warum diese Einschränkung auf Schiff-Schiff? Normalerweise willst du doch wissen ob ein Objekt mit einem anderen kollidiert. Also auch Schiff-Struktur und Schiff-Projectile usw.. Da würde ich diese Variante vorziehen.
Natürlich wird es auch andere Kollisionsfälle geben, dieser eine war nur ein Beispielfall. Die anderen haben natürlich ein anderes Verhalten, als Schiff-Schiff-Kollisionen. Deswegen werde ich jetzt wohl "über" Drawable noch ein Interface GameObject bauen, welches dann Methoden wie collidesWithLocalPlayer und collidesWithProjectile fordert und von dem alle anderen Klassen, auch Drawable, erben.

Die Lösung ist okay, auch wenn ich es etwas unnätürlich finde, dass dann halt jedes einzelne GameObject die Dinger auch implementieren muss.
Das andere Ding ist: Wieso soll denn bitteschön das *andere* Schiff entscheiden dürfen, was bei einer Kollision mir *mir* passiert? Hatte das Gefühl, dass dieses Verhalten dann auch in die LocalPlayer-Klasse gehört.

Ich könnte natürlich auch folgendes machen:

Code: Alles auswählen

void LocalPlayer::checkForCollisions()
{
  foreach (gameObject in level.gameObjects)
  {
    if ((gameObject != this) && collidesWith(gameObject))
    {
      gameObject.collidesWithLocalPlayer(this);
    }
  }
}
void Ship::collidesWithLocalPlayer(LocalPlayer localPlayer)
{
  localPlayer.collidesWithShip(this);
}
void LocalPlayer::collidesWithShip(Ship ship)
{
  // AHA! Jetzt kann LocalPlayer sagen, was passiert!
}
Aber mal ehrlich: An sich habe ich doch ein recht einfaches Problem. Trotzdem tue ich mich ziemlich schwer, eine Lösung zu machen, die mir auch gefällt. Das ärgert mich ganz schön. Guck wie umständlich das oben schon wieder aussieht :( Wie Triple-Dispatching
Benutzeravatar
kimmi
Moderator
Beiträge: 1405
Registriert: 26.02.2009, 09:42
Echter Name: Kim Kulling
Wohnort: Luebeck
Kontaktdaten:

Re: Objekte im Level - Designproblem

Beitrag von kimmi »

Der Alles-in-einer-Liste-Ansatz bringt nach meinem Verständnis verschiedene Verantwortlichkeiten zusammen, die so nicht zusammengehören. Verteile die Verantwortungen für Collisionen, Rendering und was auch immer noch anfällt. So könntest du zum Beispiel eine Manager / Server - Klasse für Drawable-Instanzen bauen und dort die jeweiligen Drawable-Objekte verwalten. Gleiches gilt für die Kollisionen. Nun kann man diese verschiendenen Instanzen in einem Gameentity unterbringen, der gemäß seiner eigenen Properties sich bei den jeweiligen Manager registriert. Sollen Kollisionen in dem Game-Loop erkannt werden, kannst du durch die Collision-Liste hindurch iterieren und mußt erst gar keine unschönen Typerkennungen durchführen, damit du einen möglichen Collider als solchen erkennen kannst.
Und man hat klare Verantwortlichkeiten definiert:
  • Game-Entity verantwortet seine Properties / Eigenschaften wie Drawable, Collision etc.
  • Drawable-Property verantwortet Rendering
  • Collision-Property verantwortet Kollisions-Erkennung
Die Game-Entities kann man natürlich in einer Liste halten. Das ist zumindest ein möglicher Ansatz.

Gruß Kimmi
Despotist
Establishment
Beiträge: 394
Registriert: 19.02.2008, 16:33

Re: Objekte im Level - Designproblem

Beitrag von Despotist »

Psycho hat geschrieben: Das andere Ding ist: Wieso soll denn bitteschön das *andere* Schiff entscheiden dürfen, was bei einer Kollision mir *mir* passiert? Hatte das Gefühl, dass dieses Verhalten dann auch in die LocalPlayer-Klasse gehört.
Das hab ich nicht geschrieben ;). Ich meinte es eher so dass du eben einen Manager hast der alle in irgendeiner Form kollidierbaren Objekte verwaltet (in einer Liste). Der Manager iteriert nun durch die Liste und prüft ob überhaupt eine Kollision vorhanden ist. Wenn das der Fall ist kann er eine Typunterscheidung machen und die entsprechende Funktion aufrufen (Schiff-Schiff, Schiff-Projektil, Schiff-Station). Das heißt der Manager behandelt die Kollsionen da er der einzige ist der beide Objekte kennt und nicht das an der Kollision beteiligte Objekt selbst. Der Manager verteilt dann den Schaden und bestimmt die Abprallwinkel und -Geschwindigkeiten. So ist die Logik fürs kollidieren in dieser Managerklasse vereint. Und sollte ein neuer Typ hinzukommen (z.b. Asteroid) dann musst du nur den Manager ändern aber nicht den Player und die ganzen anderen Klassen. Dafür fügst du einfach die Funktion Schiff-Asteroid hinzu und musst auch an der Kollsisionlogik kaum was ändern.

Stell dir immer vor du fügst von irgendetwas einen neuen Typ hinzu. Was musst du alles ändern dafür? Es sollten so wenig wie möglich Klassen davon betroffen sein. Wenn jede Klasse alle anderen kennt und beeinflusst sind Änderungen die Hölle.
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Hm so ganz hab ich es leider noch nicht verstanden.
Woher weiß der Collision-Manager denn, ob es sich bei dem Objekt in der Liste um ein Schiff, ein Projektil oder Gebäude handelt?

Du hast weiter oben geschrieben:
Bei mir haben alle Elemente in der Liste eine int-Variable die ihren Typ angibt
Genau das wollte ich eigentlich vermeiden. Ich wüsste auch nicht, wie das ohne Rumcasten geht. Da kann ich ja gleich RTTI anmachen?!
Kannst Du mir das vielleicht mit einem kleinen Beispiel verdeutlichen?

kimmis Post habe ich leider auch nicht ganz verstanden. Den Teil, dass sich die Objekte selber bei den Managern registrieren schon. Aber nicht, wie der Manager die Collisionen prüft bzw. die Auswirkungen anwendet.
Benutzeravatar
kimmi
Moderator
Beiträge: 1405
Registriert: 26.02.2009, 09:42
Echter Name: Kim Kulling
Wohnort: Luebeck
Kontaktdaten:

Re: Objekte im Level - Designproblem

Beitrag von kimmi »

Das Interface Collidalble ( oder so ähnlich ) könnte zum Beispiel dem Owner ( also zum Beispiel der Gameentity ) per Callback mitteilen, daß eine Kollision festgestellt wurde. Und über das Thema effitiente Kollisionsverwaltung gibt es zum einen jede Menge Papers ( wie erkenne ich Kollisionen zwischen verschiedenen Primitiven etc. ) und zum anderen das Stichwort: Double Dispatching.

Gruß Kimmi
Despotist
Establishment
Beiträge: 394
Registriert: 19.02.2008, 16:33

Re: Objekte im Level - Designproblem

Beitrag von Despotist »

Psycho hat geschrieben: Genau das wollte ich eigentlich vermeiden. Ich wüsste auch nicht, wie das ohne Rumcasten geht. Da kann ich ja gleich RTTI anmachen?
Naja wie gesagt brauch ich die Variable eh da sie die optische Repräsentation enthält (Grafik). Also wenn ich wissen will was das DIng ist teste ich einfach gegen diesen int-Wert. Ist vielleicht nicht ganz OOP und sauber aber mir reicht es. Warum willst du es denn unbedingt vermeiden (RTTI, Typvariable)?
Wenn du C# verwendest kannst du auch Reflections verwenden (Jedes Objekt weiß von welchem Typ es ist).

Du könntest aber z.B. auch die Collide-Funktion einfach überladen und den Compiler die Richtige rauspicken lassen.
Pseudocode (irgendwie wird nicht vernünftig eingerückt):

Code: Alles auswählen

CollisionManager:TestCollisions()
{
  for(int i = 0; i < ObjectList.Count; i++)
  {
    for(int j = i; j < ObjectList.Count; j++)
   {
      if(collision(i,j)
        Collide(ObjectList[i], ObjectList[j]);
    }
  }
}

Collide(Schiff, Schiff) {};
Collide(Schiff, Station) {};
Collide(Schiff, Asteroid) {};
Edit: Beim Überladen aber darauf achten das du wirklich ALLE Kombinationen implementieren musst also auch die vertauschten (Station-Schiff, Schiff-Station).
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Ok dann erstmal vielen Dank euch beiden bis hierher.

Eigentlich sind ja jetzt zwei Probleme draus geworden:
1. Abgrenzung des Renderings von der Kollisionserkennung durch Manager-Klassen
2. Kollisionserkennung selbst durch Double-Dispatching oder Hilfsvariable

Ob ich das erste noch berücksichtige werde ich überlegen. Mein Projekt wird ein Minispiel und von daher wäre es denke ich noch in Ordnung, wenn ich sowohl Rendering als auch Kollisionsbehandlung in der Klasse Level mache.

Ich hoffe, dass ich schnell genug fertig werde, dann werde ich es auch in der ZFX-Action einreichen.
Despotist
Establishment
Beiträge: 394
Registriert: 19.02.2008, 16:33

Re: Objekte im Level - Designproblem

Beitrag von Despotist »

Psycho hat geschrieben: Ich hoffe, dass ich schnell genug fertig werde, dann werde ich es auch in der ZFX-Action einreichen.
Oh nein, dann helfe ich ja der Konkurrenz ;). Aber es wäre natürlich schön wenn es klappt.
Psycho hat geschrieben: Ob ich das erste noch berücksichtige werde ich überlegen. Mein Projekt wird ein Minispiel und von daher wäre es denke ich noch in Ordnung, wenn ich sowohl Rendering als auch Kollisionsbehandlung in der Klasse Level mache.
Das habe ich versucht rüberzubringen. Für kleine Sachen ok, für größere wäre ein sauberer Ansatz einen Blick wert.
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Oh nein, dann helfe ich ja der Konkurrenz
Hehe, hab auch im Nachhinein gedacht, dass es ziemlich fies ist, erst hinterher damit rauszukommen.
zwergmulch
Beiträge: 91
Registriert: 07.12.2009, 16:42
Echter Name: Fabian R

Re: Objekte im Level - Designproblem

Beitrag von zwergmulch »

Psycho hat geschrieben:
Despotist hat geschrieben:Oh nein, dann helfe ich ja der Konkurrenz
Hehe, hab auch im Nachhinein gedacht, dass es ziemlich fies ist, erst hinterher damit rauszukommen.
Also ich persönlich finde es schön, wenn es viele Beiträge gibt. Ist jetzt nicht so das ich total selbstsicher bin - im Gegenteil.
Es freut mich nur wenn es viele schöne Spiele gibt, die man dann mal ausprobieren kann. :D 8-)

@Topic: Bei meinem Projekt für die ZFX-Action habe ich es so gemacht:
Es gibt eine Oberklasse GameObject. Der Konstruktor von GameObject (bzw. jetzt eine Extra-Methode, aber das ist ein "unwichtiges" Implementierungsdetail und für das Design erstmal unerheblich)
fügt Objekte automatisch zur Liste hinzu und GameObject verwaltet mittels statischer Methoden die Objekte (updaten und zeichnen - es gibt Methoden updateAll () und paintAll (Graphics g) ).
GameObject testet auch (auf Kommando) ob Objekte kollidieren und ruft dann eine Callback-Methode auf. Jetzt zu dieser Methode:
Es gibt nun auch ein Interface Moveable für bewegliche Objekte. Die entsprechende Klasse testet nun auch, ob das Objekt das Interface implementiert
(in C++ wäre das RTTI: dynamic_cast<Moveable> (obj) != 0,
in Java ist es einfacher(und vlt. auch nicht ganz so verpönt ;) ) : obj instanceof Moveable). Wenn es ein Objekt dieser Klasse ist, kann es nun mit den Methoden von Moveable das Objekt verschieben
bzw. in deinem Fall könnte es ja ein Interface Explose geben.

Meine Umsetzung... :)
Bild
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Sooo..habe jetzt einen ganzen Batzen Arbeit hinter mir und folgende neue Klassen:
RenderManager
Renderable (im Prinzip Drawable, nur anders genannt), welches RenderManager::register() und unregister() in ctor und dtor aufruft

CollisionManager
Collidable (auch mit register() und unregister())

Dann habe ich für Level::addObject() und removeObject() neue temporäre Listen eingefügt, die erst nach updaten der Positionen und prüfen der Kollisionen tatsächlich mit der gameObjects-Liste verschmolzen werden. Gab sonst einige Fehler und ungültige Iteratoren.

Bin jetzt sehr zufrieden mit dem Aufbau.

Wenn jetzt aber mitten im Spiel Level::add(new Building()) aufgerufen wird...kommt es natürlich ans Ende der Liste...und wird als letztes, über alle anderen Sprites gerendert. Also eventuell auch über Flugzeuge. Ups.

Also brauch ich wohl irgendein Attribut in Renderable, welches die Reihenfolge bestimmt. Dann sortier ich die eigene Liste von RenderManager danach und render in genau der Reihenfolge.
Was meint ihr?
Benutzeravatar
kimmi
Moderator
Beiträge: 1405
Registriert: 26.02.2009, 09:42
Echter Name: Kim Kulling
Wohnort: Luebeck
Kontaktdaten:

Re: Objekte im Level - Designproblem

Beitrag von kimmi »

Du kannst bei 2D so etwas wie eine Höhe bzw. Abstand zur Kamera als Attribut mit angeben, nach der du vor dem Rendern sortieren kannst. Dann hast derlei Probleme nicht. Und für GUI-Elemente kann man so etwas wie ein Overlay-Attribut benutzen. Das ist natürlich nur ein möglicher Ansatz.

Gruß Kimmi
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Ok das stellt kein Problem dar.

Zu den Manager-Klassen: Mich stören jetzt noch die paarweisen Aufrufe von register() und unregister(). Weil das ja Programmier-Steinzeit ist.
Sollte ich ein Extra-Objekt RegisterAtManager machen, welches die Funktionen im Ctor und Dtor aufruft?

Wird glaub ich (zu) eng mit dem 31. Mai :(
Benutzeravatar
Krishty
Establishment
Beiträge: 8246
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Objekte im Level - Designproblem

Beitrag von Krishty »

Sorry für den späten Einstieg und dass es mittlerweile Off-Topic ist, aber rieche ich hier ein Vorurteil gegen RTTI?
RTTI ist effizienter als Typvariablen und es gibt in 90 % der Fälle keinen Grund, es nicht zu aktivieren. Auch wurde extra, um Tipparbeit zu sparen, die Variablendeklaration in if eingeführt (if(Movable * MovableObject = dynamic_cast<Movable *>(UnknownObj) MovableObject->Move(); - jaa, Java ist immernoch schöner, stimmt schon).
Nur so neben die Tüte.

Gruß, Ky
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Psycho
Establishment
Beiträge: 156
Registriert: 16.09.2002, 14:23

Re: Objekte im Level - Designproblem

Beitrag von Psycho »

Ich habe bisher noch nie mit RTTI gearbeitet.

Die Alternative zu den Managern wäre wohl gewesen, innerhalb der Klasse Level mit dynamic_cast zu arbeiten und entsprechend des Klassentyps zu reagieren. Meinst Du, das wäre ein angebrachter Einsatzzweck für RTTI? :o

Hab noch im Ohr, dass mir mal jemand gesagt hat, dass es schlechter Stil sei, wenn man den Typ der Klasse kennen muss. Man solle es lieber mit Vererbung oder so lösen. OOP halt. Drölf Interfaces. Aber vielleicht ist diese Ansicht ja schon wieder überholt.
Benutzeravatar
Krishty
Establishment
Beiträge: 8246
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Objekte im Level - Designproblem

Beitrag von Krishty »

Psycho hat geschrieben:Hab noch im Ohr, dass mir mal jemand gesagt hat, dass es schlechter Stil sei, wenn man den Typ der Klasse kennen muss. Man solle es lieber mit Vererbung oder so lösen. OOP halt. Drölf Interfaces. Aber vielleicht ist diese Ansicht ja schon wieder überholt.
Normalerweise benutzt man Polymorphie gerade dazu, Problemlösungen zu verallgemeinern - was natürlich das genaue Gegenteil von dynamic_cast ist. Man kommt aber auch mit den besten Pattern (oder bei einigen Optimierungen) nicht drum herum, ab und zu einen Typ kennen zu müssen ... wenn das tatsächlich passiert, dann sollte man RTTI definitv allem Selbstgebrannten wie Typ-Variablen usw vorziehen. Aber Kritik an der Manager-Lösung sollte das keinesfalls sein.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Antworten