¿Hay una mejor manera de configurar un sistema de eventos?

9

Los sistemas de eventos son increíbles, hacen un código domador extremadamente difícil de manejar y realmente permiten la creación dinámica de juegos a través de la comunicación fácil de objetos y el bucle del juego. Estoy teniendo dificultades con la eficiencia de mi implementación actual. Actualmente, mi ligera optimización de separar las listas de objetos en los eventos a los que responden ha hecho maravillas, pero debería haber más que pueda hacer.

Actualmente tengo dos métodos:

  1. Más simple: todos los objetos se agregan a un vector cuando se envía un evento a todos los objetos se les envía el evento a través de su método handle_event ()

  2. Más complejo: tengo un mapa con cadena como clave e int como valor. Cuando se agrega un tipo de evento, se agrega a este mapa, con el int simplemente incrementado (debe haber una mejor manera)
    el vector de vectores de objetos y luego empuja hacia atrás un nuevo vector para manejar ese tipo de evento.
    Cuando se llama a un evento, simplemente llama al int correspondiente en el mapa eventTypes al tipo dentro del vector de vectores de objetos y envía ese evento a cada objeto que maneja ese tipo de evento.

Este primer método es bastante lento (obviamente) para muchos objetos, pero bastante rápido para muy pocos objetos. Mientras que el segundo método es bastante rápido con objetos grandes que desean manejar diferentes tipos de eventos, pero más lento que el primer método por objeto con objetos que manejan el mismo tipo de evento.

¿Hay una manera más rápida (en cuanto al tiempo de ejecución)? ¿Hay una forma más rápida de buscar un int desde el tipo de cadena? (Inicialmente tenía una enumeración, pero no permitía tipos personalizados, que son necesarios debido al nivel de dinamismo deseado).

ultifinitus
fuente
1
Esto es para lo que sirven los mapas de hash (o la tabla de hash), la cadena se calcula hasta un número de hash que luego se usa para buscar directamente en una matriz. en.wikipedia.org/wiki/Hash_table
Patrick Hughes
Una buena pregunta es: ¿se trata realmente de un cuello de botella en el rendimiento o simplemente te estás preocupando prematuramente?
Jari Komppa
Ya está implementado y hay un cuello de botella cuando se usan muchos objetos. Mi comentario anterior sobre la falta de cuello de botella no estaba en la aplicación en sí, sino en la diferencia de velocidad entre las dos implementaciones anteriores donde se manejaba la misma cantidad de objetos. Esperaba otros métodos para crear sistemas de eventos ... Sin embargo, algunas de las ideas en este hilo aumentarán bastante la velocidad, especialmente en la carga del sistema de eventos (tiempo de carga de 11-25 segundos completos con 100000 objetos)
ultifinitus
100K objetos suenan terriblemente alto solo por un juego. ¿Es este un servidor o una aplicación cliente de usuario final?
Patrick Hughes
Este es mi motor, así que realmente voy por la flexibilidad. Se usará en aplicaciones de servidor y aplicaciones de usuario final (con menos frecuencia la primera, optimización)
ultifinitus

Respuestas:

5

Parece que estás diciendo que el gran cuello de botella de rendimiento está buscando ID de eventos (enteros) de sus nombres de cadena. Podrías preprocesar los datos de tu juego para convertir todos los nombres de eventos en enteros antes de ejecutar el juego, o posiblemente mientras cargas el nivel; entonces no tendrías que hacer ninguna conversión durante el juego.

Si los objetos se crean y destruyen con frecuencia, puede haber mucha rotación en los vectores de los objetos. En este caso, podría beneficiarse utilizando listas vinculadas en lugar de vectores; son más rápidos para la inserción y eliminación.

