I’ve finished changing the way events work in Urho3D, implemented a custom RTTI and wanted to share some details of it.
First, the regular Urho3D code:
// Custom sample event
URHO3D_EVENT(E_SAMPLEEVENT, SampleEvent)
{
URHO3D_PARAM(P_OBJ, Obj); // (void*) RefCounted*
URHO3D_PARAM(P_INDEX, Index); // (long long) size_t
}
// =================================================
// A class which will listen to the new event:
class NodeListener : public Node
{
public:
NodeListener(Context* context) :
Node(context)
{
SubscribeToEvent(E_SAMPLEEVENT, URHO3D_HANDLER(NodeListener, OnSampleEvent));
}
~NodeListener() override = default;
URHO3D_OBJECT(NodeListener, Node);
void SetId(size_t id)
{
id_ = id;
}
size_t GetId() const
{
return id_;
}
private:
void OnSampleEvent(StringHash type, VariantMap& data)
{
Variant& node = data[SampleEvent::P_OBJ];
Variant& index = data[SampleEvent::P_INDEX];
NodeListener* listener = dynamic_cast<NodeListener*>((RefCounted*)node.GetVoidPtr());
if (listener == this)
{
SetId((size_t)index.GetInt64());
}
}
private:
size_t id_ {};
};
// =================================================
// Assuming you have a basic class for creating the Urho3D loop, scene, etc:
// Create 1000 node listeners
{
// listeners_ is of type Vector<SharedPtr<NodeListener>>
const size_t count = 100 * 10;
listeners_.Reserve(count);
for (size_t i = 0; i < count; ++i)
{
SharedPtr<NodeListener> node {new NodeListener(context_)};
scene_->AddChild(node);
listeners_.Push(node);
}
}
// =================================================
// Now, somewhere in the main loop, when you press a key:
if (input_->GetKeyPress(Key::KEY_SPACE))
{
const size_t listenerCount = listeners_.Size();
VariantMap& data = GetEventDataMap();
auto start = std::chrono::high_resolution_clock::now();
{
for (size_t i = 0; i < listenerCount; ++i)
{
data[SampleEvent::P_OBJ] = (void*)listeners_[i].Get();
data[SampleEvent::P_INDEX] = (long long)i;
SendEvent(E_SAMPLEEVENT, data);
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
URHO3D_LOGINFOF("Sending %d events to %d node listeners took %d ms", listenerCount, listenerCount, duration);
}
On my hardware, the above scenario consistently produces:
“INFO: Sending 1000 events to 1000 node listeners took 36 ms”
Now, after some major changes, here’s the same sample:
// Custom sample event
struct SampleEvent
{
RefCounted* obj;
size_t index;
};
// =================================================
// The class which will listen to the event:
class NodeListener : public Node
{
public:
NodeListener(Context* context) :
Node(context)
{
ListenEvent<SampleEvent>([this](const SampleEvent& data)
{
NodeListener* listener = RTTI::DynamicCast<NodeListener*>(data.obj);
if (listener == this)
{
SetId(data.index);
}
});
}
~NodeListener() override = default;
RTTI_IMPL();
void SetId(size_t id)
{
id_ = id;
}
size_t GetId() const
{
return id_;
}
private:
size_t id_ {};
};
// =================================================
// Creating the 1000 node listeners is exactly the same as before (except it's using the std::vector);
// Then, somewhere in the main loop:
if (input_->GetKeyPress(KeyCode::Space))
{
size_t listenerCount = listeners_.size();
auto start = std::chrono::high_resolution_clock::now();
{
for (size_t i = 0; i < listenerCount; ++i)
{
SendEvent(SampleEvent {
listeners_[i].Get(),
i
});
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
URHO3D_LOGINFO(fmt::format("Sending {0:d} events to {0:d} node listeners took {1:d}ms", listenerCount, duration).c_str());
}
On my hardware, the changes above consistently produces:
“INFO: Sending 1000 events to 1000 node listeners took 8ms”
Both of the tests were compiled on windows using MSVC 2019: x64 Static Release, C++17;
C++ exceptions disabled;
The regular Urho3D uses the default c++ RTTI, and thus it was enabled;
The custom RTTI was coded by Samuel Kahn, he explains how the magic works here:
http://kahncode.com/2019/09/24/c-tricks-fast-rtti-and-dynamic-cast/
I’ve only made minor changes to it. Instead of generating a random id at runtime, it is generated at compile time.
The event handler was replaced by EventBus (2.6):
https://github.com/gelldur/EventBus
One thing to consider is you can’t listen an event from a specific sender.
Some other notes:
All containers (except String) were replaced by the ::std ones;
Urho2D/UI and scripting languages were ditched (I’ll be using a third party lib for UI);