¿Cómo funciona la comunicación de la entidad?

115

Tengo dos casos de usuario:

  1. ¿Cómo entity_Aenviaría un take-damagemensaje a entity_B?
  2. ¿Cómo sería la entity_Aconsulta entity_Bde HP?

Esto es lo que he encontrado hasta ahora:

  • Cola de mensajes
    1. entity_Acrea un take-damagemensaje y lo publica en entity_Bla cola de mensajes.
    2. entity_Acrea un query-hpmensaje y lo publica entity_B. entity_Ba cambio crea un response-hpmensaje y lo publica entity_A.
  • Publicar / Suscribir
    1. entity_Bse suscribe a los take-damagemensajes (posiblemente con algún filtrado preventivo para que solo se entreguen mensajes relevantes). entity_Aproduce take-damagemensaje que hace referencia entity_B.
    2. entity_Ase suscribe a update-hpmensajes (posiblemente filtrados). Cada cuadro entity_Btransmite update-hpmensajes.
  • Señal / Ranuras
    1. ???
    2. entity_Aconecta una update-hpranura a entity_Bla update-hpseñal de.

¿Hay algo mejor? ¿Tengo una comprensión correcta de cómo estos esquemas de comunicación se vincularían con el sistema de entidad de un motor de juego?

deft_code
fuente

Respuestas:

67

¡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.

Sorteo
fuente
Gracias por la respuesta. Grandes ideas La razón por la que he terminado de diseñar el mensaje que pasa es por Lua. Me gustaría poder crear nuevas armas sin un nuevo código C ++. Entonces sus pensamientos respondieron algunas de mis preguntas no formuladas.
deft_code
En cuanto a las corutinas, yo también creo mucho en las corutinas, pero nunca juego con ellas en C ++. Tenía una vaga esperanza de usar corutinas en el código lua para manejar llamadas de bloqueo (por ejemplo, esperar a la muerte). ¿Valió la pena el esfuerzo? Me temo que puedo estar cegado por mi intenso deseo de corutinas en c ++.
deft_code
Por último, ¿Cuál fue el juego de iPhone? ¿Puedo obtener más información sobre el sistema de entidades que utilizó?
deft_code
2
El sistema de entidades estaba principalmente en C ++. Entonces, por ejemplo, había una clase Imp que manejaba el comportamiento del Imp. Lua podría cambiar los parámetros de Imp en el desove o por mensaje. El objetivo con Lua era encajar en un calendario apretado, y la depuración del código de Lua lleva mucho tiempo. Usamos Lua para guiar los niveles (qué entidades van a dónde, eventos que suceden cuando presionas los disparadores). Entonces, en Lua, diríamos cosas como SpawnEnt ("Imp") donde Imp es una asociación de fábrica registrada manualmente. Siempre se generaría en un grupo global de entidades. Agradable y simple Usamos muchos smart_ptr y weak_ptr.
Sorteo
1
Entonces BananaRaffle: ¿Diría que este es un resumen preciso de su respuesta: "Las 3 soluciones que publicó tienen sus usos, al igual que otras. No busque la solución perfecta, solo use lo que necesita donde tenga sentido ".
Ipsquiggle
76
// in entity_a's code:
entity_b->takeDamage();

Preguntaste cómo lo hacen los juegos comerciales. ;)

tenpn
fuente
8
¿Un voto negativo? En serio, ¡así es como se hace normalmente! Los sistemas de entidades son excelentes, pero no ayudan a alcanzar los primeros hitos.
Tenpn
Hago juegos Flash profesionalmente, y así es como lo hago. Llama a enemigo.daño (10) y luego busca cualquier información que necesite de captadores públicos.
Iain
77
Así es como lo hacen los motores de juegos comerciales. El no está bromeando. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer, etc.) suele ser así.
AA Grapsas
3
¿Los juegos comerciales también escriben mal "daño"? :-P
Ricket
15
Sí, hacen daño de ortografía, entre otras cosas. :)
LearnCocos2D
17

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.

