1
\$\begingroup\$

I'm working on the event system for my engine, and have some trouble deciding what types of parameters my events should have to be both generic and specific at the same time. I want to be able to notify entities in the world about anything that is happening using something similar to the observer pattern, but merely the notification message itself is not enough, some messages need to carry additional information such as location, radius, damage, and so on. How do I encapsulate all these different kinds of parameters in one event type without making it very bloated like it would be if every event held a field for every type of parameter?

Some examples to show what I would like to achieve and what I have considered so far:

struct Event {
    TYPE     type;
    int      x, y, z;
    unsigned radius;
    float    damage;
    unsigned cost;
    float    healing;
    ...
void Observer::notify(Event event) {
    if (event.type == explosion)
        handle_explotion(event.x, event.y, event.z, event.damage);
    else if (event.type == healing)
        add_hitpoints(event.healing);
    ...

That would make the Event type very bloated, and the handling gets tedious quickly. I considered having fewer parameters, but they would remain unspecified:

struct Event {
    int arg1, arg2, arg3, arg4;
    const char * str1, str2, str3;
    bool b1, b2, b3;
    ...

This would cut down on the amount of parameters because all types are condensed into the same few fields. However from a user point of view this approach is much worse because now I have to remember what each parameter was supposed to represent every time; was arg3 the radius or the damage in an explosion? To fix this maybe the parameters could live in a std::map, but that still makes handling obnoxious:

struct Event {
    std::map<const std::string, int> int_data;
    std::map<const std::string, float> float_data;
    ...
void Observer::notify(Event event) {
    if (event.type == collision)
        resolve_collision(event.int_data["x"], event.int_data["y"],
                          event.int_data["z"], event.int_data["width"],
                          event.int_data["height"]
    ...

I considered making the event hold some kind of general data field that can be cast to the appropriate type:

struct Event {
    void * data;
    TYPE type
};
void Observer::notify(Event event) {
    if (event.type == weapon_augment)
        modify_weapon((Weapon *)(event.data));

This makes instantiating and deleting the event somewhat more tedious, but at least it allows me to use specific types for every event handler.

What is the conventional approach to solving this problem? Even better, what is your own ingenious solution? Thanks.

\$\endgroup\$
4
\$\begingroup\$

Don't shove all the events into one type. You are using C++, a language that supports Abstract Data Types. Use them!

Inheritance/Interfaces

struct Event {
    TYPE type;
    virtual void Handle() = 0;
};

struct DamageEvent : Event {
    float damage;
    int target;
    void Handle() final {
        FindTarget(target).TakeDamage(damage);
    }
};

void Observer::notify(Event event) {
    if (/* event.type needs to be handled here */) {
        event.Handle();
    }
}

One thing you need to watch out for here is object slicing. You'll need to store pointers to events, instead of by value in containers; preferably using smart pointers e.g. std::vector<std::unique_ptr<Event>>

Unions

struct Event {
    TYPE type;
    union {
        struct {
            float damage;
            int target;
        } damage;
        struct {
            int x, y, z;
            unsigned radius;
            float damage;
        } explosion;
        // ...
    };
};

void Observer::notify(Event event) {
    switch (event.type) {
    case EVENT_DAMAGE:
        FindTarget(event.damage.target).TakeDamage(event.damage.damage);
        break;
    // ...
    }
}

Note that by using a union, those data members take up the same memory, so adding more event types does not increase the size of struct Event. Instead, it is as big as it needs to be, to fit the largest type of event. They're useful if you need to avoid dynamic memory for some reason. Outside of C where it is used to fake inheritance, it's kind of rare in C++. But at least you're dealing with real variable names instead of argN.

\$\endgroup\$
  • \$\begingroup\$ +1 for unions, -1 for inheritance - not much point in making logistics of the Event struct hard and requiring memory allocation and management where none should be necessary. It's a shame that C++ doesn't allow generating the latter from the former automatically, though. \$\endgroup\$ – snake5 Jul 2 '15 at 8:09
1
\$\begingroup\$

Another way you could go about it by using a "generic" map for your parameters:

int HashStringToInt(const std::string& aStringToHash) {
  // Hash your string here
}

union Value 
{
  int _int;
  double _double;
  bool _bool;
};

struc Event
{
  int _id;
  std::map<int, Value> _parameters;
};

void
boom()
{
  static const int EVENT_EXPLOSION = HashStringToInt( "EVENT_EXPLOSION" );
  static const int PARAM_X         = HashStringToInt( "PARAM_X"      );
  static const int PARAM_Y         = HashStringToInt( "PARAM_Y"      );
  static const int PARAM_Z         = HashStringToInt( "PARAM_Z"      );
  static const int PARAM_DAMAGE    = HashStringToInt( "PARAM_DAMAGE" );

  Event event;
  event._id = EVENT_EXPLOSION;
  Value param;
  param._double = 2.5;
  event._parameters[PARAM_X] = param;
  param._double = 1.4;
  event._parameters[PARAM_Y] = param;
  param._double = 0.0;
  event._parameters[PARAM_Z] = param;
  param._double = 200.0;
  event._parameters[PARAM_DAMAGE] = param;

  // fire your event
}

void Observer::notify(Event event) 
{
  static const int EVENT_EXPLOSION = HashStringToInt( "EVENT_EXPLOSION" );
  static const int PARAM_X         = HashStringToInt( "PARAM_X"      );
  static const int PARAM_Y         = HashStringToInt( "PARAM_Y"      );
  static const int PARAM_Z         = HashStringToInt( "PARAM_Z"      );
  static const int PARAM_DAMAGE    = HashStringToInt( "PARAM_DAMAGE" );

  if ( event._id == EVENT_EXPLOSION )
  {
    // fetch the values back from the event parameters map and 
    // process the explosition
  }
}
  • I'm using hashed strings because comparisons are faster between ints than between strings.
  • You can pas the events by copy, by ref, by unique_ptr, by shared_ptr, whatever suits your needs.

Pros of this way of doing it:

  • very loosely coupled events and parameters: there is absolutely no link between the sender and the receiver of the event, read: you don't need to include a header that describes the event structure in the event sender and the event receiver.
  • Events are copyable (off course, there is a cost)
  • You get only what you need (you don't get what you don't need)

Cons:

  • using strings as basis for parameters and events names may cause some small issues if you misspell some values
  • wrongly using the union may get unexpected values (put an int, get a double.. you'll mess up in this case; please note: this is an issue with the union, not with the design); take a look at boost::variant for an improvement on this
  • your hash algorithm has to be good enough to avoid collisions in event names, and in parameter names within an event (you started by using strings, if you're comfortable with the overhead, you could use straight strings instead of hashes)

As you see, there seems to be more cons than pros, but the first pro is a real one, IMHO, so it's worth considering :)

\$\endgroup\$
  • \$\begingroup\$ That's an interesting solution, I would not have thought of hashing the event id's, I would have tried to use enums or just strings instead which doesn't include the pros. However it looks like a lot of boilerplate code to create and to unpack an event in this style, and that is something I wanted to avoid. \$\endgroup\$ – Anonymous Entity Jul 2 '15 at 15:28
  • \$\begingroup\$ @user11177 I'm not sure there is technically a lot of overhead into using it like that (as in: I did not look into it yet), but yes, there is more to type. Using enums for event types has the inconvenience to having to share a single file between two 'entities', which is against the idea of decoupling things, and it is why I thought about ids like that. \$\endgroup\$ – Alexandre Vaillancourt Jul 2 '15 at 15:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.