I'm writing a rogue-like game in C++ and have problems with double dispatch.
class MapObject {
virtual void collide(MapObject& that) {};
virtual void collide(Player& that) {};
virtual void collide(Wall& that) {};
virtual void collide(Monster& that) {};
};
and then in derived classes:
void Wall::collide(Player &that) {
that.collide(*this);
}
void Player::collide(Wall &that) {
if (that.get_position() == this->get_position()){
this->move_back();
}
}
And then I try to use the code:
vector<vector<vector<shared_ptr<MapObject>>>> &cells = ...
where cells is created like:
objs.push_back(make_shared<Monster>(pnt{x, y}, hp, damage)); //and other derived types
...
cells[pos.y][pos.x].push_back(objs[i]);
And when I try to collide player and wall:
cells[i][j][z]->collide(*cells[i][j][z+1]);
The player collides with the base class, but not with the wall. What am I doing wrong?
This is more complex than just solving your problem. You are doing manual double dispatch, and you have bugs. We can fix your bugs.
But the problem you have isn't your bugs, it is the fact you are doing manual double dispatch.
Manual double dispatch is error prone.
Every time you add a new type, you have to write O(N) new code, where N is the number of existing types. This code is copy-paste based, and if you make mistakes they silently continue to mis-dispatch some corner cases.
If you continue to do manual double dispatch, you'll continue to have bugs whenever you or anyone else modifies the code.
C++ does not provide its own double dispatch machinery. But with c++17 we can automate the writing of it
Here is a system that requires linear work to manage the double dispatch, plus work for each collision.
For each type in the double dispatch you add a type to
pMapType. That's it, the rest of the dispatch is auto-written for you. Then inherit your new map typeXfromcollide_dispatcher<X>.If you want two types to have collision code, write a free function
do_collide(A&,B&). The one easier in thepMapTypevariant should beA. This function must be defined before bothAandBare defined for the dispatch to work.That code gets run if either
a.collide(b)orb.collide(a)is run, whereAandBare the dynamic types ofaandbrespectively.You can make
do_collidea friend of one or the other type as well.Without further ado:
Live example.
While the plumbing here is complex, it does mean that you aren't manually doing any double-dispatching. You are just writing endpoints. This reduces the number of places you can have corner-case typos.
Test code:
Output is:
You could also create a central typedef for
std::variant<Player*, Wall*, Monster*>and havemap_type_indexuse that central typedef to determine its ordering, reducing the work to add a new type to the double dispatch system to adding a type at a single location, implementing the new type, and forward declaring the collision code that is supposed to do something.What more, this double dispatch code can be made inheritance friendly; a derived type from
Wallcan dispatch toWalloverloads. If you want this, you have to makecollide_dispatchermethod overloads non-final, allowingSpecialWallto reoverload them.This is c++17, but current versions of every major compiler now supports what it needs. Everything can be done in c++14 or even c++11 but it gets much more verbose and may require boost.
While it takes a linear amount of code to define what happens, the compiler will generate a quadratic amount of code or static table data to implement the double dispatch. So take care before having 10,000+ types in your double dispatch table.
If you want
MapObjectto be concrete, split off the interface from it and removefinalfrom the dispatcher and addMapObjecttopMapTypelive example.
as you want
Playerto descend fromMapObjectyou have to use theBaseargument ofcollide_dispatcher: