Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

21

08.12.2009, 21:07

Re: Templates vs. Interface

Zitat von »"Nexus"«

Ein weiterer Vorteil dieser Vorgehensweise besteht darin, dass du gleich automatische Objekte in Container packen kannst und keine Umwege über polymorphe Zeiger gehen musst. Hüte dich so gut es geht vor rohen, besitzenden Zeigern in STL-Containern, denn diese birgen nicht wenige Risiken.


Das setzt entsprechende Kopiersemantik seiner Objekte voraus die bei komplexen Spielobjekten wohl nicht gegeben ist. Vor allem in Verbindung mit Polymorphie wäre das problematisch (slicing!).

22

08.12.2009, 21:21

Re: Templates vs. Interface

Zitat von »"dot"«

Das setzt entsprechende Kopiersemantik seiner Objekte voraus die bei komplexen Spielobjekten wohl nicht gegeben ist.
Kommt drauf an, wie man seine Objekte gestaltet. Wenn man dem Ideal nahekommt, sie als reine Logikobjekte zu implementieren, hat man leichtgewichtige Instanzen. Natürlich muss man sich auch irgendwo um Ressourcen und ähnliches kümmern. Das muss aber nicht zwangsläufig Bestandteil Spielobjekt-Klasse selbst sein, denn damit erschwert man Objektsemantik recht stark (vor allem durch Exceptionsicherheit und Performance).

Zitat von »"dot"«

Vor allem in Verbindung mit Polymorphie wäre das problematisch (slicing!).
Eben nicht, da das auf oberster Ebene passiert. Soll heissen, man hantiert mit statischen Typen. Irgendwo muss man den exakten, statischen Typen eines Objekts kennen – mindestens bei der Konstruktion. Sofern man es zustande bringt, Spielobjekte an einem gemeinsamen Ort zu konstruieren, hat man auch dieses Problem nicht mehr.

Aber selbst ein ptr_container<Enemy> ist noch besser als ein ptr_container<Object>, sofern man Nutzen aus der zusätzlichen Typinformation ziehen kann. Und letzteres ist meist der Fall, da sich Gegner von anderen Objekten unterscheiden und speziell behandelt werden müssen (was nicht immer mit virtuellen Funktionen geht). Mit ptr_container meine ich übrigens ein Container-Klassentemplate, das seine Elemente als Zeiger speichert und die Speicherverwaltung übernimmt, wie die Pointer-Containers von Boost.

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

23

08.12.2009, 21:30

Re: Templates vs. Interface

Zitat von »"Nexus"«

Zitat von »"dot"«

Das setzt entsprechende Kopiersemantik seiner Objekte voraus die bei komplexen Spielobjekten wohl nicht gegeben ist.
Kommt drauf an, wie man seine Objekte gestaltet. Wenn man dem Ideal nahekommt, sie als reine Logikobjekte zu implementieren, hat man leichtgewichtige Instanzen. Natürlich muss man sich auch irgendwo um Ressourcen und ähnliches kümmern. Das muss aber nicht zwangsläufig Bestandteil Spielobjekt-Klasse selbst sein, denn damit erschwert man Objektsemantik recht stark (vor allem durch Exceptionsicherheit und Performance).


Leichtgewichtigkeit bringt aber noch nicht gleich Wertsemantik mit sich. Die Lebensdauer is zum Beispiel auch interessant. Die Objekte in meiner Welt sollten idealerweise nicht plötzlich kopiert und zerstört werden. Wenn du die Objekte in einem vector ablegst passierts dir z.B. dass Pointer/Referenzen/Iteratoren drauf auf einmal ungültig sind wenn der vector wächst usw. Und das mit den Ressourcen ist beim kopieren auch sehr interessant.

Zitat von »"Nexus"«

Zitat von »"dot"«

Vor allem in Verbindung mit Polymorphie wäre das problematisch (slicing!).
Eben nicht, da das auf oberster Ebene passiert. Soll heissen, man hantiert mit statischen Typen. Irgendwo muss man den exakten, statischen Typen eines Objekts kennen – mindestens bei der Konstruktion. Sofern man es zustande bringt, Spielobjekte an einem gemeinsamen Ort zu konstruieren, hat man auch dieses Problem nicht mehr.


Ja natürlich, so lange du mit statischen Typen hantierst nicht. So lange is ja aber auch keine Polymorphie im Spiel ;)

Zitat von »"Nexus"«

Aber selbst ein ptr_container<Enemy> ist noch besser als ein ptr_container<Object>, sofern man Nutzen aus der zusätzlichen Typinformation ziehen kann. Und letzteres ist meist der Fall, da sich Gegner von anderen Objekten unterscheiden und speziell behandelt werden müssen (was nicht immer mit virtuellen Funktionen geht). Mit ptr_container meine ich übrigens ein Container-Klassentemplate, das seine Elemente als Zeiger speichert und die Speicherverwaltung übernimmt, wie die Pointer-Containers von Boost.


Dem stimme ich zu. Wie ich schon gesagt hab, das hängt vom konkreten Anwendungsfall ab, in dem Fall ist das sicher keine schlechte Lösung...

24

08.12.2009, 21:45

Re: Templates vs. Interface

Zitat von »"Nexus"«

Gut, genau so habe ich mir das vorgestellt. Es scheint so, als ob mein Entwurf gar nicht so schlecht passen würde. Sowas wie "Objekt" könntest du als Basisklasse nehmen, von dem du dann z.B. kollidierbare Objekte ableitest, diese werden wiederum in Gegner, Spieler, Projektile unterteilt
... Jo, auf jedenfall eine gute Möglichkeit.

Zitat von »"Nexus"«

Das sehen zwar nicht alle gleich, aber ich persönlich finde es wichtig, Grafik und Logik zu trennen.
Im Grunde ja, aber hier die Frage: Kollision gehört zur Gamelogik, sie baut allerdings auf grafische Elemente (Bildgröße, Alphablending, Rotation) auf. Oder anders ausgedrückt: Durch eine strikte Trennung müsste man hier redundante Informationen speichern.

Diesbezüglich gabs auch mal einen informativen Thread auf Developia: Ein Character IST ein Sprite oder HAT ein Sprite?


Zitat von »"Nexus"«

... Wenn diese darauf hinausläuft, dass wieder Fallunterscheidungen ins Spiel kommen, hat man das Prinzip nicht ganz verstanden.

Für Aufrufe "erster Ordnung" wie Zeichne Objekt XY"(also nur mit einem Objekt) lässt sich das sehr schön über Interfaces als Traits lösen.
Dann kann man alle Objekte durchgehen und sehen, ob "QueryInterface(Drawable)" true zurückgibt und dementsprechend Zeichnen lassen.
Probleme stellen sich halt erst ein, wenn man 2 Typen gegeneinander prüft, weil das von C++ nicht nativ unterstützt wird. (Gibts da zufällig ne Boost-Lösung für?)

Zitat von »"Nexus"«

Hüte dich so gut es geht vor rohen, besitzenden Zeigern in STL-Containern, denn diese birgen nicht wenige Risiken.

Was kann ich mir darunter vorstellen? Gibts ein kleines Code-Beispiel? :)

So Far...

Laguna

25

08.12.2009, 21:56

Zitat von »"dot"«

Und das mit den Ressourcen ist beim kopieren auch sehr interessant.
Es kommt halt sehr darauf an, wie das gesamte Design gestaltet ist. Nicht immer ist es angebracht, Logikobjekte 1:1 auf Ressourcen abzubilden (z.B. dann nicht, wenn eine Ressource von mehreren Objekten geteilt wird). Ich will damit nur sagen, dass die Ressource nicht unbedingt ein Attribut des Logikobjekts sein muss.

Zitat von »"dot"«

Leichtgewichtigkeit bringt aber noch nicht gleich Wertsemantik mit sich. Die Lebensdauer is zum Beispiel auch interessant. Die Objekte in meiner Welt sollten idealerweise nicht plötzlich kopiert und zerstört werden.
Das kann ich nachvollziehen. ;)
Aber das werden sie eigentlich auch nicht. Wenn du auf Reallokationen von std::vector etc. anspielst: Das macht nichts, sofern sichergestellt werden kann, dass nachher die richtige Anzahl Objekte mit den richtigen Stati vorhanden ist (worum man sich meistens nicht explizit kümmern muss). Wenn man diesen Weg wählt, steht eine erneute Ressourcenanforderung auch nicht im Kopierkonstruktor. Aber wie gesagt: Das ist eine Möglichkeit.

Zitat von »"dot"«

Wenn du die Objekte in einem vector ablegst passierts dir z.B. dass Pointer/Referenzen/Iteratoren drauf auf einmal ungültig sind wenn der vector wächst usw.
Und das passiert mit Zeigern, die man in einem std::vector ablegt, nicht?

Zitat von »"dot"«

Ja natürlich, so lange du mit statischen Typen hantierst nicht. So lange is ja aber auch keine Polymorphie im Spiel ;)
Nicht direkt, aber man kann ja ohne weiteres Zeiger und Referenzen auf statische Objekte deklarieren (auch implizit bei Funktionsübergaben). Ich will nur sagen, dass es ohne Probleme möglich ist, Laufzeitpolymorphie auf automatisch verwaltete Objekte anzuwenden.

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

26

08.12.2009, 22:11

Zitat von »"Nexus"«

Zitat von »"dot"«

Und das mit den Ressourcen ist beim kopieren auch sehr interessant.
Es kommt halt sehr darauf an, wie das gesamte Design gestaltet ist. Nicht immer ist es angebracht, Logikobjekte 1:1 auf Ressourcen abzubilden (z.B. dann nicht, wenn eine Ressource von mehreren Objekten geteilt wird). Ich will damit nur sagen, dass die Ressource nicht unbedingt ein Attribut des Logikobjekts sein muss.


Ja, aber es muss irgendwie eine Beziehung zwischen der Ressource und dem Logikobjekt hergestellt werden. Entweder indem das Objekt eine Referenz auf die Resource hat oder umgekehrt oder sonst wie. Natürlich is das durchaus beherrschbar. Ich wollte nur aufzeigen das dieser andere Ansatz halt auch andere Probleme mit sich bringt.


Zitat von »"Nexus"«

Zitat von »"dot"«

Wenn du die Objekte in einem vector ablegst passierts dir z.B. dass Pointer/Referenzen/Iteratoren drauf auf einmal ungültig sind wenn der vector wächst usw.
Und das passiert mit Zeigern, die man in einem std::vector ablegt, nicht?


Doch aber die Zeiger zeigen immer auf das selbe Objekt.

Zitat von »"Nexus"«

Zitat von »"dot"«

Ja natürlich, so lange du mit statischen Typen hantierst nicht. So lange is ja aber auch keine Polymorphie im Spiel ;)
Nicht direkt, aber man kann ja ohne weiteres Zeiger und Referenzen auf statische Objekte deklarieren. Inwiefern hat Polymorphie etwas damit zu tun, ob Objekte automatisch oder dynamisch angefordert werden?


Gar nicht. Ich hab auch nicht von dynamischer allokation etc. geredet sondern vom statischen/dynamischen Typ eines Objektes ;)
Ich meinte lediglich dass slicing erst ein Problem wird wenn Polymorphie im Spiel ist und polymorphe Typen im allgemeinen keine Wertsemantik haben sollten. Wenn du lediglich mit statischen Typen hantierst hast du das Problem natürlich nicht, dann machst du aber auch keinen Gebrauch von Polymorphie.

Wie gesagt, die Lösung mit statischer Typisierung und mehreren Containern ist für den geschilderten Anwendungsfall sicherlich eine gute Lösung, das will ich gar nicht bestreiten. Ich wollte nur ein paar der Konsequenzen davon ansprechen :)