Nathan Reed
fuente
El cuello de botella no es grande (para 100,000 objetos pierde .0000076 ms / objeto) pero creo que su idea es una gran idea. Creo que en realidad haré una búsqueda única del id, y tendré el eventID almacenado como un int en lugar de los datos de la cadena original. Y en realidad no había pensado en listas enlazadas, buena idea.
ultifinitus
1
+1 Para preprocesar las ID. También podría hacerlo perezosamente al tener un tipo EventType que cada módulo podría obtener a través de algo así EventType takeDamageEvent = EventSystem::CacheEventType("takeDamageEvent");. Conviértalo en un miembro estático de las clases y solo tendrá una copia flotando en cada clase que lo necesite.
michael.bartnett
2
Tenga en cuenta que almacenar identificadores en lugar de cadenas hará que la depuración sea algo difícil. Siempre es útil poder ver un texto que especifica de dónde proviene un objeto. Puede ir a la mitad almacenando tanto la identificación como la cadena, que mantiene alrededor para fines de depuración (tal vez incluso se elimine en las versiones de lanzamiento).
Nicol Bolas
2

Bueno, primero saquemos las cosas simples del camino. Tiene esto mapentre cadenas (el nombre del evento, presumiblemente) y enteros (el índice de los oyentes de eventos registrados).

El tiempo de búsqueda en a mapse basa en dos cosas: la cantidad de elementos en el mapa y el tiempo que lleva hacer la comparación entre dos claves (ignorando los problemas de caché). Si el tiempo de búsqueda es un problema, una forma de manejarlo es cambiar la función de comparación cambiando el tipo de cadena.

Asumamos que estás usando std::stringy operator<para comparación. Esto es altamente ineficiente; hace una comparación byte-wise. No te importa la cadena real menos que la comparación; solo necesita algún tipo de comparación que dé un orden estrictamente débil (porque mapno funciona de otra manera).

Por lo tanto, debe usar una cadena de longitud fija de 32 bytes en lugar de std::string. Los uso para identificadores. Las pruebas de comparación para estas cadenas fijas no hacen comparaciones por bytes; hacen comparaciones de 32 bits (o 64 bits) en su lugar. Toma cada 4 bytes como un entero sin signo y lo compara con los 4 bytes correspondientes de la otra cadena. De esa manera, la comparación solo toma como máximo 8 comparaciones. Asegura un orden estrictamente débil, aunque el orden no tiene nada que ver con los datos como caracteres.

El almacenamiento de cadenas de más de 31 bytes (necesita el carácter NULL) trunca la cadena (pero desde el medio, en lugar de los extremos. Encuentro que la entropía tiende a ser mayor al principio y al final). Y las cadenas más cortas que eso rellenan los caracteres restantes con \0.

Ahora, puedes deshacerte del maptodo y usar una tabla hash. Si realmente tiene más de 100,000 tipos diferentes de tipos de eventos, esta podría ser una buena idea. Pero no sé de un juego en el que eso sería algo remotamente razonable.

Nicol Bolas
fuente
Por lo tanto, debe usar una cadena de longitud fija de 32 bytes en lugar de std :: string. ¡Fantástico! No había pensado en cambiar el tipo de cadena.
ultifinitus
0

Para responder a la pregunta general:

Mejor manera de configurar un sistema de eventos

No hay ninguno. Todo lo que puede hacer es identificar las necesidades específicas que tendrá para sus sistemas de eventos (puede haber varias diferentes) y luego usar la herramienta adecuada para el trabajo correcto.

He implementado e intentado generalizar toneladas de sistemas de eventos y he llegado a esta conclusión. Debido a que los intercambios son realmente diferentes si lo hace en tiempo de compilación o en tiempo de ejecución, si usa jerarquías de objetos o pizarras, etc.

La mejor manera es estudiar los diferentes tipos de sistemas de eventos y luego tendrás pistas sobre cuál usar en cada caso.

Ahora, si desea el sistema realmente más flexible, implemente un sistema de pizarra (tiempo de ejecución) con propiedades dinámicas de eventos y tendrá algo muy flexible pero quizás muy lento.

Klaim
fuente