Creating a Request Bus in O3DE
Feb 12, 2023
A request bus in O3DE is a type of EBus designed to receive requests. An example of a request bus that exists in the engine is the Transform Bus, which provides an interface for manipulating an entity's transform component.
Creating EBuses is mildly laborious and confusing. Here I'll try to document the process as densely as I can.
Request Bus vs Notification Bus
A request bus is like a notification bus, but usually paired with a single handler. Both of them are just EBuses under the hood, so the difference is mainly about convention. It's helpful to distinguish between them because the O3DE codebase makes heavy use of this distinction. Very often you'll find APIs that are either <X>RequestBus
or <X>NotificationBus
, and those names tell you how you're meant to use them... however, not all EBuses follow that naming convention.
When you see a RequestBus, then that almost always means there's one handler connected to that bus, and the handler was developed alongside the bus for a specific purpose (example: TransformBus). By contrast, a NotificationBus may have dozens of different handlers connected to it from various independent subsystems (example: TickBus).
It's confusing because under the hood the lines between them are kind of a blur.
In short:
- request bus: one handler (usually)
- notification bus: multiple handlers (usually)
Requirements
At minimum, a request bus requires the following:
- an interface class which declares the request API
- a handler class which implements the functions declared in (1)
- an instance of (2) that is connected (1)
Full Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
The interface class TomatoRequests
inherits from AZ::EBusTraits
. This just provides some configuration variables required by the EBus system. We can override them if we want, but in this example we don't. (more info here)
The handler class TomatoRequestHandler
is what will handle the actual events we send through the request bus. Any code that can find our interface class (i.e. can #include
this header) is able to send a request on the TomatoRequests EBus. The EBus system will try to dispatch those to a connected handler instance, but if it doesn't find one, then the request will simply be ignored.
...Thus, we need to connect our handler to the TomatoRequests EBus. An obvious place to do this is in the constructor (and disconnect in the destructor). Here's an example implementation for TomatoRequestHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Using the request bus
Once all of that is in place, using the request bus is easy. Here's an example of how to do it from your project's AZ::Module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
The
EBUS_*
macros are defined in EBus.h
Compiling that and launching the editor will print this to standard output:
1 2 3 4 5 6 7 |
|
Since we instantiated 2 handlers, BusConnect()
was called twice, so our events get handled twice. This is only possible because the default handler policy of AZ::EBusTraits
is EBusHandlerPolicy::Multiple
. This is what allows us to connect multiple handlers to our request bus. If we change that to EBusHandlerPolicy::Single
, then one of those handlers will be ignored so that the event only gets handled by one of them.
By default, multiple handlers are processed in an undefined order, however you can control the order by providing a custom ordering function. Read about that here.
In a real project, you'll likely allocate your handlers differently so that they can survive longer than just the project initialization process. Where you use/how you manage your handler instances will depend on what you're trying to do.
One very common scenario will be to have a custom Component that also acts as a request bus handler for some related custom request bus. This is easier to deal with because O3DE automatically handles Component lifecycles, so you don't have to worry about allocating it yourself. In these cases, you'll likely want to use AZ::ComponentBus
instead of AZ::EBusTraits
.
Component EBus
Reading through O3DE source code, you'll encounter some components inheriting from a class called AZ::ComponentBus
. Here is the entire source code for that class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
As you can see, all it really does is override the default EBus traits to values that make sense for components.
Basically, consider the situation where you have 100 enemies in your level, and those enemies all have a EnemyAIComponent
attached. If EnemyAIComponent is a handler (like TomatoRequestHandler above) for an AI-related request bus, how the heck is O3DE supposed to dispatch this code:
1 |
|
There are 100 handlers attached to that bus, so which one should handle it? That's where the AddressPolicy comes in handy; it gives us a way to assign an address to an event. AZ::ComponentBus
enables this addressing feature by using entity IDs. That way, we can replace the event dispatch with something like this:
1 |
|
More details about these traits are available at the O3DE docs.
Sources
- https://www.o3de.org/docs/api/frameworks/azcore/class_a_z_1_1_e_bus.html
- https://www.o3de.org/docs/user-guide/programming/messaging/ebus-design/
- O3DE source code
© Alejandro Ramallo 2024