Publish-Subscribe Event System

Prodigy Engine uses a simple publish-subscribe event system that uses static function callbacks to perform tasks. The event system is used in numerous places in the engine including but not limited to the dev console, physics system, object loader and more.

The event system is fairly simple like most other systems used in Prodigy but provides the desired functionality in order to simplify certain tasks.

Implementation of the event system is highlighted below:

typedef bool (*EventCallBackFn)(EventArgs& args);

//------------------------------------------------------------------------------------------------------------------------------
class EventSubscription
{
	friend class EventSystems;

private:
	EventSubscription(EventCallBackFn name)
	{
		m_callbackFn = name;
	}

private:
	EventCallBackFn m_callbackFn = nullptr;
};

//------------------------------------------------------------------------------------------------------------------------------
class EventSystems
{
	//Use this for std::map being stored as m_eventSubscriptions
	typedef std::vector<EventSubscription*> SubscribersList;

public:
	EventSystems();
	~EventSystems();

	void			StartUp();
	void			BeginFrame();
	void			EndFrame();
	void			ShutDown();

	void			SubscribeEventCallBackFn(const std::string& eventName, EventCallBackFn callBack);
	void			UnsubscribeEventCallBackFn(const std::string& eventName, EventCallBackFn callBack);
	int				FireEvent(const std::string& eventName);
	int				FireEvent(const std::string& eventName, EventArgs& args);
	int				GetNumSubscribersForCommand(const std::string& eventName) const;
	void			GetSubscribedEventsList(std::vector<std::string>& eventNamesWithSubscribers) const;

private:
	std::map<std::string, SubscribersList > m_eventSubscriptions;
};

To use the system, there needs to be some event subscription call that allows an event to be used within the event system and a string identifier that calls the corresponding event call back.

The event system supports the passing of arguments through the event call using an EventArgs which is a typedef of the Named Properties class.

Named properties by itself cannot determine the type of the property to be passed as arguments, instead, it uses 2 other classes which are the Object Properties and the Typed Properties class to determine the argument type passed. Named properties is not concerned with type information and hence uses abstracted Typed properties to store the arguments.

Below is the implementation used for NamedProperties, ObjectProperties and TypedProperties:

//------------------------------------------------------------------------------------------------------------------------------
class NamedProperties
{
public:
	NamedProperties();
	~NamedProperties();

public:

	// template interface functions
	template <typename T>
	void SetValue(std::string const &key, T const &value)
	{
		//calls set value on T
		TypedProperty<T> *prop = new TypedProperty<T>(value);

		std::map<std::string, BaseProperty*>::iterator itr = m_properties.find(key);
		if (itr != m_properties.end())
		{
			delete itr->second;
		}

		m_properties[key] = prop;
	}

	template <typename T>
	T GetValue(std::string const &key, T const &defaultValue)
	{
		std::map<std::string, BaseProperty*>::iterator value = m_properties.find(key);
		if (value == m_properties.end()) 
		{
			return defaultValue;
		}

		BaseProperty *prop = value->second;

		TypedProperty<T> *typedProp = dynamic_cast<TypedProperty<T>*>(prop);
		if (typedProp == nullptr) 
		{
			std::string str = prop->ToString();
			return FromString(str.c_str(), defaultValue);
		}
		else {
			return typedProp->m_value;
		}
	}

	//	Handling cases where pointers are passed to the GetValue and SetValue functions
	template <typename T>
	T GetValue(std::string const &key, T *def)
	{
		std::map<std::string, BaseProperty*>::iterator value;
		value = m_properties.find(key);
		
		if (value == m_properties.end()) 
		{
			return *def;
		}

		BaseProperty *prop = value->second;
		TypedProperty<T*> *typedProp = dynamic_cast<TypedProperty<T*>*>(prop);
		if (typedProp == nullptr) 
		{
			return *def;
		}
		else 
		{
			return *typedProp->m_value;
		}
	}

	template <typename T>
	T SetValue(std::string const &key, T *ptr)
	{
		//Dereference and call on type T
		SetValue<T*>(key, ptr);
	}

private:
	std::map<std::string, BaseProperty*> m_properties;
};

inline std::string ToString(void const * ptr) { UNUSED(ptr);  return ""; }
//------------------------------------------------------------------------------------------------------------------------------
// Base property type to allow us to make a good interface for all types by calling a generic type's ToString
//------------------------------------------------------------------------------------------------------------------------------
class BaseProperty
{
public:
	
	virtual std::string ToString() const = 0;
};
//------------------------------------------------------------------------------------------------------------------------------
// Generic template typedProperty. This is used for type checking using RTTI (Run time type information) or using static memory
// to create a unique identifier for types (This is I will  use because I can pass it void pointers)
//------------------------------------------------------------------------------------------------------------------------------
template <typename T>
class TypedProperty : public BaseProperty
{
public:
	TypedProperty(T const &val)
		: m_value(val)
	{}

	//Using duck typing (This will fail if there is no ToString() specified for the type being passed)
	virtual std::string ToString() const override
	{
		return ::ToString(m_value);
	}

public:
	T m_value;
};
//------------------------------------------------------------------------------------------------------------------------------
// Template functions to get from string
//------------------------------------------------------------------------------------------------------------------------------
template <typename T>
T FromString(char const *str, T const &def)
{
	if (str != nullptr)
	{
		return T(str);
	}
	else
	{
		return def;
	}
}

template <>
float FromString(char const *str, float const &def);

template <>
bool FromString(char const* str, bool const &def);

template <>
int FromString(char const* str, int const &def);

template <>
std::string FromString(char const* str, std::string const &def);

//------------------------------------------------------------------------------------------------------------------------------
// Template functions to convert back to string
//------------------------------------------------------------------------------------------------------------------------------
template <typename T>
std::string ToString(const T &value)
{
	return value.GetAsString();
}

template <>
std::string ToString(float const &value);

template <>
std::string ToString(double const &value);

template <>
std::string ToString(bool const &value);

template <>
std::string ToString(int const &value);

template <>
std::string ToString(std::string const &value);

Usage

Below is an example of usage of the event system where corresponding collision meshes are loaded for render meshes if collision information exists in the custom .mesh file. Below is the subscription logic for the event from the PhysXSystem:

//First subscribe the LoadCollisionMeshFromData function as a LoadCollisionMeshFromData event
	g_eventSystem->SubscribeEventCallBackFn("LoadCollisionMeshFromData", LoadCollisionMeshFromData);

When the object loader finds the corresponding collision mesh information in the custom .mesh file, this is the logic that is implemented:

elem = root->FirstChildElement("collision");
		while (elem != nullptr)
		{
			//We requested to create a static collider with this model so generate that using the PhysX System
			NamedProperties eventArgs;
			eventArgs.SetValue("id", ParseXmlAttribute(*root, "id", ""));
			eventArgs.SetValue("src", ParseXmlAttribute(*elem, "src", ""));
			eventArgs.SetValue("physXFlags", ParseXmlAttribute(*elem, "physXFlags", ""));
			eventArgs.SetValue("position", ParseXmlAttribute(*elem, "position", Vec3::ZERO));

			eventArgs.SetValue("transform", m_transform);
			eventArgs.SetValue("scale", m_scale);
			eventArgs.SetValue("invert", m_invert);
			eventArgs.SetValue("tangents", m_tangents);

			g_eventSystem->FireEvent("ReadCollisionMeshFromData", eventArgs);

			elem = elem->NextSiblingElement("collision");
		}

With the required arguments set on the EventArgs object passed to the event system’s FireEvent call, the event system now calls the function callback which was sent to it during the event subscription.