Evert Heylen
Evert Heylen

Reputation: 1061

Event-based Game engine based on polymorphism of Entities

I would like to create a simple framework for throwing and catching events in a game. Events could be things like a Collision which (according to the type) can take several arguments (note that every Event type may take another amount of arguments, not just two as in the example).

I would then like to implement functions/classes/... to deal with a Collision, based on polymorphism. This example should illustrate the problem:

#include <iostream>
#include <vector>

class Entity {};

class Player: public Entity {};

class Bomb: public Entity {
public:
    bool exploded;
};

class MineSweeper: public Entity {};


// For now, I only included Collisions, but I eventually want to extend it to
// more types of Events too (base class Event, Collision is derived class)

void onCollision(Player* p, Bomb* b) {
    if (! b->exploded) {
        std::cout << "BOOM";
        b->exploded = true;
    }
}

void onCollision(Entity* e, Entity* f) {
    std::cout << "Unhandled collision\n";
}

// Possibility for Collision between Minesweeper and Bomb later


class Game {
public:
    std::vector<Entity*> board;  // some kind of linear board

    Game() {
        board = {new Player, new Bomb, new MineSweeper};
    }

    void main_loop() {
        onCollision(board[0], board[1]); // player and bomb!
        onCollision(board[1], board[2]);
    }
};


int main() {
    Game g;
    g.main_loop();
}

Note that I understand perfectly well why the above code doesn't work as intended, I included this example solely to illustrate my problem better.

The above example uses functions for the events, but I'm perfectly fine with classes or any other solution that is maintainable.

I hope it is clear that I would like C++ to decide which event handler to use based on the types of the arguments (presumably at runtime).

My question: How can I do this in C++? An example would be appreciated.

(not my question: fix my code please)

Upvotes: 0

Views: 732

Answers (3)

Aymar Fisherman
Aymar Fisherman

Reputation: 388

Maybe a good structure for your case could be something like this:

class Entity{
public:
    virtual int getType() = 0;
};

enum EntityTypes {
    ACTOR,
    BOMB,
    MINESWEEPER,
};

class Actor : public Entity{
public:
    virtual int getType() {return int(ACTOR);}

    void applyDamage() {
        std::cout << "OUCH";
    }
};

class Bomb : public Entity{    
public:
    Bomb() : exploded(false) {}

    virtual int getType() {return int(BOMB);}

    void explode() {
        this->exploded = true;
    }

    bool isExploded() {
        return this->exploded;
    }

protected:
    bool exploded;
};

class MineSweeper : public Entity{
public:
    virtual int getType() {return int(MINESWEEPER);}
};

class CollisionSolver {
public:
    virtual solve(Entity* entity0, Entity* entity1) = 0;
};

class ActorBombCollisionSolver : public CollisionSolver {
public:
    virtual solve(Entity* entity0, Entity* entity1) {
        Actor* actor;
        Bomb* bomb;
        if (entity0->getType() == ACTOR && entity1->getType() == BOMB) {
            actor = static_cast<Actor*>(entity0);
            bomb = static_cast<Bomb*>(entity1);
        }else if (entity1->getType() == ACTOR && entity0->getType() == BOMB) {
            actor = static_cast<Actor*>(entity1);
            bomb = static_cast<Bomb*>(entity0);
        }else {
            //throw error;
        }
        if (!bomb->isExploded()) {
            bomb->explode();
            actor->applyDamage();
        }
    }
};

class CollisionDispatcher {
public:
    CollisionDispatcher() {
        CollisionSolver* actorBombCollisionSolver = new ActorBombCollisionSolver;
        this->solvers[ACTOR][BOMB] = actorBombCollisionSolver;
        this->solvers[BOMB][ACTOR] = actorBombCollisionSolver;     

        // this part wouldn't be necessary if you used smart pointers instead of raw... :)
        this->solvers[BOMB][MINESWEEPER] = 0;     
        this->solvers[MINESWEEPER][BOMB] = 0;     
        this->solvers[ACTOR][MINESWEEPER] = 0;     
        this->solvers[MINESWEEPER][ACTOR] = 0;     
    }

