Ich werde eine Implementierung skizzieren, die nicht auf Doppelversand angewiesen ist. Stattdessen verwendet es eine Tabelle, in der alle Funktionen registriert sind. Auf diese Tabelle wird dann über den dynamischen Typ der Objekte zugegriffen (als Basisklasse übergeben).
Zuerst haben wir einige Beispielformen. Ihre Typen sind in einem enum class
eingetragen. Jede Formklasse definiert einen MY_TYPE
als ihren jeweiligen Enum-Eintrag. Darüber hinaus müssen sie rein virtuelle type
Methode der Basisklasse implementieren:
enum class ObjectType
{
Circle,
Box,
_Count,
};
class PhysicsObject
{
public:
virtual ObjectType type() const = 0;
};
class Circle : public PhysicsObject
{
public:
static const ObjectType MY_TYPE = ObjectType::Circle;
ObjectType type() const override { return MY_TYPE; }
};
class Box : public PhysicsObject
{
public:
static const ObjectType MY_TYPE = ObjectType::Box;
ObjectType type() const override { return MY_TYPE; }
};
Als nächstes müssen Sie Ihre Kollisionsauflösung Funktionen, müssen sie je nach den Formen umgesetzt werden, natürlich.
void ResolveCircleCircle(Circle* c1, Circle* c2)
{
std::cout << "Circle-Circle" << std::endl;
}
void ResolveCircleBox(Circle* c, Box* b)
{
std::cout << "Circle-Box" << std::endl;
}
void ResolveBoxBox(Box* b1, Box* b2)
{
std::cout << "Box-Box" << std::endl;
}
Beachten Sie, dass wir nur Circle
-Box
hier keine Box
-Circle
, wie ich ihre Kollision übernehmen in der gleichen Art und Weise erfasst wird.Mehr zu dem Kollisionsfall Box
- Circle
später.
Nun zu dem Kernteil, die Funktionstabelle:
std::function<void(PhysicsObject*,PhysicsObject*)>
ResolveFunctionTable[(int)(ObjectType::_Count)][(int)(ObjectType::_Count)];
REGISTER_RESOLVE_FUNCTION(Circle, Circle, &ResolveCircleCircle);
REGISTER_RESOLVE_FUNCTION(Circle, Box, &ResolveCircleBox);
REGISTER_RESOLVE_FUNCTION(Box, Box, &ResolveBoxBox);
Der Tisch selbst ist ein 2D-Array aus std::function
s. Beachten Sie, dass diese Funktionen Zeiger auf PhysicsObject
akzeptieren, nicht die abgeleiteten Klassen. Dann verwenden wir einige Makros für die einfache Registrierung. Natürlich könnte der entsprechende Code von Hand geschrieben werden und ich bin mir durchaus der Tatsache bewusst, dass die Verwendung von Makros typischerweise als schlechte Angewohnheit betrachtet wird. Meiner Meinung nach sind diese Dinge jedoch genau das, wofür Makros gut sind, und solange Sie aussagekräftige Namen verwenden, die Ihren globalen Namensraum nicht durcheinander bringen, sind sie akzeptabel. Beachten Sie nochmals, dass nur Circle
- Box
registriert ist, nicht umgekehrt.
Nun zur Phantasie Makro:
#define CONCAT2(x,y) x##y
#define CONCAT(x,y) CONCAT2(x,y)
#define REGISTER_RESOLVE_FUNCTION(o1,o2,fn) \
const bool CONCAT(__reg_, __LINE__) = []() { \
int o1type = static_cast<int>(o1::MY_TYPE); \
int o2type = static_cast<int>(o2::MY_TYPE); \
assert(o1type <= o2type); \
assert(!ResolveFunctionTable[o1type][o2type]); \
ResolveFunctionTable[o1type][o2type] = \
[](PhysicsObject* p1, PhysicsObject* p2) { \
(*fn)(static_cast<o1*>(p1), static_cast<o2*>(p2)); \
}; \
return true; \
}();
Das Makro definiert einen eindeutig benannte Variable (mit der Zeilennummer), aber diese Variable dient lediglich um den Code zu bekommen innerhalb der Initialisierung Lambda-Funktion ausgeführt werden. Die Typen (aus der ObjectType
enum) der übergebenen zwei Argumente (das sind die konkreten Klassen Box
und Circle
) werden genommen und verwendet, um die Tabelle zu indizieren. Der gesamte Mechanismus geht davon aus, dass es eine Gesamtordnung für die Typen gibt (wie in der Enumeration definiert), und prüft, ob für die Argumente in dieser Reihenfolge tatsächlich eine Funktion für die Kollision Circle
10 registriert ist. Die assert
sagt Ihnen, wenn Sie es falsch machen (versehentlich Registrierung Box
- Circle
). Dann wird eine Lambda-Funktion in der Tabelle für dieses spezielle Typenpaar registriert. Die Funktion selbst nimmt zwei Argumente vom Typ PhysicsObject*
und wandelt sie vor dem Aufruf der registrierten Funktion in die konkreten Typen um.
Als nächstes können wir uns ansehen, wie die Tabelle dann verwendet wird. Es ist nun leicht eine einzige Funktion zu implementieren, die Kollision von zwei beliebigen PhysicsObject
s überprüft:
void ResolveCollision(PhysicsObject* p1, PhysicsObject* p2)
{
int p1type = static_cast<int>(p1->type());
int p2type = static_cast<int>(p2->type());
if(p1type > p2type) {
std::swap(p1type, p2type);
std::swap(p1, p2);
}
assert(ResolveFunctionTable[p1type][p2type]);
ResolveFunctionTable[p1type][p2type](p1, p2);
}
Es nimmt die dynamischen Typen des Arguments und übergibt sie an die Funktion für die jeweiligen Typen innerhalb der ResolveFunctionTable
registriert. Beachten Sie, dass die Argumente ausgetauscht werden, wenn sie nicht in der richtigen Reihenfolge sind. Somit können Sie ResolveCollision
mit Box
und Circle
aufrufen und intern die für Circle
- Box
registrierte Kollision aufrufen.
Schließlich werde ich ein Beispiel geben, wie man es benutzt:
int main(int argc, char* argv[])
{
Box box;
Circle circle;
ResolveCollision(&box, &box);
ResolveCollision(&box, &circle);
ResolveCollision(&circle, &box);
ResolveCollision(&circle, &circle);
}
Leicht ist es nicht? Siehe this für eine funktionierende Implementierung der obigen.
Nun, was ist der Vorteil dieses Ansatzes? Der obige Code ist im Grunde alles, was Sie benötigen, um eine beliebige Anzahl von Formen zu unterstützen. Angenommen, Sie fügen eine Triangle
hinzu. Alles, was Sie tun müssen, ist:
Triangle
zum ObjectType
Enum einen Eintrag hinzufügen.
- Implementieren Sie Ihre
ResolveTriangleXXX
Funktionen, aber Sie müssen dies in allen Fällen tun.
- sie auf den Tisch mit dem Makro registrieren:
Das ist es. Es müssen keine weiteren Methoden zu PhysicsObject
hinzugefügt werden, es müssen keine Methoden in allen vorhandenen Typen implementiert werden.
Ich bin mir einiger 'Fehler' dieses Ansatzes bewusst, wie die Verwendung von Makros, die eine zentrale enum
aller Typen haben und sich auf eine globale Tabelle verlassen. Der letztere Fall könnte zu Problemen führen, wenn die Formklassen in mehrere gemeinsam genutzte Bibliotheken integriert sind. Meiner bescheidenen Meinung nach ist dieser Ansatz jedoch ziemlich praktisch (außer für sehr spezielle Anwendungsfälle), da er nicht zur Codeexplosion führt, wie dies bei anderen Ansätzen der Fall ist (z. B. Doppelversand).
Schauen Sie sich [double dispatch] (https://en.wikipedia.org/wiki/Double_dispatch) an. – Jarod42
Das Problem ist offensichtlich - keine Überladung benötigt 2 'PhysicsObject' als Argumente, daher der Fehler. Stellen Sie entweder die Überladung bereit oder verwenden Sie 'dynamic_cast'. –
Sicher kann ich eine Überladung erstellen, die zwei PhysicsObjects benötigt, aber ich weiß immer noch nicht, welche Art von Physik-Objekt sie sind, also habe ich das gleiche Problem. Danke @ Jarod42, werde es jetzt lesen. – Beetroot