c++ design-patterns design circular-dependency tightly-coupled-code

c++ - Objetos de juego hablando el uno al otro



design-patterns design (7)

Aquí hay un sistema de eventos ordenado escrito para C ++ 11 que puede usar. Utiliza plantillas y punteros inteligentes, así como lambdas para los delegados. Es muy flexible. A continuación también encontrará un ejemplo. Envíeme un correo electrónico a [email protected] si tiene preguntas sobre esto.

Lo que estas clases te dan es una forma de enviar eventos con datos arbitrarios adjuntos y una manera fácil de vincular directamente funciones que aceptan tipos de argumentos ya convertidos que el sistema lanza y verifica la conversión correcta antes de llamar a tu delegado.

Básicamente, cada evento se deriva de la clase IEventData (puede llamarlo IEvent si lo desea). Cada "cuadro" se llama ProcessEvents () en cuyo punto el sistema de eventos recorre todos los delegados y llama a los delegados que han sido suministrados por otros sistemas que se han suscrito a cada tipo de evento. Cualquiera puede elegir a qué eventos desea suscribirse, ya que cada tipo de evento tiene una ID única. También puedes usar lambdas para suscribirte a eventos como este: AddListener (MyEvent :: ID (), [&] (shared_ptr ev) {haz lo que quieras} ...

De todos modos, aquí está la clase con toda la implementación:

#pragma once #include <list> #include <memory> #include <map> #include <vector> #include <functional> class IEventData { public: typedef size_t id_t; virtual id_t GetID() = 0; }; typedef std::shared_ptr<IEventData> IEventDataPtr; typedef std::function<void(IEventDataPtr&)> EventDelegate; class IEventManager { public: virtual bool AddListener(IEventData::id_t id, EventDelegate proc) = 0; virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) = 0; virtual void QueueEvent(IEventDataPtr ev) = 0; virtual void ProcessEvents() = 0; }; #define DECLARE_EVENT(type) / static IEventData::id_t ID(){ / return reinterpret_cast<IEventData::id_t>(&ID); / } / IEventData::id_t GetID() override { / return ID(); / }/ class EventManager : public IEventManager { public: typedef std::list<EventDelegate> EventDelegateList; ~EventManager(){ } //! Adds a listener to the event. The listener should invalidate itself when it needs to be removed. virtual bool AddListener(IEventData::id_t id, EventDelegate proc) override; //! Removes the specified delegate from the list virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) override; //! Queues an event to be processed during the next update virtual void QueueEvent(IEventDataPtr ev) override; //! Processes all events virtual void ProcessEvents() override; private: std::list<std::shared_ptr<IEventData>> mEventQueue; std::map<IEventData::id_t, EventDelegateList> mEventListeners; }; //! Helper class that automatically handles removal of individual event listeners registered using OnEvent() member function upon destruction of an object derived from this class. class EventListener { public: //! Template function that also converts the event into the right data type before calling the event listener. template<class T> bool OnEvent(std::function<void(std::shared_ptr<T>)> proc){ return OnEvent(T::ID(), [&, proc](IEventDataPtr data){ auto ev = std::dynamic_pointer_cast<T>(data); if(ev) proc(ev); }); } protected: typedef std::pair<IEventData::id_t, EventDelegate> _EvPair; EventListener(std::weak_ptr<IEventManager> mgr):_els_mEventManager(mgr){ } virtual ~EventListener(){ if(_els_mEventManager.expired()) return; auto em = _els_mEventManager.lock(); for(auto i : _els_mLocalEvents){ em->RemoveListener(i.first, i.second); } } bool OnEvent(IEventData::id_t id, EventDelegate proc){ if(_els_mEventManager.expired()) return false; auto em = _els_mEventManager.lock(); if(em->AddListener(id, proc)){ _els_mLocalEvents.push_back(_EvPair(id, proc)); } } private: std::weak_ptr<IEventManager> _els_mEventManager; std::vector<_EvPair> _els_mLocalEvents; //std::vector<_DynEvPair> mDynamicLocalEvents; };

Y el archivo Cpp:

#include "Events.hpp" using namespace std; bool EventManager::AddListener(IEventData::id_t id, EventDelegate proc){ auto i = mEventListeners.find(id); if(i == mEventListeners.end()){ mEventListeners[id] = list<EventDelegate>(); } auto &list = mEventListeners[id]; for(auto i = list.begin(); i != list.end(); i++){ EventDelegate &func = *i; if(func.target<EventDelegate>() == proc.target<EventDelegate>()) return false; } list.push_back(proc); } bool EventManager::RemoveListener(IEventData::id_t id, EventDelegate proc){ auto j = mEventListeners.find(id); if(j == mEventListeners.end()) return false; auto &list = j->second; for(auto i = list.begin(); i != list.end(); ++i){ EventDelegate &func = *i; if(func.target<EventDelegate>() == proc.target<EventDelegate>()) { list.erase(i); return true; } } return false; } void EventManager::QueueEvent(IEventDataPtr ev) { mEventQueue.push_back(ev); } void EventManager::ProcessEvents(){ size_t count = mEventQueue.size(); for(auto it = mEventQueue.begin(); it != mEventQueue.end(); ++it){ printf("Processing event../n"); if(!count) break; auto &i = *it; auto listeners = mEventListeners.find(i->GetID()); if(listeners != mEventListeners.end()){ // Call listeners for(auto l : listeners->second){ l(i); } } // remove event it = mEventQueue.erase(it); count--; } }

Utilizo una clase EventListener por conveniencia como clase base para cualquier clase que quiera escuchar eventos. Si deriva su clase de escucha de esta clase y la suministra con su administrador de eventos, puede usar la muy conveniente función OnEvent (...) para registrar sus eventos. Y la clase base cancelará automáticamente su clase derivada de todos los eventos cuando se destruya. Esto es muy conveniente ya que olvidarse de eliminar un delegado del administrador de eventos cuando se destruye su clase casi seguramente hará que su programa falle.

Una forma clara de obtener un ID de tipo único para un evento simplemente declarando una función estática en la clase y luego convirtiendo su dirección en un int. Dado que cada clase tendrá este método en diferentes direcciones, se puede utilizar para la identificación única de eventos de clase. También puede convertir typename () en int para obtener un id único si lo desea. Hay maneras diferentes de hacer esto.

Así que aquí hay un ejemplo sobre cómo usar esto:

#include <functional> #include <memory> #include <stdio.h> #include <list> #include <map> #include "Events.hpp" #include "Events.cpp" using namespace std; class DisplayTextEvent : public IEventData { public: DECLARE_EVENT(DisplayTextEvent); DisplayTextEvent(const string &text){ mStr = text; } ~DisplayTextEvent(){ printf("Deleted event data/n"); } const string &GetText(){ return mStr; } private: string mStr; }; class Emitter { public: Emitter(shared_ptr<IEventManager> em){ mEmgr = em; } void EmitEvent(){ mEmgr->QueueEvent(shared_ptr<IEventData>( new DisplayTextEvent("Hello World!"))); } private: shared_ptr<IEventManager> mEmgr; }; class Receiver : public EventListener{ public: Receiver(shared_ptr<IEventManager> em) : EventListener(em){ mEmgr = em; OnEvent<DisplayTextEvent>([&](shared_ptr<DisplayTextEvent> data){ printf("It''s working: %s/n", data->GetText().c_str()); }); } ~Receiver(){ mEmgr->RemoveListener(DisplayTextEvent::ID(), std::bind(&Receiver::OnExampleEvent, this, placeholders::_1)); } void OnExampleEvent(IEventDataPtr &data){ auto ev = dynamic_pointer_cast<DisplayTextEvent>(data); if(!ev) return; printf("Received event: %s/n", ev->GetText().c_str()); } private: shared_ptr<IEventManager> mEmgr; }; int main(){ auto emgr = shared_ptr<IEventManager>(new EventManager()); Emitter emit(emgr); { Receiver receive(emgr); emit.EmitEvent(); emgr->ProcessEvents(); } emit.EmitEvent(); emgr->ProcessEvents(); emgr = 0; return 0; }

¿Cuál es una buena forma de tratar con los objetos y hacer que hablen entre ellos?

Hasta ahora, todos mis juegos hobby / student han sido pequeños, por lo que este problema generalmente se resolvió de una manera bastante fea, lo que llevó a una estrecha integración y dependencias circulares. Lo cual estuvo bien para el tamaño de los proyectos que estaba haciendo.

Sin embargo, mis proyectos se han vuelto cada vez más grandes en tamaño y complejidad y ahora quiero empezar a volver a usar el código y hacer que mi cabeza sea un lugar más simple.

El principal problema que tengo es generalmente lo mismo que el Player necesita saber sobre el Map y también lo hace el Enemy , esto generalmente ha descendido a establecer muchos apuntadores y tener muchas dependencias, y esto se convierte en un desastre rápidamente.

He pensado en la línea de un sistema de estilo de mensaje. pero realmente no veo cómo esto reduce las dependencias, ya que todavía estaría enviando los punteros a todas partes.

PD: Supongo que esto ya se ha discutido antes, pero no sé cómo se llama simplemente la necesidad que tengo.


EDITAR: A continuación describo un sistema básico de mensajes de eventos que he usado una y otra vez. Y se me ocurrió que ambos proyectos escolares son de código abierto y en la web. Puede encontrar la segunda versión de este sistema de mensajería (y bastante más) en http://sourceforge.net/projects/bpfat/ .. ¡Disfrútelo y lea a continuación para obtener una descripción más detallada del sistema!

Escribí un sistema de mensajería genérico y lo introduje en un puñado de juegos que se han lanzado en PSP, así como en algunos softwares de aplicaciones de nivel empresarial. El objetivo del sistema de mensajería es pasar solo los datos que se necesitan para procesar un mensaje o evento, según la terminología que desee utilizar, de modo que los objetos no tengan que conocerse entre sí.

Un resumen rápido de la lista de objetos utilizados para lograr esto es algo así como:

struct TEventMessage { int _iMessageID; } class IEventMessagingSystem { Post(int iMessageId); Post(int iMessageId, float fData); Post(int iMessageId, int iData); // ... Post(TMessageEvent * pMessage); Post(int iMessageId, void * pData); } typedef float(*IEventMessagingSystem::Callback)(TEventMessage * pMessage); class CEventMessagingSystem { Init (); DNit (); Exec (float fElapsedTime); Post (TEventMessage * oMessage); Register (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback* fpMethod); Unregister (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback * fpMethod); } #define MSG_Startup (1) #define MSG_Shutdown (2) #define MSG_PlaySound (3) #define MSG_HandlePlayerInput (4) #define MSG_NetworkMessage (5) #define MSG_PlayerDied (6) #define MSG_BeginCombat (7) #define MSG_EndCombat (8)

Y ahora un poco de una explicación. El primer objeto, TEventMessage, es el objeto base para representar los datos enviados por el sistema de mensajería. De forma predeterminada, siempre tendrá el ID del mensaje enviado, por lo tanto, si desea asegurarse de haber recibido un mensaje que esperaba, puede hacerlo (generalmente solo lo hago en depuración).

El siguiente paso es la clase de interfaz que le da un objeto genérico para que el sistema de mensajería lo use para lanzar al hacer devoluciones de llamada. Además, esto también proporciona una interfaz "fácil de usar" para publicar () diferentes tipos de datos en el sistema de mensajería.

Después tenemos nuestro Callback typedef, en pocas palabras, espera un objeto del tipo de la clase de interfaz y pasará por un puntero TEventMessage ... Opcionalmente puede hacer que el parámetro sea const pero he usado el procesamiento de goteo antes para cosas como depuración de pila y tal del sistema de mensajería.

Por último y en el núcleo está el objeto CEventMessagingSystem. Este objeto contiene una matriz de pilas de objetos de devolución de llamada (o listas o colas vinculadas o como quiera almacenar los datos). Los objetos de devolución de llamada, que no se muestran arriba, necesitan mantener (y están definidos de forma única) un puntero al objeto, así como el método para invocar dicho objeto. Cuando te registras () agregas una entrada en la pila de objetos debajo de la posición de la matriz del ID del mensaje. Cuando anula el registro () elimina esa entrada.

Eso es básicamente eso. Ahora bien, esto tiene la estipulación de que todo lo que necesita saber sobre el IEventMessagingSystem y el objeto TEventMessage ... pero este objeto no debería cambiar tan a menudo y solo pasa las partes de información que son vitales para la lógica dictada por el evento al que se llama. De esta forma, un jugador no necesita saber sobre el mapa o el enemigo directamente para enviar eventos a él. Un objeto administrado también puede llamar a una API a un sistema más grande, sin necesidad de saber nada al respecto.

Por ejemplo: cuando un enemigo muere, quieres que reproduzca un efecto de sonido. Suponiendo que tiene un administrador de sonido que hereda la interfaz IEventMessagingSystem, configuraría una devolución de llamada para el sistema de mensajería que aceptaría un TEventMessagePlaySoundEffect o algo así. El Administrador de sonido luego registrará esta devolución de llamada cuando los efectos de sonido estén habilitados (o anule el registro de la devolución de llamada cuando desee silenciar todos los efectos de sonido para facilitar las funciones de encendido / apagado). A continuación, tendría el objeto enemigo también heredaría de IEventMessagingSystem, armaría un objeto TEventMessagePlaySoundEffect (necesitaría MSG_PlaySound para su ID de mensaje y luego la ID del efecto de sonido para reproducir, ya sea una ID int o el nombre del sonido efecto) y simplemente llame a Post (& oEventMessagePlaySoundEffect).

Ahora, este es solo un diseño muy simple sin implementación. Si tiene una ejecución inmediata, entonces no necesita almacenar el búfer en los objetos TEventMessage (lo que utilicé principalmente en juegos de consola). Si se encuentra en un entorno de subprocesos múltiples, esta es una forma muy bien definida para que los objetos y sistemas que se ejecutan en hilos separados hablen entre sí, pero querrá conservar los objetos TEventMessage para que los datos estén disponibles durante el procesamiento.

Otra modificación es para objetos que solo necesitan datos de Post (), puede crear un conjunto estático de métodos en IEventMessagingSystem para que no tengan que heredar de ellos (se usa para facilitar el acceso y las capacidades de devolución de llamada, no directamente - necesario para las llamadas Post ()).

Para todas las personas que mencionan MVC, es un patrón muy bueno, pero puede implementarlo de muchas maneras diferentes y en diferentes niveles. El proyecto actual en el que estoy trabajando profesionalmente es una configuración de MVC aproximadamente 3 veces más, existe el MVC global de toda la aplicación y luego diseño, cada MV y C también es un patrón MVC autónomo. Entonces, lo que he intentado hacer aquí es explicar cómo hacer una C que sea lo suficientemente genérica como para manejar casi cualquier tipo de M sin la necesidad de acceder a una Vista ...

Por ejemplo, un objeto cuando ''muere'' podría querer reproducir un efecto de sonido. Haría una estructura para el sistema de sonido como TEventMessageSoundEffect que hereda de TEventMessage y agrega un ID de efecto de sonido (ya sea un Int precargado, o el nombre del archivo sfx, sin embargo, se rastrean en su sistema). Entonces, todo el objeto solo necesita juntar un objeto TEventMessageSoundEffect con el ruido Death apropiado y el mensaje de llamada (& oEventMessageSoundEffect); objeto ... Asumiendo que el sonido no esté silenciado (lo que desearía Anular el registro de los administradores de sonido.

EDITAR: Para aclarar esto un poco con respecto al comentario a continuación: Cualquier objeto para enviar o recibir un mensaje solo necesita saber sobre la interfaz IEventMessagingSystem, y este es el único objeto que EventMessagingSystem necesita conocer de todos los demás objetos. Esto es lo que te da el desapego. Cualquier objeto que quiera recibir un mensaje simplemente se registrará (MSG, Object, Callback). Luego, cuando un objeto llama a Post (MSG, Data) y lo envía al EventMessagingSystem a través de la interfaz que conoce, el EMS notificará a cada objeto registrado del evento. Podrías hacer un MSG_PlayerDied manejado por otros sistemas, o el jugador puede llamar a MSG_PlaySound, MSG_Respawn, etc. para que las cosas que escuchan esos mensajes actúen sobre ellos. Piense en la Publicación (MSG, Datos) como una API abstraída a los diferentes sistemas dentro de un motor de juego.

Oh! Otra cosa que me fue señalada. El sistema que describo arriba se ajusta al patrón Observer en la otra respuesta dada. Entonces, si quiere una descripción más general para que la mía tenga más sentido, es un breve artículo que le da una buena descripción.

¡Espero que esto ayude y disfrútalo!


Esto probablemente no solo se aplica a las clases de juego, sino a las clases en el sentido general. el patrón MVC (modelo-vista-controlador) junto con su bomba de mensajes sugeridos es todo lo que necesita.

"Enemigo" y "Jugador" probablemente encajen en la parte Modelo de MVC, no importa mucho, pero la regla general es que todos los modelos y vistas interactúen a través del controlador. Entonces, desearía mantener referencias (mejores que punteros) a (casi) todas las demás instancias de clase de esta clase ''controladora'', llamémosle ControlDispatcher. Agregue un mensaje de bomba a él (varía según la plataforma en la que esté codificando), ejemplifíquelo primero (antes que cualquier otra clase y tenga los otros objetos como parte de él) o por último (y tenga los otros objetos almacenados como referencias en ControlDispatcher).

Por supuesto, la clase ControlDispatcher probablemente tendrá que dividirse más en controladores más especializados solo para mantener el código por archivo en alrededor de 700-800 líneas (este es el límite al menos para mí) e incluso puede tener más hilos de bombeo y procesando mensajes dependiendo de sus necesidades.

Aclamaciones


La mensajería es definitivamente una excelente manera de hacerlo, pero los sistemas de mensajería pueden tener muchas diferencias. Si desea mantener sus clases agradables y limpias, escríbalas para ignorar un sistema de mensajería y en su lugar haga que tomen dependencias de algo simple como un ''ILocationService'' que luego puede implementarse para publicar / solicitar información de elementos como la clase Map. . Si bien terminarás con más clases, serán pequeñas, simples y alentarán el diseño limpio.

La mensajería es más que un desacoplamiento, también le permite avanzar hacia una arquitectura más asincrónica, concurrente y reactiva. Patrones de integración empresarial por Gregor Hophe es un excelente libro que habla de buenos patrones de mensajería. La implementación de Erlang OTP o Scala del Actor Pattern me ha proporcionado mucha orientación.


La sugerencia de @kellogs de MVC es válida y se usa en algunos juegos, aunque es mucho más común en aplicaciones y marcos web. Puede ser excesivo y demasiado para esto.

Repensaría tu diseño, ¿por qué el Jugador necesita hablar con Enemigos? ¿No podrían ambos heredar de una clase Actor? ¿Por qué los actores necesitan hablar con el mapa?

A medida que leo lo que escribí, empieza a encajar en un marco MVC ... Obviamente, he trabajado demasiados rieles últimamente. Sin embargo, estaría dispuesto a apostar, solo necesitan saber cosas como, están colisionando con otro Actor, y tienen una posición, que debería ser relativa al Mapa de todos modos.

Aquí hay una implementación de Asteroids que trabajé. Tu juego puede ser, y probablemente sea, complejo.


Tenga cuidado con "un sistema de estilo de mensaje", probablemente dependa de la implementación, pero normalmente perderá la comprobación de tipo estático y puede hacer que algunos errores sean muy difíciles de depurar. Tenga en cuenta que al llamar a los métodos del objeto ya es un sistema similar a un mensaje.

Probablemente simplemente te faltan algunos niveles de abstracción, por ejemplo, para la navegación, un jugador podría usar un navegador en lugar de conocer todo sobre el mapa en sí. También dice que this has usually descended into setting lots of pointers , ¿cuáles son esos indicadores? Probablemente, les está dando una abstracción equivocada ... Hacer que los objetos sepan sobre los demás directamente, sin pasar por interfaces e intermedios, es una forma directa de obtener un diseño estrechamente unido.