SiO2 EventBus vs. Guava EventBus

Hi @pspeed

I’ve come across Google Guavas EventBus, which seems really neatly done, but then I noticed your EventBus in SiO2 - and it looks like the idea is somewhat the same (except in Guava there’s an annotation in the evenhandler instead of searching for method names). Is there any specific usecase for the SiO2 EventBus over the Guava one ?

There isn’t much documentation on SiO2 events, so I’m merely curious, because your designs admittedly always hold some sort of genius. flatter over and out

@pspeed’s architectural patterns always involve method reflection. It’s much more elegant because it’s a late binding pattern that allows for extensions of the framework through configuration instead of coding. As for performance, the first call gets a slight performance hit but thanks to Java’s internal cache subsequent calls are very fast. The reflection pattern has been around for decades and has proven very effective in late binding configuration driven frameworks.

They are a bit different in other ways also.

Guava hinges only off of the event class. SiO2 hinges on the event type object. So you can have several event types that use the same event context object. For example, login/logoff might both use a User object as their context. In guava’s event bus you would have to wrap those in an event and have registered twice to get them.

In addition, SiO2’s event bus also supports traditional listeners. This can be nice if you have your own dispatch mechanism beyond the listener and don’t want to have to know ahead of time all of the different listeners you will need (ie: 90 different methods in Guava). For example, if you are distributing certain events over the network… one traditional listener can do that.

Let’s compare in more specific detail some things I think it’s worth pointing out.

From Guava’s javadocs: (EventBus (Guava: Google Core Libraries for Java 22.0 API))

Receiving Events

To receive events, an object should:

  1. Expose a public method, known as the event subscriber, which accepts a single argument of the type of event desired;
  2. Mark it with a Subscribe annotation;
  3. Pass itself to an EventBus instance’s register(Object) method.

In SiO2, you can register regular listeners or you can register an object with the appropriately named methods based on your event type. These methods do not need to be public (which is good because you almost never want to expose such methods as a public API).

So, if you have:

EventType<Player> joined = EventType.create("PlayerJoined", Player.class);
EventType<Player> joining = EventType.create("PlayerJoining", Player.class);
EventType<Player> leaving = EventType.create("PlayerLeaving", Player.class);
EventType<Player> left = EventType.create("PlayerLeft", Player.class);

Then you can have one object handle all of those:

class MyPlayerHandler {
    private void onPlayerJoined( Player player ) {
    ....
    }
    public void playerJoining( Player player ) {
    ....
    }
    protected void playerLeaving( Player player ) {
    ....
    }
    private void onPlayerLeft( Player player ) {
    ....
    }
}

Note: in that example I gave a bunch of different ways that the method naming and access will work. I don’t recommend doing that for real, it’s an example.

For me, it’s quite common to define protected or private methods right on the class that wants to know about stuff. (Say, an app state) without exposing them publicly. Otherwise, the typical pattern would be to create an inner class that you register… and quite often that’s just going to forward to internal private methods anyway.

Or in the not-app-state example, maybe I have a chat box that wants to watch player events. I don’t have to create a separate object, I can just put protected or private methods right on my class.

To register for the above events in SiO2’s event bus, you’d do something like:
EventBus.addListener(instanceOfMyPlayerHandler, joined, joining, leaving, left);

In Guava, I’d have to have four separate annotated methods, four separate event wrapper classes, and four separate calls to register().

Other differences, from Guava’s javadocs:

Subscribers should not, in general, throw. If they do, the EventBus will catch and log the exception.

In SiO2, events that throw exceptions are wrapped in an ErrorEvent and redispatched. So you could have some listeners watching for these erroring events… log them, display them in a UI, whatever.

Other difference, from Guava’s javadocs:

Dead Events

If an event is posted, but no registered subscribers can accept it, it is considered “dead.” To give the system a second chance to handle dead events, they are wrapped in an instance of DeadEvent and reposted.

SiO2’s event bus will return true/false from the publishEvent() method if the message was delivered or not. It’s a stylistic difference but I felt it more likely that the thing publishing might care (or not) if their event was delivered rather than that being a globally handlable state. (Note: the static publish() seems to not return the boolean… I think that’s a bug.)

Which brings me to another difference, SiO2’s EventBus can be used as instances like Guava’s but there is also a static singleton that the whole application can easily use to coordinate/register for events. To me, it’s almost always the case (and certainly in games) that something like a decoupled event bus would want wide exposure rather than having an instance passed all over the application. For application servers and stuff, maybe not (but then you can also have separate instances)… but for games, certainly one static one is super convenient.

This is one of the very very few cases where I think I did better than Guava in the flexibility department for very little extra complexity. Though note that I didn’t repurpose my API for an AsyncEventBus like they did… but still I don’t think that matters.

For games, I think my pattern is superior. But I would, wouldn’t I? :slight_smile:

Edit: added a javadoc link for Guava since I quoted it. Bolded the SiO2 references so it is easier to distinguish when I’m talking about one library or the other…and highlighting SiO2 since that’s what we are talking about.

