¡Buena pregunta! Antes de llegar a las preguntas específicas que hizo, diré: no subestime el poder de la simplicidad. Tenpn tiene razón. Tenga en cuenta que todo lo que intenta hacer con estos enfoques es encontrar una manera elegante de diferir una llamada a la función o desacoplar a la persona que llama de la persona que llama. Puedo recomendar corutinas como una forma sorprendentemente intuitiva de aliviar algunos de esos problemas, pero eso está un poco fuera de tema. A veces, es mejor simplemente llamar a la función y vivir con el hecho de que la entidad A está acoplada directamente a la entidad B. Ver YAGNI.
Dicho esto, he usado y estoy contento con el modelo de señal / ranura combinado con el simple paso de mensajes. Lo usé en C ++ y Lua para un título de iPhone bastante exitoso que tenía un calendario muy apretado.
Para el caso de señal / ranura, si quiero que la entidad A haga algo en respuesta a algo que hizo la entidad B (por ejemplo, abrir una puerta cuando algo muere), podría hacer que la entidad A se suscriba directamente al evento de muerte de la entidad B. O posiblemente la entidad A se suscribirá a cada uno de un grupo de entidades, incrementará un contador en cada evento disparado y abrirá la puerta después de que N de ellos hayan muerto. Además, "grupo de entidades" y "N de ellas" normalmente se definirían por diseñador en los datos de nivel. (Por otro lado, esta es un área donde las corutinas realmente pueden brillar, por ejemplo, WaitForMultiple ("Dying", entA, entB, entC); door.Unlock ();)
Pero eso puede volverse engorroso cuando se trata de reacciones estrechamente acopladas al código C ++, o eventos inherentemente efímeros del juego: infligir daño, recargar armas, depurar, retroalimentación de IA basada en la ubicación impulsada por el jugador. Aquí es donde el mensaje que pasa puede llenar los vacíos. Básicamente se reduce a algo así como "diga a todas las entidades en esta área que sufran daños en 3 segundos" o "cada vez que complete la física para descubrir a quién disparé, dígales que ejecuten esta función de script". Es difícil descubrir cómo hacerlo de manera agradable usando publicación / suscripción o señal / ranura.
Esto puede ser fácilmente exagerado (en comparación con el ejemplo de tenpn). También puede ser una hinchazón ineficiente si tienes mucha acción. Pero a pesar de sus inconvenientes, este enfoque de "mensajes y eventos" encaja muy bien con el código de juego con guión (por ejemplo, en Lua). El código del script puede definir y reaccionar a sus propios mensajes y eventos sin que el código C ++ se preocupe en absoluto. Y el código de script puede enviar fácilmente mensajes que activan el código C ++, como cambiar niveles, reproducir sonidos o incluso dejar que un arma establezca cuánto daño produce el mensaje TakeDamage. Me ahorró un montón de tiempo porque no tenía que perder el tiempo constantemente con luabind. Y me permitió mantener todo mi código luabind en un solo lugar, porque no había mucho. Cuando está correctamente acoplado,
Además, mi experiencia con el caso de uso # 2 es que es mejor manejarlo como un evento en la otra dirección. En lugar de preguntar cuál es la salud de la entidad, dispare un evento / envíe un mensaje cada vez que la salud haga un cambio significativo.
En términos de interfaces, por cierto, terminé con tres clases para implementar todo esto: EventHost, EventClient y MessageClient. EventHosts crea ranuras, EventClients se suscribe / conecta a ellos y MessageClients asocia un delegado con un mensaje. Tenga en cuenta que el objetivo delegado de un MessageClient no necesariamente tiene que ser el mismo objeto que posee la asociación. En otras palabras, MessageClients puede existir únicamente para reenviar mensajes a otros objetos. FWIW, la metáfora de host / cliente es algo inapropiada. Fuente / sumidero podrían ser mejores conceptos.
Lo siento, divagué un poco allí. Es mi primera respuesta :) Espero que tenga sentido.
Preguntaste cómo lo hacen los juegos comerciales. ;)
fuente
Una respuesta más seria:
He visto que las pizarras se usan mucho. Las versiones simples no son más que puntales que se actualizan con cosas como el HP de una entidad, que las entidades pueden consultar.
Sus pizarras pueden ser la vista del mundo de esta entidad (pregunte en la pizarra de B cuál es su HP), o la vista de una entidad del mundo (A consulta su pizarra para ver cuál es el HP del objetivo de A).
Si solo actualiza las pizarras en un punto de sincronización en el marco, puede leerlas en un punto posterior desde cualquier subproceso, haciendo que el subprocesamiento múltiple sea bastante sencillo de implementar.
Las pizarras más avanzadas pueden ser más como tablas hash, asignando cadenas a valores. Esto es más fácil de mantener pero obviamente tiene un costo de tiempo de ejecución.
Tradicionalmente, una pizarra es solo una comunicación unidireccional: no manejaría la distribución de daños.
fuente
long long int
He estudiado un poco este tema y he visto una buena solución.
Básicamente se trata de subsistemas. Es similar a la idea de pizarra mencionada por tenpn.
Las entidades están hechas de componentes, pero son solo bolsas de propiedades. No se implementa ningún comportamiento en las propias entidades.
Digamos que las entidades tienen un componente de salud y un componente de daño.
Luego tiene algunos MessageManager y tres subsistemas: ActionSystem, DamageSystem, HealthSystem. En un momento, ActionSystem hace sus cálculos sobre el mundo del juego y genera un evento:
Este evento se publica en el Administrador de mensajes. Ahora, en un momento dado, el MessageManager revisa los mensajes pendientes y descubre que el DamageSystem se ha suscrito a los mensajes HIT. Ahora el MessageManager entrega el mensaje HIT al DamageSystem. El DamageSystem revisa su lista de entidades que tienen un componente de Daño, calcula los puntos de daño según el poder de golpe o algún otro estado de ambas entidades, etc. y publica el evento
HealthSystem se ha suscrito a los mensajes DAMAGE y ahora, cuando el MessageManager publica el mensaje DAMAGE en HealthSystem, HealthSystem tiene acceso a ambas entidades entity_A y entity_B con sus componentes Health, por lo que nuevamente HealthSystem puede hacer sus cálculos (y tal vez publicar el evento correspondiente). al administrador de mensajes).
En dicho motor de juegos, el formato de los mensajes es el único acoplamiento entre todos los componentes y subsistemas. Los subsistemas y entidades son completamente independientes y no se conocen entre sí.
No sé si algún motor de juego real ha implementado esta idea o no, pero parece bastante sólido y limpio y espero algún día implementarlo yo mismo para mi motor de juego de nivel aficionado.
fuente
entity_b->takeDamage();
)¿Por qué no tener una cola de mensajes global, algo como:
Con:
Y al final del juego de bucle / manejo de eventos:
Creo que este es el patrón de comando. Y
Execute()
es un virtual puro en elEvent
que los derivados definen y hacen cosas. Entonces aquí:fuente
Si tu juego es para un jugador, solo usa el método de objetos objetivo (como se sugiere).
Si es (o quiere admitir) multijugador (multicliente para ser exactos), use una cola de comandos.
fuente
Yo diría: no use ninguno, siempre que no necesite explícitamente retroalimentación instantánea del daño.
La entidad / componente que toma el daño / lo que sea debe empujar los eventos a una cola de eventos local o a un sistema en un nivel igual que contenga eventos de daños.
Entonces debería haber un sistema de superposición con acceso a ambas entidades que solicite los eventos de la entidad a y lo pase a la entidad b. Al no crear un sistema de eventos general que cualquier cosa pueda usar desde cualquier lugar para pasar un evento a cualquier cosa en cualquier momento, crea un flujo de datos explícito que siempre hace que el código sea más fácil de depurar, más fácil de medir el rendimiento, más fácil de entender y leer y, a menudo conduce a un sistema más bien diseñado en general.
fuente
Solo haz la llamada. No haga request-hp seguido de query-hp; si sigue ese modelo, se encontrará con un mundo de dolor.
Es posible que desee echar un vistazo a las Continuaciones Mono también. Creo que sería ideal para los NPC.
fuente
Entonces, ¿qué sucede si los jugadores A y B intentan golpearse entre sí en el mismo ciclo de actualización ()? Supongamos que la Actualización () para el jugador A ocurre antes de la Actualización () para el jugador B en el Ciclo 1 (o marca, o como se llame). Hay dos escenarios en los que puedo pensar:
Procesamiento inmediato a través de un mensaje:
Esto es injusto, los jugadores A y B deberían golpearse entre sí, el jugador B murió antes de golpear A solo porque esa entidad / objeto de juego recibió una actualización () más tarde.
Poniendo en cola el mensaje
De nuevo, esto es injusto ... ¡se supone que el jugador A toma los puntos de golpe en el mismo turno / ciclo / tic!
fuente
pEntity->Flush( pMessages );
. Cuando entidad_A genera un nuevo evento, no es leído por entidad_B en ese marco (también tiene la posibilidad de tomar la poción), entonces ambos reciben daño y luego procesan el mensaje de curación de poción que sería el último en la cola . El jugador B aún muere de todos modos ya que el mensaje de poción es el último en la cola: P, pero puede ser útil para otro tipo de mensajes, como borrar punteros a entidades muertas.