This documentation is for a prerelease version of O3DE. Click here to switch to the latest release, or select a version from the dropdown.

Version:

AZ::Event

The AZ::Event template class is used to subscribe to and publish single value messages across the different components of your game. It’s designed to replace value-based event pub/sub patterns that are currently implemented using EBus, only with significantly simpler syntax. There are a number of benefits to this new system, including simpler code, fewer files, removal of aggregate interfaces where a handler only cares about a subset of events, and improved runtime performance when dispatching value changes to registered handlers.

AZ::Event is defined as a C++ template (template <typename... Params>) in the following header: %INSTALL-ROOT%\dev\Code\Framework\AzCore\AzCore\Ebus\Event.h

AZ::Event limitations include the following:

  • The event system is single-threaded only. Handlers should Connect() and Disconnect() on the same thread that is dispatching events.
  • Handlers can be bound only to an existing event instance. You can’t bind to an event prior to its creation (the way you can with an address by ID EBus).
  • A handler can be bound only to a single event. You can’t bind a single handler to more than one event.
  • There are no return results for handlers. The handler function signature must have a void return result.
  • There is no event queuing. A queue can be built as a modular handler wrapper, but in the single-threaded implementation, all events immediately dispatch to all handlers.

AZ::Event provides a Handler class and the following explicit constructors:

  • Handler(std::nullptr_t)
  • Handler(Callback callback)
  • Handler(const Handler& rhs)
  • Handler(Handler&& rhs)

AZ::Event::Handler has the following methods defined on it:

  • To connect to a Handler instance: void Connect(Event<Params...>& event);
  • To disconnect from a Handler instance: void Disconnect();

Example usage

  • To create an event for handling, declare an instance of AZ::Event with the following C++ syntax:

    AZ::Event<{type}> {name_of_event};

    For example, to declare an event that can publish a Boolean value:

    AZ::Event<bool> isPlayerActive;

  • To declare a handler that will process the event when it is signaled:

    AZ::Event<bool>::Handler playerActiveHandler([]({type} value) {});

    For example, to create a handler for the event from the previous example:

    AZ::Event<bool>::Handler playerActiveHandler([](bool value) {});

When you declare the event and the handler in your header, you can connect to the event and signal it. Here is a simple example using the declarations and calls from the prior examples:

// Declaration in your header
AZ::Event<bool> isPlayerActive; // Declare the event
AZ::Event<bool>::Handler playerActiveHandler([](bool value) {}); // Declare our handler

// Usage in your code
handler.Connect(isPlayerActive); // Connect the handler to to our event
// ...
isPlayerActive.Signal(true); // Signal the event to inform subscribers that the player is active

Here is a more complex example that signals multiple events with a class to handle them:

class ExampleEventComponent
   : public AZ::Component
{
public:
    using Event1Type = AZ::Event<const AZ::Vector3&>;
    using Event2Type = AZ::Event<float, float>;

    void Tick()
    {
        // Update component state
        if (value1Changed)
        {
            m_event1.Signal(value1);
        }

        if (value2Changed)
        {
            m_event2.Signal(value2.x, value2.y);
        }
    }

    void ConnectEvent1Handler(Event1Type::Handler& handler) { handler.Connect(m_event1); }
    void ConnectEvent2Handler(Event2Type::Handler& handler) { handler.Connect(m_event2); }
private:
    Event1Type m_event1;
    Event2Type m_event2;
};