Edit 2: for completeness, here is SiO2’s code with relatively sparse javadoc for EventBus: https://github.com/Simsilica/SiO2/blob/master/src/main/java/com/simsilica/event/EventBus.java

ie: sparse because there is no nice large over-arching description (yet) like for Guava but the methods are well documented and there is a class level description.

3 Likes

You once told me that when having an Entity System in place it’s rare to need events.

Could you maybe elaborate on that a bit (if not already done), like: the Player joined Event could also be a component on the PlayerEntity or something.

…well, the player is not necessarily a game object (though their avatar might be). There are going to be a lot of application events that have nothing to do with game objects and more to do with application state. (Errors being a pretty significant one.)

What you don’t want to do is have some game level service using events to communicate with some other game level service. That’s what the ES is for.

Also, sometimes events are useful in coordination of the player input. For example, I think in Mythruna I used a cell change event to let a string of listeners decide if the block should be changed, changed to what, etc… user initiated action goes through a chop-chain of listeners before modifying the actual world. (And in any case, the world itself in Mythruna is not a game object and is not managed by the ES… so there are also chunk change events that propagate back out, etc.)

You should not use events to communicate about game objects if you have an ES… you are kind of making an end run around ES management at that point and will ultimately regret it.

Edit: and actually, in response specifically to the playerJoined event… I think that is when I setup the player entity, etc. on their behalf. If interested, I can try to search for various examples I use event bus events for.

1 Like

In the sim-eth-es example, there is only one:

I could have also done chat events this way.

If there were real player accounts, I’d also expect some account related events as well as login/logout events.

1 Like

How would you integrate extensions/mod/addons with the ES if not through events (for game logic)?

My approach is to have the mod be an extension of AbstractGameSytem, check for that when doing the classloading and add to the update-loop. This only require the component classes when ‘coding’ the addon. These addons could also be eventhandlers (either through SiO2 or Guava).

This isn’t how I understand it. Correct me if I misread (i’ll see if I can find the example I found)
In Guava, you annotate the handling method with @Subscribe, and then simply register the class as a Listener. Whenever an event is posted, that fits the parameters of the annotated methods in the registered class, it gets propagated to that class instance. So you would only need to annotate any methods, and register the class (1 call).

So in SiO2 you would have four methods (private or not), as you would in Guava (public as far as I can tell). In SiO2 you register your methods (1 call), in Guava you register the class (1 call). In SiO2 ýou ‘group’ (?) your events into EventType, in Guava you can extend your Event-classes which allow your subscribers to filter for more or less granular event.

Edit: Ah, but I see that SiO2 would only have one event-class to contain the information. Not sure I understand that part right now.

Not sure I understand the question. They would have access to the ES just like anything else. Why would they need events?

Fair enough. One call versus one call. But in guava you have to have a different event class for every type of event you want to handle… even if they ultimately contain a lot of the same object. Made worse by the fact that the methods will be public so to do it properly then you’d have an inner class.

SiO2:
EventBus.publish(PlayerEvent.joined, player);

Guava:
someInstanceOfEventBusIGotFromSomewhere.post(new PlayerJoinedEvent(player));

The listener pattern would be different, too…

SiO2:

public class SomeAppState extends BaseAppState {
    .... 
    protected void onEnable() {
        EventBus.addListener(this, PlayerEvent.joined,  PlayerEvent.left);
    }

    protected void onDisable() {
        EventBus.removeListener(this, PlayerEvent.joined,  PlayerEvent.left);
    }

    private void onJoined( Player player ) {
        // Do some joining stuff
    }

    private void onLeft( Player player ) {
        // Do some leaving stuff
    }
}

In Guava:

public class SomeAppState extends BaseAppState {
    private EventBus theEventBus;
    private PlayerEventHandler playerEventHandler = new PlayerEventHandler();

    public SomeAppState( EventBus theEventBus ) {
        this.theEventBus = theEventBus;
    }

    protected void onEnable() {
        theEventBus.register(playerEventHandler);
    }

    protected void onDisable() {
        theEventBus.unregister(playerEventHandler);
    }

    private class PlayerEventHandler {
        @Subscribe public void onJoined( PlayerJoinedEvent event ) {
                // Do some joined stuff with event.getPlayer()... likely calling a private
                // method on the app state
        }
        @Subscribe public void onLeft( PlayerJoinedEvent event ) {
                // Do some joined stuff with event.getPlayer()... likely calling a private
                // method on the app state
        }
    }
}

There’s just a whole bunch of extra boiler plate and garbage with Guava’s approach.

Thanks for the clarification. Means a lot.

Edit:

Event classes
Guava: One per event, can be hierarchical
SiO2: One per group of event

EventBus
Guava: Instantiated
SiO2: Singleton

…or instantiated. Both are supported in SiO2.

Edit: the static methods just delegate to a preset singleton.