    void dispatchCollision(Entity* entity0, Entity* entity1) {
        CollisionSolver* solver = this->solvers[entity0->getType()][entity1->getType()];
        if (!solver) {
            return;
        }
        solver->solve(entity0, entity1);
    }

protected:
    unordered_map<int, unordered_map<int, CollisionSolver*> > solvers;
};

class Game {
public:
    std::vector<Entity*> board;  // some kind of linear board    

    Game() : dispatcher(new CollisionDispatcher) 
    {
        board = {new Player, new Bomb, new MineSweeper};
    }

    void main_loop() {
        dispatcher->dispatchCollision(board[0], board[1]);
        dispatcher->dispatchCollision(board[0], board[2]);
        dispatcher->dispatchCollision(board[1], board[2]);
    }
protected:
    CollisionDispatcher* dispatcher;
};


int main() {
    Game g;
    g.main_loop();
}

This way you can easily add new collision solvers, just define the class, and register t in the CollisionDispatcher constructor.

If you use smart pointers you won't need to set zeroes in the map entries not registered, but if you use raw pointers you have to set them to zero OR use unordered_map::find method instead of just grabbing the solver using operator []

Hope it helps!

Upvotes: 0

Evert Heylen
Evert Heylen

Reputation: 1061

user2864740 provided enough clues for me to find a solution myself. Multiple dispatch was indeed the missing piece.

The following code works as intended, making use of dynamic_cast to dispatch correctly.

#include <iostream>
#include <vector>

class Entity {
    virtual void please_make_this_polymorphic() {}
    // although this function does nothing, it is needed to tell C++ that it
    // needs to make Entity polymorphic (and thus needs to know about the type
    // of derived classes at runtime).
};

class Player: public Entity {};

class Bomb: public Entity {
public:
    bool exploded;
};

class MineSweeper: public Entity {};

// For now, I only included Collisions, but I eventually want to extend it to
// more types of Events too (base class Event, Collision is derived class)

void onCollision(Player* p, Bomb* b) {
    if (!b->exploded) {
        std::cout << "BOOM\n";
        b->exploded = true;
    }
}

void onCollision(Entity* e, Entity* f) {
    std::cout << "Unhandled collision\n";
}

void dispatchCollision(Entity* e, Entity* f) {
    Player* p = dynamic_cast<Player*>(e);
    Bomb* b = dynamic_cast<Bomb*>(f);
    if (p != nullptr && b != nullptr) {
        onCollision(p, b);  // player and bomb
    } else {
        onCollision(e, f);  // default
    }
}


class Game {
public:
    std::vector<Entity*> board;  // some kind of linear board

    Game() {
        board = {new Player, new Bomb, new MineSweeper};
    }

    void main_loop() {
        dispatchCollision(board[0], board[1]);  // player and bomb
        dispatchCollision(board[1], board[2]);
    }
};


int main() {
    Game g;
    g.main_loop();
}

Although it works, I'd like to point out some problems with this code:

  • Manual editing of dispatchCollision needed when adding new Collisions.
  • Currently, the dispatcher using a simple kind of rule-based system. (Does it fit rule 1? What about rule 2? ...) When adding loads of different functions it needs to dispatch, that may have an impact on the performance.
  • A collision between A and B should be the same as a collision between B and A, but that isn't properly handled yet.

Solving these problems is not necessarily in the scope of this question IMHO.

Also, the example given should work just as well for more than 2 arguments. (Multiple dispatch, not just double dispatch.)

Upvotes: 1

c-smile
c-smile

Reputation: 27460

You should decide first what event subscription model you need. It could be signal/slot mechanism and you can find plenty of libraries: https://code.google.com/p/cpp-events/ , http://sigslot.sourceforge.net/ and the like. Or it could be bubbling/sinking events like in HTML DOM when event gets propagated on parent/child chain ( from event source element to its containers). Or even other schema.

It is quite easy to create whatever you need with std::function holders in modern C++.

Upvotes: 0

Related Questions