27

08.12.2009, 22:38

Re: Templates vs. Interface

Zitat von »"Laguna"«

Im Grunde ja, aber hier die Frage: Kollision gehört zur Gamelogik, sie baut allerdings auf grafische Elemente (Bildgröße, Alphablending, Rotation) auf. Oder anders ausgedrückt: Durch eine strikte Trennung müsste man hier redundante Informationen speichern.
Okay, ich kenn dein Projekt zu wenig, um das zu beurteilen. Kann durchaus sein, dass sich die Trennung nicht vernünftig durchführen lässt. Ich habe jetzt an einen Fall gedacht, bei dem die Kollision grundsätzlich unabhängig von den Grafikdaten ist.

Zitat von »"Laguna"«

Diesbezüglich gabs auch mal einen informativen Thread auf Developia: Ein Character IST ein Sprite oder HAT ein Sprite?
Da gehts aber mehr um Vererbung vs. Aggregation. Und bei diesem Beispiel bin ich klar der Meinung, dass Vererbung falsch ist (und meist aus Faulheit und/oder Unerfahrenheit resultiert).

Du kannst auch Grafikdaten als Aggregation realisieren.

Zitat von »"Laguna"«

Probleme stellen sich halt erst ein, wenn man 2 Typen gegeneinander prüft, weil das von C++ nicht nativ unterstützt wird. (Gibts da zufällig ne Boost-Lösung für?)
Mir ist nichts bekannt, ich denke auch nicht, dass dynamisches Double-Dispatch ohne dynamic_cast und typeid (oder was Nachgebautem) funktioniert. Und diese beiden Operatoren sollte man eher sparsam einsetzen.

Ich finde es jedoch gar nicht so schlecht, wenn es eine Klasse gibt, welche die groben Befehle erteilt und mehrere Objekte aufeinander prüft. Also eine Art Managerklasse. Wenn jedes Spielobjekt selbst weiss, wie es auf die anderen zu reagieren hat, hast du einerseits ziemliche Abhängigkeiten, weil jedes Objekt über jedes andere Bescheid wissen muss. Andererseits wird es extrem schwierig, Dritte zu benachrichtigen. Stell dir vor, eine Rakete trifft ein Raumschiff und soll Kollateralschaden anrichten. Dazu muss bekannt sein, welche anderen Raumschiffe in der Nähe sind, was wiederum ein Zugriff auf die spielverwaltenden Container nötig macht.

Ich versuche, bei eigenen Projekten jede Klasse möglichst eigenständig und frei von unnötigen Abhängigkeiten zu schreiben. Ein Raumschiff hätte weder Kenntnis von anderen Raumschiffen, des umgebenden Raums, noch von seiner grafischen Repräsentation. Ich persönlich habe die Erfahrung gemacht, dass man auf diese Weise recht flexibel ist, weil wenig bestehende Beziehungen berücksichtigt werden müssen.

Zitat von »"Laguna"«

Was kann ich mir darunter vorstellen? Gibts ein kleines Code-Beispiel? :)
Okay, ich schau mal, ob ich gleich ein gutes Beispiel bringen kann.

28

08.12.2009, 22:56

Zitat von »"dot"«

Gar nicht. Ich hab auch nicht von dynamischer allokation etc. geredet sondern vom statischen/dynamischen Typ eines Objektes ;)
Ja, ich weiss, ich hab da was durcheinandergebracht. Aber wenigstens gleich gemerkt und editiert, aber du warst zu schnell. :p

Zitat von »"dot"«

Ich meinte lediglich dass slicing erst ein Problem wird wenn Polymorphie im Spiel ist und polymorphe Typen im allgemeinen keine Wertsemantik haben sollten. Wenn du lediglich mit statischen Typen hantierst hast du das Problem natürlich nicht, dann machst du aber auch keinen Gebrauch von Polymorphie.
Ich finde, auf oberster Ebene dürfen sie durchaus Wertsemantik haben. Sonst verfangen wir uns in Techniken von anderen Programmiersprachen (z.B. Clone()), was in C++ oft unnötig ist und eben auch Probleme bringt.