tenpn
fuente
Nunca antes había oído hablar del modelo de pizarra.
deft_code
También son buenos para reducir las dependencias, de la misma manera que lo hace una cola de eventos o un modelo de publicación / suscripción.
tenpn
2
Esta es también la "definición" canónica de cómo "debería funcionar" un sistema E / C / S "ideal". Los Componentes forman la pizarra; Los sistemas son el código que actúa sobre él. (Las entidades, por supuesto, son solo long long int
so
6

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:

HIT, source=entity_A target=entity_B power=5

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

DAMAGE, source=entity_A target=entity_B amount=7

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.

JustAMartin
fuente
Esta es una respuesta mucho mejor que la respuesta aceptada IMO. Desacoplado, mantenible y extensible (y tampoco un desastre de acoplamiento como la respuesta de broma entity_b->takeDamage();)
Danny Yaroslavski
4

¿Por qué no tener una cola de mensajes global, algo como:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

Con:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

Y al final del juego de bucle / manejo de eventos:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Creo que este es el patrón de comando. Y Execute()es un virtual puro en el Eventque los derivados definen y hacen cosas. Entonces aquí:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}
El pato comunista
fuente
3

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.

  • Cuando A hace daño a B en el cliente 1, simplemente ponga en cola el evento de daño.
  • Sincronice las colas de comandos a través de la red
  • Manejar los comandos en cola en ambos lados.
Andreas
fuente
2
Si te tomas en serio evitar las trampas, A no daña a B en el cliente. El cliente propietario de A envía un comando de "ataque B" al servidor, que hace exactamente lo que tenían que decir; el servidor luego sincroniza ese estado con todos los clientes relevantes.
@ Joe: Sí, si hay un servidor que es un punto válido a considerar, pero a veces está bien confiar en el cliente (por ejemplo, en una consola) para evitar una gran carga de servidores.
Andreas
2

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.

Simon
fuente
1

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.

James Bellinger
fuente
1

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:

  1. Procesamiento inmediato a través de un mensaje:

    • el jugador A.Update () ve que el jugador quiere golpear a B, el jugador B recibe un mensaje que notifica el daño.
    • player B.HandleMessage () actualiza los puntos de vida del jugador B (muere)
    • el jugador B.Update () ve que el jugador B está muerto ... no puede atacar al jugador A

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.

  1. Poniendo en cola el mensaje

    • El jugador A.Update () ve que el jugador quiere golpear a B, el jugador B recibe un mensaje que notifica el daño y lo almacena en una cola
    • El jugador A.Update () verifica su cola, está vacía
    • el jugador B.Update () primero verifica los movimientos para que el jugador B envíe un mensaje al jugador A con daño también
    • el jugador B.Update () también maneja mensajes en la cola, procesa el daño del jugador A
    • Nuevo ciclo (2): el jugador A quiere beber una poción de vida, por lo que se llama al jugador A.Update () y se procesa el movimiento
    • El jugador A.Update () verifica la cola de mensajes y procesa el daño del jugador B

De nuevo, esto es injusto ... ¡se supone que el jugador A toma los puntos de golpe en el mismo turno / ciclo / tic!


fuente
44
Realmente no estás respondiendo la pregunta, pero creo que tu respuesta sería una excelente pregunta en sí misma. ¿Por qué no seguir adelante y preguntar cómo resolver una priorización tan "injusta"?
bummzack
Dudo que a la mayoría de los juegos les importe esta injusticia porque se actualizan con tanta frecuencia que rara vez es un problema. Una solución simple es alternar entre iterar hacia adelante y hacia atrás a través de la lista de entidades al actualizar.
Kylotan
Utilizo 2 llamadas, así que llamo Update () a todas las entidades, luego, después del ciclo, itero nuevamente y llamo algo así 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.
Pablo Ariel
Creo que en los niveles de cuadro, la mayoría de las implementaciones de juegos son simplemente injustas. como dijo Kylotan.
v.oddou
Este problema es increíblemente fácil de resolver. Simplemente aplique el daño entre sí en los manejadores de mensajes o lo que sea. Definitivamente no deberías marcar al jugador como muerto dentro del controlador de mensajes. En "Update ()" simplemente haces "if (hp <= 0) die ();" (al principio de "Actualizar ()", por ejemplo). De esa forma, ambos pueden matarse entre sí al mismo tiempo. Además: a menudo no dañas al jugador directamente, sino a través de algún objeto intermedio como una bala.
Tara