This blog post is a part of the C++ Game Engine that I coded during my C++ Game Engineering class here in the Master of Entertainment Arts & Engineering program at University of Utah.

This versatile, custom built multicast delegate system can come in real handy when we want to provide callback functions while maintaining control.

Delegate

The cDelegate template class is the most important part of this multicast delegate system. Simply speaking, the delegate holds a void * m_pInstance that points to a class T,  also stores a pointer to class T’s function which is tMethodStub.

The cDelegate class provides a public static factory function for the outside world to use.

Note: The template statements can sometimes get removed by the code snippets, which is weird, they are

template<class … Params>        for class declaration, and
template <class T, void (T::*TMethod)(Params …)>        for function declaration

template
class cDelegate
{
public:
    cDelegate() :
        m_pInstance(nullptr),
        m_pMethodStub(nullptr)
    {}

    bool operator==(const cDelegate & i_other)
    {
        return (m_pInstance == i_other.m_pInstance) && (m_pMethodStub == i_other.m_pMethodStub);
    }

    // Static function to create a Delegate with a class T instance as parameter i_pInstance
    // This is template with a class T and a function pointer from class T
    template 
    static cDelegate Create(T* i_pInstance)
    {
        return cDelegate(i_pInstance, &method_stub);
    }

    operator bool() const
    {
        return m_pInstance != nullptr;
    }

    void operator()(Params ... i_Parameter) const
    {
        EAE6320_ASSERT(m_pMethodStub);
        return (*m_pMethodStub)(m_pInstance, i_Parameter);
    }

    void ExecuteIfBound(Params ... i_Parameter) const
    {
        if (m_pInstance)
        {
            EAE6320_ASSERT(m_pMethodStub);
            (*m_pMethodStub)(m_pInstance, i_Parameter ...);
        }
    }

private:
    // Define tMethodStub as a function pointer with paramaters (void * i_pInstance, Params ... i_Parameter)
    typedef void(*tMethodStub)(void * i_pInstance, Params ... i_Parameters);

    // Explicit constructor, does not allow implicit conversion something with (i_pInstance, tMethodStub) into Delegate
    // Needs to EXPLICITLY call this constructor
    explicit cDelegate(void * i_pInstance, tMethodStub i_pMethodStub) :
        m_pInstance(i_pInstance),
        m_pMethodStub(i_pMethodStub)
    {
        EAE6320_ASSERT(i_pInstance && "i_pInstance is nullptr");
        EAE6320_ASSERT(i_pMethodStub && "i_pMethodStub is nullptr");
    }

    // Get the pointer to the function TMethod within class T
    template 
static void method_stub(void * m_pInstance, Params ... i_Parameters)
    {
        (static_cast(m_pInstance)->*TMethod)(i_Parameters ...);
    }

    void *          m_pInstance;
    tMethodStub     m_pMethodStub;
};

Multicast delegate

Below is the multicast delegate class. This is fairly straightforward comparing to the cDelegate class. It stores a vector of delegates, also provides functions such as AddDelegate, RemoveDelegate, and ExecuteOnBound.

After delegates being added to a multicast delegate, it basically means the function bound to delegates are listening to the multicast delegate’s ExecuteOnBound.

template
class cMultiCastDelegate
{
public:
    void AddDelegate(cDelegate & i_Delegate)
    {
        Receivers.push_back(i_Delegate);
    }

    void RemoveDelegate(cDelegate & i_Delegate)
    {
        Receivers.erase(std::remove_if(Receivers.begin(), Receivers.end(),
            [&i_Delegate](const cDelegate & i_Entry) {return i_Entry == i_Delegate; }),
            Receivers.end());
    }

    void ExecuteOnBound(Params ... i_Parameters)
    {
        for (auto Receiver : Receivers)
            Receiver.ExecuteIfBound(i_Parameters ...);
    }

    void Clear() {
        Receivers.clear();
        Receivers = std::vector(0);
    }

private:
    std::vector Receivers;
};

 

Usage

I will be using the navigation system that I made for my Game Engineering II class to demonstrate how to use the delegates.

To fully understand the structs and classes the I am using in this code, check out my Game Engineering II final project series.

[COURSE] GAME ENG II WRITE-UP ENGINE COMPONENT UPDATE I
[COURSE] GAME ENG II WRITE-UP ENGINE COMPONENT UPDATE II
[COURSE] GAME ENG II WRITE-UP ENGINE COMPONENT UPDATE III/SUMMARY

My agent has a multicast delegate member that will be invoked when an agent reaches his destination, along with a public accessor to the multicast delegate.

// public interface for the multi-cast delegate
// ==========================================
Delegate::cMultiCastDelegate* OnAgentMoveSuccess() const;

// Multi-Cast Delegates
// ========================
Delegate::cMultiCastDelegate* m_onAgentMoveSuccess = nullptr;

The multicast delegate is initialized when this agent is created.

// Initialize the multi-cast delegate
m_onAgentMoveSuccess = new eae6320::Delegate::cMultiCastDelegate();

Inside the eae6320::Navigation namespace, I define an AgentEventReceiver as a delegate that takes a pointer to cNavigationGraphAgent as parameter.

// Typedef a Delegate type that has a cNavigationGraphAgent ptr as parameter
// Main Game cpp can use this to create multiple delegates to listen to different broadcasts
typedef Delegate::cDelegate AgentEventReceiver;

My NavigationGraphAgentManager has a private delegate that will be listening to each agent’s MovementSuccess.

// The delegate that gets created in the beginning, and added to every agents' multi-cast delegate
AgentEventReceiver m_agentMoveSuccessReceiver;

And It’s initialized when the agent manager is being constructed. The template type is the agent manager class and a pointer to the OnAnyAgentMoveSuccess function.

// Initialize delegate
m_agentMoveSuccessReceiver = AgentEventReceiver::Create(this);

Now that every time a new agent is created through the agent manager’s static factory function, the manager will add its delegate to the agent’s multicast delegate.

void eae6320::Navigation::cNavigationGraphAgentManager::AddListenerToAgentMoveSuccess(cNavigationGraphAgent * i_agent)
{
    // add the delegate that was created to the agent's multi cast delegate
    i_agent->OnAgentMoveSuccess()->AddDelegate(m_agentMoveSuccessReceiver);
}

Now that when the m_onAgentMoveSuccess executes on bound with an agent, the agent manager’s OnAnyAgentMoveSuccess function will get called.