Die Polymorphie in tieferen Schichten wird durch die Wertsemantik zuoberst nicht beeinträchtigt. Um es zu verdeutlichen: Innerhalb der Klasse World hantiert man nur mit statischen Typen, dennoch ist Polymorphie gut möglich. Das Beispiel ist nicht das beste, sollte aber die Idee klar machen.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Object {}; // polymorph

class Enemy : public Object {};
class Tree : public Object {};

class GraphicManager
{
    public:
        void Draw(const Object& Obj);
};

class World
{
    public:
        void DrawEverything() const
        {
            MyGraphicMgr.Draw(MyEnemy);
            MyGraphicMgr.Draw(MyTree);
        }
    private:
        GraphicManager MyGraphicMgr;
        Enemy MyEnemy;
        Tree MyTree;
};


Zitat von »"dot"«

Wie gesagt, die Lösung mit statischer Typisierung und mehreren Containern ist für den geschilderten Anwendungsfall sicherlich eine gute Lösung, das will ich gar nicht bestreiten. Ich wollte nur ein paar der Konsequenzen davon ansprechen :)
Gut, dass du das machst, so wird die Diskussion vielfältiger. :)

Ich sag ja nicht, dass mein Ansatz keine Nachteile hat. Aber ich persönlich mag Trennung von Grafik und Logik, bis jetzt konnte ich das auch einigermassen gut durchsetzen, vielleicht ist das auf Dauer zu idealistisch. Ich habe auch noch nicht jahrelange Erfahrung in der Spieleprogrammierung hinter mir (zumindest nicht auf einem Wissensstand, auf dem ich über solche Dinge nachgedacht habe). ;)

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

29

08.12.2009, 22:57

Re: Templates vs. Interface

Zitat von »"Nexus"«

Zitat von »"Laguna"«

Diesbezüglich gabs auch mal einen informativen Thread auf Developia: Ein Character IST ein Sprite oder HAT ein Sprite?
Da gehts aber mehr um Vererbung vs. Aggregation. Und bei diesem Beispiel bin ich klar der Meinung, dass Vererbung falsch ist (und meist aus Faulheit und/oder Unerfahrenheit resultiert).


Ohne mir das Beispiel genau angeschaut zu haben muss ich das sofort unterschreiben ;)

Zitat von »"Nexus"«

Zitat von »"Laguna"«

Probleme stellen sich halt erst ein, wenn man 2 Typen gegeneinander prüft, weil das von C++ nicht nativ unterstützt wird. (Gibts da zufällig ne Boost-Lösung für?)
Mir ist nichts bekannt, ich denke auch nicht, dass dynamisches Double-Dispatch ohne dynamic_cast und typeid (oder was Nachgebautem) funktioniert. Und diese beiden Operatoren sollte man eher sparsam einsetzen.


Doch das geht ohne, genau das ist der Sinn von Double Dispatch. Allerdings wäre die Implikation davon in diesem Fall hier dass jedes Objekt wissen müsste wie es mit jedem anderen Objekt zu interagieren hat, was hier evtl. nicht sehr praktisch ist. Vielleicht aber auch schon, der Vorteil ist wieder dass man eben das Verhalten eines Objektes mit einem anderen in der Klasse des Objektes drin gekapselt hat. Hängt wieder "davon ab" ob das gut oder weniger gut ist.

Kleine Illustration wie das aussehen könnte:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Player;
class Enemy;

class GameObject
{
private:
  GameObject(const GameObject&);
  GameObject& operator =(const GameObject&);
public:
  GameObject() {}
  virtual ~GameObject() {}

  virtual void interact(GameObject& object) = 0;

  virtual void interact(Player& player) {}
  virtual void interact(Enemy& enemy) {}
};


