Event system

posted in trill41 for project ABx
Published August 24, 2019
C++
Advertisement

When I realized that the classes are getting really big and complicated, I splitted them into smaller classes. So the Actor class doesn't do everything anymore, but has some components that do the work instead.

A Common problem with this approach is, how do Compositors (in this case the Actor) and Components communicate? Some might say they shouldn't communicate at all, use something like an ECS etc. I looked at ECS et al., and they may be fine when you design your application around it, but I didn't.

Up to now I just friend'ed everything and called private methods, but that doesn't seem to be very elegant.

Another approach is some event/messaging system. So interested parties subscribe to interesting events, and other may trigger those events. There are many such event systems out there, but all seemed to be too heavy for me, so I made a minimal implementation of such a system.

I think this implementation has some Pro's and many Con's:

Pros

  • Type safety
  • Event function can have any signature even with return value. The `CallOne()` and `CallAll()` functions returns the same type as the called function (or a vector of it).
  • You must define at compile time the signatures of functions your a going to call, and you can not call anything else. You get a compile error when a function signature is not found.
  • A called function may be a `std::function` or Lambda.
  • No inheritance needed.
  • Single header file `events.h`, only ~110 lines.
  • Minimal run time overhead.

Cons

  • Function signature must be passed to each call of `Subscribe()`, `CallOne()` and `CallAll()`.
  • You must define at compile time the signatures of functions your a going to call
  • If you do anything wrong (e.g. wrong function signature), you get weird error messages
  • Increased compile time, because a lot is done at compile time. But what you can do at compile time, doesn't have to be done at run time.
  • You can not unsubscribe from events :(.

Examples

Lambda


sa::Events<
    int(int, int)
> events;
events.Subscribe<int(int, int)>(1, [](int i, int j) -> int
{
    return i * j;
});
auto result = events.CallOne<int(int, int)>(1, 2, 3);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 6);

Lambda 2


sa::Events<
    int(int, int)
> events;
auto func = [](int i, int j) -> int
{
    return i + j;
};
events.Subscribe<int(int, int)>(2, func);
auto result = events.CallOne<int(int, int)>(2, 4, 5);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 9);

Event not found

When an event does not exist or nobody subscribed to the event it returns a default value, e.g. `0` for an `int`:


sa::Events<
    int(int, int)
> events;
events.Subscribe<int(int, int)>(1, [](int i, int j) -> int
{
    return i * j;
});
auto result = events.CallOne<int(int, int)>(2, 2, 3);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 0);

Methods


class Foo
{
private:
    sa::Events<
        int(int, int)
    > events;
    int Bar(int i, int j)
    {
        return i * j;
    }
public:
    Foo()
    {
        events.Subscribe<int(int, int)>(1, std::bind(&Foo::Bar, this, std::placeholders::_1, std::placeholders::_2));
    }
    int DoBar(int i, int j)
    {
        return events.CallOne<int(int, int)>(1, i, j);
    }
};

Foo foo;
auto result = foo.DoBar(3, 2);
assert(result == 6);

Different signatures


sa::Events<
    int(int, int),
    bool(int),
    void(void)
> events;
events.Subscribe<int(int, int)>(1, [](int i, int j) -> int
{
    return i * j;
});
events.Subscribe<bool(int)>(2, [](int i) -> bool
{
    return i != 0;
});
events.Subscribe<void(void)>(3, []()
{
    std::count << "No arguments :(" << std::endl;
});

auto result = events.CallOne<int(int, int)>(1, 4, 5);
static_assert(std::is_same<decltype(result), int>::value);
assert(result == 20);

auto result2 = events.CallOne<bool(int)>(2, 5);
static_assert(std::is_same<decltype(result2), bool>::value);
assert(result2 == true);

// void
events.CallOne<void(void)>(3);

Multiple subscribers


sa::Events<
    void(const std::string&)
> events;
events.Subscribe<void(const std::string&)>(1, [](const std::string& s)
{
    std::cout << "Subscriber 1 " << s << std::endl;
});
events.Subscribe<void(const std::string&)>(1, [](const std::string& s)
{
    std::cout << "Subscriber 2 " << s << std::endl;
});

events.CallAll<void(const std::string&)>(1, "Hello Subscribers!");
Should print:

Subscriber 1 Hello Subscribers!
Subscriber 2 Hello Subscribers!

Conclusion

I was able to get rid of many virtual functions and the classes got lighter. But the usage is a bit cumbersome, because you always have to pass the function signature of the event as template argument, but you get type safety in return.

Download

Get it from the Github respository if you are interested (MIT).

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement

Latest Entries

Advertisement