IN THIS ARTICLE
Components and EBuses: Best Practices
Follow these best practices for creating and using components and EBuses.
EBus Names
The following EBus naming conventions remove ambiguity and provide consistency.
Use the name format
MyComponentRequestBus
for the bus that others use to invoke functions onMyComponent
, as in the following example.class CheeseburgerComponentRequests : public AZ::ComponentBus { bool ICanHasCheeseburger() const = 0; }; using CheeseburgerComponentRequestBus = AZ::EBus<CheeseburgerComponentRequests>;
Use the name format
MyComponentNotificationBus
for events that are broadcast fromMyComponent
, as in the following example.class CheeseburgerComponentNotifications : public AZ::ComponentBus { void OnCheeseburgerEaten(AZ::u8 yelpRating) {}; }; using CheeseburgerComponentNotificationBus = AZ::EBus<CheeseburgerComponentNotifications>;
Provide Default Implementations of Methods
Notification buses typically provide default implementations of the methods within the interface. Many other components can monitor your component’s events, but not all of them are interested in every event that your component sends. If you provide default implementations for all your methods, other components that subscribe to your events can implement only those events that are relevant to them.
EBus Event Naming
Good EBus event names are verbose. Classes can monitor multiple buses, so descriptive event names makes it clear which bus the function corresponds to. This practice also prevents potential name collisions among event interfaces from different buses.
The following example is a clearly named PhysicsComponentNotificationBus
event.
virtual void OnPhysicsEnabled() = 0;
The following example is an ambiguously named PhysicsComponentNotificationBus
event.
virtual void OnEnabled() = 0;
Avoid Using Type Definitions for Serialized Data
An instructive example from O3DE shows the importance of using classes instead of type definitions for serialized data. Formerly, EntityId
used the type definition uint32_t
. When the decision was made to change this to 64-bit, upgrade functions had to be written for every class that contained an EntityId
. If EntityId
had been a class, a single upgrade function could have been written for the class, and no further work would have been required. Obviously, this principle does not apply to primitive types like bool
, float
, int
, and string
. However, if you have a specific type that is serialized and might change in the future, implement it as a reflected class. This provides a single context where you can easily make the conversion for the class or type.
EBus Results
Always initialize a variable before calling an EBus event that overwrites the variable. Even if you are sure that a particular class or component is listening on the bus, it’s worth handling the exceptional case. This is especially true in distributed environments in which entities can come and go as part of area-of-interest or other dynamic patterns.
The following example initializes a result variable before calling an EBus event that produces a result.
AZ::Transform targetEntityTransform = AZ::Transform::Identity(); // initialize result variable...
EBUS_EVENT_ID_RESULT(targetEntityTransform, targetEntityId, AZ::TransformBus, GetWorldTM); // ...in case of no response
EBus Timing
Following are some best practices for the timing of EBus actions.
- In the Activate() function, make sure that connecting to buses is the last step.
- In the Deactivate() function, make sure that disconnecting from buses is the first step.
- In a multithreaded environment, it’s possible to receive bus events from the moment that you connect to the bus until the moment you disconnect. For this reason, make sure of the following:
- Your component is fully activated before it starts reacting to events.
- Your component stops receiving events before it starts deactivation.
This practice prevents your component from being in a half-activated state when it starts reacting to events, or in a half-deactivated while still receiving events.
- When you send events on a notification-style bus, the last step in a function should ensure that the data is fully populated.
The following is an example to avoid.
EBUS_EVENT_ID(GetEntityId(), OnTransformChanged, newTransform);
m_transform = newTransform;
If a component is monitoring the OnTransformChanged
event and sets your transform in response to the event, the component’s action will be undone by the m_transform = newTransform;
assignment.
Making Functions Public or Protected
Consider the following when deciding to make functions public or private.
- Make your bus functions
public
if they constitute the public interface for your class. While it’s discouraged, O3DE does not prevent users from getting direct pointers to components and calling functions directly. To avoid this, make sure that your useful functions are public. For example,MyComponent
should probably implement functions fromMyComponentRequestBus
publicly. - Make your bus functions
protected
if they contain the private workings of your class. For example, your component’s reaction to theTransformNotificationBus::OnTransformChanged
event would likely be a private implementation detail.