Creating a Request Bus in O3DE

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:

  1. an interface class which declares the request API
  2. a handler class which implements the functions declared in (1)
  3. 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
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/Debug/Trace.h>

namespace TomatoFarm {
    // INTERFACE class
    class TomatoRequests : public AZ::EBusTraits {
    public:
        virtual ~TomatoRequests() = default;

        //Request bus interface functions:

        virtual void Plant() = 0;
        virtual int Harvest() = 0;

        //this one isn't pure virtual, so if a handler doesn't override it,
        //we can use the fallback implementation below
        virtual void Eat(int amount) {
            AZ_TracePrintf("TomatoRequests", "Handler didn't implement Eat()! amount: %d\n", amount);
        };
    };

    // this lets us use a friendlier name for the bus
    using TomatoRequestBus = AZ::EBus<TomatoRequests>;

    // HANDLER class (note what we're inheriting from)
    class TomatoRequestHandler : public TomatoRequestBus::Handler {
    public:
        TomatoRequestHandler();
        ~TomatoRequestHandler();
        void Plant() override;
        int Harvest() override;
        //Note that we're not implementing Eat()!
    };

}; // namespace TomatoFarm

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
//TomatoRequestHandler.cpp
#include "TomatoRequests.h"

namespace TomatoFarm {

    TomatoRequestHandler::TomatoRequestHandler() {
        //Connect this handler instance to the TomatoRequestBus
        TomatoRequestBus::Handler::BusConnect();
    }

    TomatoRequestHandler::~TomatoRequestHandler() {
        //Disconnect when we're destroyed
        TomatoRequestBus::Handler::BusDisconnect();
    }

    void TomatoRequestHandler::Plant() {
        AZ_TracePrintf("TomatoFarm", "Planted tomato.");
    }

    int TomatoRequestHandler::Harvest() {
        AZ_TracePrintf("TomatoFarm", "Harvested tomato.\n");
        return 1234;
    }

}; //namespace TomatoFarm

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
//...
#include "TomatoRequests.h"

namespace MyProject
{
    class MyProjectModule
        : public AZ::Module
    {
    public:
        AZ_RTTI(MyProjectModule, "{7ef21243-7b84-43d5-9033-36a02be64fc5}", AZ::Module);
        AZ_CLASS_ALLOCATOR(MyProjectModule, AZ::SystemAllocator, 0);

        MyProjectModule(): AZ::Module(){
            m_descriptors.insert(m_descriptors.end(), {
                MyProjectSystemComponent::CreateDescriptor(),
                //...
            });

            /*
                create an instance, so that a handler exists on the bus
                remember, the constructor calls `BusConnect()`
            */
            TomatoFarm::TomatoRequestHandler my_handler_whatever;
            TomatoFarm::TomatoRequestHandler my_handler_whatever2;

            /*
                Send some events on the bus
                note that we're not using the handler instances above,
                just the request bus name from the namespace.

                The EBus system knows how to route them to our handler(s)
            */
            EBUS_EVENT(TomatoFarm::TomatoRequestBus, Plant);

            int num_tomatos = -1;
            EBUS_EVENT_RESULT(num_tomatos, TomatoFarm::TomatoRequestBus, Harvest);
            AZ_TracePrintf("MyProject", "Harvested %d tomatos\n", num_tomatos);

            EBUS_EVENT(TomatoFarm::TomatoRequestBus, Eat, 5);

            /*
                when our handlers go out of scope, the destructor will call
                `BusDisconnect()`, so the bus will not have any handlers
                and any future events will not be handled by anything
            */
        }
//...
 };

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
TomatoFarm: Planted tomato.
TomatoFarm: Planted tomato.
TomatoFarm: Harvested tomato.
TomatoFarm: Harvested tomato.
MyProject: Harvested 1234 tomatos
TomatoRequests: Handler didn't implement Eat()! amount: 5
TomatoRequests: Handler didn't implement Eat()! amount: 5

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
//from Code/Framework/AzCore/AzCore/Component/ComponentBus.h
/**
 * Base class for message buses.
 * Most components that derive from AZ::Component use this class to implement 
 * their buses, and then override the default AZ::EBusTraits to suit their needs. 
 */
class ComponentBus
    : public AZ::EBusTraits
{
public:

    /**
     * Destroys a component bus.
     */
    virtual ~ComponentBus() = default;

    //////////////////////////////////////////////////////////////////////////
    // EBusTraits overrides
    /**
     * Overrides the default AZ::EBusTraits address policy so that the bus
     * has multiple addresses at which to receive messages. This bus is 
     * identified by EntityId. Messages addressed to an ID are received by 
     * handlers connected to that ID.
     */
    static const EBusAddressPolicy AddressPolicy = EBusAddressPolicy::ById;
    /**
     * Overrides the default AZ::EBusTraits ID type so that entity IDs are 
     * used to access the addresses of the bus.
     */
    typedef EntityId BusIdType;
    //////////////////////////////////////////////////////////////////////////
};

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
EBUS_EVENT(MyProject::EnemyAIRequestBus, WalkInCircles);

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
EBUS_EVENT_ID(enemy_entities[69], MyProject::EnemyAIRequestBus, WalkInCircles);

More details about these traits are available at the O3DE docs.

Sources