class ExampleHandlerComponent
   : public AZ::Component
{
public:
    ExampleHandlerComponent()
       : m_handler1([this](const AZ::Vector3& value) { this->OnEvent1Invoked(value); })
       , m_handler2([this](float value2x, float value2y) { this->m_value2x = value2x; this->m_value2y = value2y;})
    {
    }

    void Activate()
    {
        ExampleEventComponent* eventComponent = GetEntity()->FindComponent<ExampleEventComponent>();
        if (eventComponent)
        {
            eventComponent->ConnectEvent1Handler(m_handler1);
            eventComponent->ConnectEvent2Handler(m_handler2);
        }
    }

    void OnEvent1Invoked(int32_t value) { // do something with value }
private:
    ExampleEventComponent::Event1Type::Handler m_handler1;
    ExampleEventComponent::Event2Type::Handler m_handler2;
};

Performance AZ::Event is roughly another 20% faster than even the lambda syntax for EBus, and over 40% faster than EBus’s member function pointer model. These performance deltas scale linearly with the number of handlers, so AZ::Event is 40% faster than using standard EBus member function pointers whether there’s 1,000 handlers attached, or 1,000,000.

To compare the EBus handler implementation code against AZ::Event, here is an example of code used to signal a change to a single value using EBus.

// Single-value message handler using EBus

// Bus interface
class EBusEventExample
    : public AZ::EBusTraits
{
public:
    using MutexType = NullMutex;
    static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
    static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
    virtual void OnSignal(int32_t) = 0;
};
using EBusEventExampleBus = AZ::EBus<EBusEventExample>;

// Bus implementation
class EBusEventExampleImpl
    : public EBusPerfBaselineBus::Handler
{
public:
    EBusEventExampleImpl() { EBusEventExampleBus::Handler::BusConnect(); }
    ~EBusEventExampleImpl() { EBusEventExampleBus::Handler::BusDisconnect(); }
    void OnSignal(int32_t) override {}
};

// Usage
EBusEventExampleImpl handler;
EBusEventExampleBus::Broadcast(&EBusEventExample::OnSignal, 1);

And here is an example that performs the same work using AZ::Event.

// Single-value message handler implemented using AZ::Event
AZ::Event<int32_t> event; // Declare the event
AZ::Event<int32_t>::Handler handler([](int32_t value) {}); // Declare our handler

// Usage
handler.Connect(event); // Connect the handler to our event
event.Signal(1); // Signal an event, this will invoke our handler's lambda

Note the reduced lines of code, as well as the overall simpler code pattern. Try it out by porting some of your current EBus message handlers to use AZ::Event, and then test it using our built-in unit tests and benchmarks.

Unit testing and benchmarking

The AZ::Event system includes a number of unit tests and benchmarks to validate correct behavior and confirm the performance advantages over an equivalent EBus implementation.

To execute the unit tests, the following command-line arguments can be provided to the AzTestRunner:

%INSTALL-ROOT%\dev\Bin64vc141.Test\AzCoreTests.dll AzRunBenchmarks –pause-on-completion –benchmark_filter=BM_EventPerf*

You should see unit testing output like this.

[==========] Running 7 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 7 tests from EventTests
[ RUN      ] EventTests.TestHasCallback
[       OK ] EventTests.TestHasCallback (0 ms)
[ RUN      ] EventTests.TestScopedConnect
[       OK ] EventTests.TestScopedConnect (0 ms)
[ RUN      ] EventTests.TestEvent
[       OK ] EventTests.TestEvent (1 ms)
[ RUN      ] EventTests.TestEventMultiParam
[       OK ] EventTests.TestEventMultiParam (0 ms)
[ RUN      ] EventTests.TestConnectDuringEvent
[       OK ] EventTests.TestConnectDuringEvent (0 ms)
[ RUN      ] EventTests.TestDisconnectDuringEvent
[       OK ] EventTests.TestDisconnectDuringEvent (0 ms)
[ RUN      ] EventTests.TestDisconnectDuringEventReversed
[       OK ] EventTests.TestDisconnectDuringEventReversed (1 ms)
[----------] 7 tests from EventTests (9 ms total)

To execute the benchmarks, the following command-line arguments can be provided to the AzTestRunner:

%INSTALL-ROOT%\dev\Bin64vc141.Test\AzCoreTests.dll AzRunBenchmarks –pause-on-completion –benchmark_filter=BM_EventPerf*

You should see benchmark output like this.

Benchmark name                     benchmark time   cpu time   iterations
BM_EventPerf_EventEmpty               16869 ns      16881 ns      40727
BM_EventPerf_EventIncrement           20124 ns      20508 ns      37333
BM_EventPerf_EBusEmpty                29421 ns      29157 ns      23579
BM_EventPerf_EBusIncrement            29686 ns      29297 ns      22400
BM_EventPerf_EBusIncrementLambda      24516 ns      24554 ns      28000