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 typeX
fromcollide_dispatcher<X>
.If you want two types to have collision code, write a free function
do_collide(A&,B&)
. The one easier in thepMapType
variant should beA
. This function must be defined before bothA
andB
are defined for the dispatch to work.That code gets run if either
a.collide(b)
orb.collide(a)
is run, whereA
andB
are the dynamic types ofa
andb
respectively.You can make
do_collide
a 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_index
use 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
Wall
can dispatch toWall
overloads. If you want this, you have to makecollide_dispatcher
method overloads non-final
, allowingSpecialWall
to 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
MapObject
to be concrete, split off the interface from it and removefinal
from the dispatcher and addMapObject
topMapType
live example.
as you want
Player
to descend fromMapObject
you have to use theBase
argument ofcollide_dispatcher
: