我一直在为我的游戏引擎编写一个事件类,我遇到了以下问题:
在给定类型标志的情况下,将基类对象转换为派生类对象是一种很好的编程设计吗?
让我举几个简单的例子。 我有这个enum持有事件的类别:
enum DisplayEventType
{
DET_UNSET,
DET_WINDOW_EVENT,
DET_KEYBOARD_EVENT,
DET_MOUSE_EVENT,
DET_JOYSTICK_EVENT
};
这是所有事件的基类:
typedef std::unique_ptr<DisplayEventData> DisplayEventDataMP;
class DisplayEvent
{
private:
// The type of the display event
DisplayEventType mType;
// The data of the event
DisplayEventDataMP mEventData;
public:
// Constructor
DisplayEvent(DisplayEventType type = DisplayEventType::DET_UNSET, DisplayEventDataMP eventData = std::unique_ptr<DisplayEventData>((DisplayEventData*) 0));
// Retrieves the event type of the current event
DisplayEventType GetType() const;
// Sets the event type of the current event
void SetType(DisplayEventType type);
// Retrieves a raw pointer to the event data of the current event
DisplayEventData* GetEventData() const;
// Sets the event data of the current event
void SetEventData(DisplayEventDataMP eventData);
};
这是事件数据的基类:
class DisplayEventData
{
public:
// Virtual destructor, for the deallocation of the interface implementors to work
virtual ~DisplayEventData();
};
这些是派生类
class WindowEventData : public DisplayEventData
{
// Window event data specific data and methods
};
class KeyboardEventData : public DisplayEventData
{
// Keyboard event data specific data and methods
};
class MouseEventData : public DisplayEventData
{
// Mouse event data specific data and methods
};
class JoystickEventData : public DisplayEventData
{
// Joystick event specific data and methods
};
一些基本用法是:
// Poll for next event in the queue
DisplayEvent ev;
display->PollEvent(ev); // Fills ev with the needed data
if (ev.GetType() == DisplayEventType::DET_KEYBOARD_EVENT)
{
KeyboardEventData* kev = static_cast<KeyboardEventData>(ev.GetEventData());
// Do stuff
}
上述设计是否可行/正确?如果不是,在面向对象甚至通用编程方面会有什么更好的替代方案?
答案 0 :(得分:0)
我不相信事件系统有一个'正确'的设计,特别是在游戏引擎中。我最近为自己的游戏引擎实现了一个事件系统,我将在下面分享实现细节。它可能是也可能不是最适合您的问题,但它应该在您的头脑中产生一些想法用于替代设计。
注意:我的事件系统尚未完全优化。一些简单的优化包括散列字符串和/或使用无序映射而不是映射。
关键设计要点:
非模板NamedProperty类的示例(非常简单)
class NamedProperty {
public:
explicit NamedProperty();
virtual ~NamedProperty();
};
模板化TypeNamedProperty类的示例
template < typename T_PropertyDataType >
class TypedNamedProperty : public NamedProperty {
public:
friend class NamedProperties;
explicit TypedNamedProperty<T_PropertyDataType>() {}
virtual ~TypedNamedProperty() {}
// Inline Mutators
inline void TypedNamedProperty<T_PropertyDataType>::setData( const T_PropertyDataType & data ) {
m_data = data;
}
inline void TypedNamedProperty<T_PropertyDataType>::getData( T_PropertyDataType & data ) {
data = m_data;
}
private:
T_PropertyDataType m_data;
};
NamedProperties类的示例,它是NamedProperty参数的容器。它返回一个枚举,指示属性检索的状态。因为我正在处理任何类型的0..N可能的params,所以getProperty是一个模板函数,它将为所提供的类型使用动态强制转换。如果动态转换失败(Failed dynamic casts return nullptr),它将设置适当的枚举值并返回。如果成功,它将返回正确的参数。
typedef enum {
PROPERTY_GET_SUCCESS,
PROPERTY_GET_FAILED_NO_SUCH_PROPERTY,
PROPERTY_GET_FAILED_WRONG_TYPE,
PROPERTY_GET_FAILED_NULLPTR,
} PropertyResult;
class NamedProperties {
public:
~NamedProperties();
// Mutators
template <typename T_PropertyDataType>
inline PropertyResult getProperty( const std::string & propertyName, T_PropertyDataType & valueOut ) {
PropertyResult result;
std::map<std::string, NamedProperty *>::iterator itProp;
itProp = m_namedProperties.find( propertyName );
if ( itProp != m_namedProperties.end() ) {
// property found in the map... Need to cast it to check if the type is correct
NamedProperty * retrievedResult = itProp->second;
if ( retrievedResult == nullptr ) {
result = PROPERTY_GET_FAILED_NULLPTR;
return result;
}
TypedNamedProperty<T_PropertyDataType> * propertyWithType = dynamic_cast<TypedNamedProperty<T_PropertyDataType> *>( retrievedResult );
if ( propertyWithType != nullptr ) {
propertyWithType->getData( valueOut );
result = PROPERTY_GET_SUCCESS;
} else {
result = PROPERTY_GET_FAILED_WRONG_TYPE;
}
} else {
// No such property exists in the map
result = PROPERTY_GET_FAILED_NO_SUCH_PROPERTY;
}
return result;
} // end getProperty
template <typename T_PropertyDataType>
inline void setProperty( const std::string & propertyName, const T_PropertyDataType & propertyValue ) {
TypedNamedProperty<T_PropertyDataType> * typedProperty = new TypedNamedProperty<T_PropertyDataType>;
typedProperty->setData( propertyValue );
m_namedProperties.insert( std::pair< std::string, TypedNamedProperty<T_PropertyDataType>*>( propertyName, typedProperty ) );
}
protected:
std::map<std::string, NamedProperty* > m_namedProperties;
};
非模板EventSubscriberBase类的示例,它是存储在EventSystemManager单例中的EventSystemSubscribers数据结构中的基类。注意虚拟的callCallbackFunction是纯虚拟的。
class EventSubscriberBase {
public:
virtual ~EventSubscriberBase() {}
EventSubscriberBase() {}
virtual void callCallbackFunction( NamedProperties & params ) = 0;
virtual void * getSubscriberObjectPointer() = 0;
};
模板化TypedEventSubscriber类的示例
#include "EventSubscriberBase.hpp"
template< typename T_SubscriberType >
class TypedEventSubscriber : public EventSubscriberBase {
typedef void ( T_SubscriberType::*ObjectMemberFunctionType )( NamedProperties & );
public:
virtual ~TypedEventSubscriber<T_SubscriberType>() {}
explicit TypedEventSubscriber<T_SubscriberType>() {
m_subscriberObject = nullptr;
}
virtual void callCallbackFunction( NamedProperties & params ) {
(m_subscriberObject->*m_callbackFunction)(params);
}
void setCallbackFunction( ObjectMemberFunctionType callbackFunction ) {
m_callbackFunction = callbackFunction;
}
void setSubscriber( T_SubscriberType * subscriber ) {
assert( subscriber != nullptr );
m_subscriberObject = subscriber;
}
T_SubscriberType * getSubscriber() {
return m_subscriberObject;
}
virtual void * getSubscriberObjectPointer() {
return m_subscriberObject;
}
private:
T_SubscriberType * m_subscriberObject;
ObjectMemberFunctionType m_callbackFunction;
};
请注意,成员函数指针有一个typedef。这就是使用模板类的原因。这允许任何类通过提供模板参数来注册其类型的回调函数。以下是游戏代码如何注册活动的示例...
在.hpp标题中
void saveFileCallbackFunc( NamedProperties& namedProps );
在.cpp文件中
// Call this function in the CTOR or appropriate initialize function
RegisterEventCallbackForObject( "EventNameForCallbackHere", this, &FireAntGame::saveFileCallbackFunc );
// The callback function definition
void FireAntGame::saveFileCallbackFunc( NamedProperties& namedProps ) {
unsigned int fileBufferSize = 0;
char* fileBuffer = nullptr;
namedProps.getProperty( SF_FILE_BUFFER_STRING, fileBuffer );
namedProps.getProperty( SF_FILE_BUFFER_SIZE_STRING, fileBufferSize );
}
用于注册和触发事件的EventSystem单例类函数实现的示例。要注意的重要细节是单例存储EntitySubscriberBase类指针的映射而不是子模板类TypedEntitySubscriber。
template< typename T_RegisteringObjectType, typename T_ObjectMethodType >
void registerEvent( const std::string & eventName, T_RegisteringObjectType * classObject, T_ObjectMethodType callbackFunction ) {
assert( classObject != nullptr );
TypedEventSubscriber< T_RegisteringObjectType > * eventSubscriber = new TypedEventSubscriber< T_RegisteringObjectType >;
eventSubscriber->setSubscriber( classObject );
eventSubscriber->setCallbackFunction( callbackFunction );
std::map< std::string, std::vector<EventSubscriberBase*>>::iterator it;
it = m_eventSubscribers.find( eventName );
if ( it != m_eventSubscribers.end() ) {
std::vector<EventSubscriberBase*>& eventSubVec = it->second;
eventSubVec.push_back( eventSubscriber );
} else {
// Event did not previously exist
std::vector<EventSubscriberBase*> eventSubVec;
eventSubVec.push_back( eventSubscriber );
std::pair<std::string,std::vector<EventSubscriberBase*>> elementToInsert;
elementToInsert.first = eventName;
elementToInsert.second = eventSubVec;
m_eventSubscribers.insert(elementToInsert);
}
}
void fireEvent( const std::string & eventName, NamedProperties & params ) {
std::map<std::string,std::vector<EventSubscriberBase*>>::iterator itFind;
itFind = m_eventSubscribers.find( eventName );
if ( itFind != m_eventSubscribers.end() ) {
// Someone has registered for this event
std::vector<EventSubscriberBase*> & subscribersVector = itFind->second;
for ( size_t i = 0; i < subscribersVector.size(); ++i ) {
EventSubscriberBase * eventSubscriber = subscribersVector[i];
eventSubscriber->callCallbackFunction( params );
}
}
}
这种设计的优点在于任何对象都可以在没有继承的情况下注册事件。 这也意味着不必为所涉及的每种可能的类类型创建枚举。它还允许通过NamedProperties类在回调函数中传递0..N参数。如果在从NamedProperties类请求属性时不存在属性,则返回默认值(如果您愿意,可以由用户指定)。
最终,关键的用处是使用带有模板子类的非模板基类。没有这种设计习语,它就会崩溃。这允许我们的数据结构利用基类指针的多态性,该指针可以调用任何成员函数类型的回调函数,或者通过任何类型的NamedProperties传入参数!在我看来,这为事件系统提供了最大的灵活性。
对不起,很长的帖子!希望这为实现事件系统提供了一个想法。