class Player : public GameObject
{
public:
  void interact(GameObject& object)
  {
    object.interact(*this);
  }

  void interact(Enemy& enemy)
  {
    // Spieler tut irgendwas mit Gegner

  }
};


class Enemy : public GameObject
{
public:
  void interact(GameObject& object)
  {
    object.interact(*this);
  }

  void interact(Player& object)
  {
    // Gegner tut irgendwas mit Spieler

  }
};


Du kannst einfach zwei GameObjects miteinander interagieren lassen indem du object1.interact(object2); machst. Die Objekte selber wissen welchen konkreten Typ sie haben und rufen die entsprechende Methode des jeweils anderen Objektes auf (das is der "double dispatch" weils quasi "über zwei Ecken geht") in der das Verhalten implementiert ist.

30

08.12.2009, 23:39

Hey, diese Technik ist echt cool, kannte ich so gar nicht. Ich bin immer wieder erstaunt darüber, welche Möglichkeiten man in C++ hat, statische Polymorphie (Templates und Überladung) mit dynamischer zu kombinieren. Vielen Dank für die Erläuterung! :idea:

Vor nicht allzu langer Zeit habe ich die gleiche Erfahrung mit Templates gemacht. Es geht um einen Smart-Pointer, der durch polymorphe Zeiger Kopien des dynamischen Typs anfertigen kann, ohne eine Clone()-Funktion oder was Ähnliches zu erfordern. Ich war ziemlich beeindruckt... ;)

Ich kann dir den Code gerne schicken, falls du interessiert bist (er ist eben etwas grösser).

____
P.S. Ich habe etwas mit Double-Dispatch herumexperimentiert, konkret gehts um Schere-Stein-Papier. Hier der Code (Const-Correctness hab ich mal ignoriert):

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
struct Schere;
struct Stein;
struct Papier;

struct Taktik
{
    virtual bool Besiegt(Taktik* Other) = 0;
    virtual bool Besiegt(Schere* Other) = 0;
    virtual bool Besiegt(Stein* Other) = 0;
    virtual bool Besiegt(Papier* Other) = 0;

    virtual ~Taktik() {}
};

struct Schere : public Taktik
{
    virtual bool Besiegt(Taktik* Other)
    {
        return !Other->Besiegt(this);
    }
    
    virtual bool Besiegt(Schere* Other)
    {
        return false;
    }
    
    virtual bool Besiegt(Stein* Other)
    {
        return false;
    }
    
    virtual bool Besiegt(Papier* Other)
    {
        return true;
    }
};

struct Stein : public Taktik
{
    virtual bool Besiegt(Taktik* Other)
    {
        return !Other->Besiegt(this);
    }
    
    virtual bool Besiegt(Schere* Other)
    {
        return true;
    }
    
    virtual bool Besiegt(Stein* Other)
    {
        return false;
    }
    
    virtual bool Besiegt(Papier* Other)
    {
        return false;
    }
};

struct Papier : public Taktik
{
    virtual bool Besiegt(Taktik* Other)
    {
        return !Other->Besiegt(this);
    }
    
    virtual bool Besiegt(Schere* Other)
    {
        return false;
    }
    
    virtual bool Besiegt(Stein* Other)
    {
        return true;
    }
    
    virtual bool Besiegt(Papier* Other)
    {
        return false;
    }
};

int main()
{
    Taktik* t[3] = {new Schere, new Stein, new Papier};

    for (int j = 0; j < 3; ++j)
    {
        for (int i = 0; i < 3; ++i)
        {
            std::cout << t[j]->Besiegt(t[i]);
        }
        std::cout << std::endl;
    }

    for (int i = 0; i < 3; ++i)
    {
        delete t[i];
    }
}

Wird allerdings bei mehreren Typen recht schnell komplex. Wenn die Möglichkeit besteht, zur Kompilierzeit einen Dispatch zu vollziehen, kann man sich meist sehr viel Code sparen. Dennoch scheint mir diese Technik sehr mächtig. :)

Werbeanzeige