¿Cómo se implementaría un sistema de instantáneas de estado de juego para juegos en red en tiempo real?

12

Quiero crear un juego multijugador en tiempo real simple de cliente-servidor como proyecto para mi clase de redes.

He leído mucho sobre modelos de red multijugador en tiempo real y entiendo las relaciones entre el cliente y el servidor y las técnicas de compensación de retraso.

Lo que quiero hacer es algo similar al modelo de red Quake 3: básicamente, el servidor almacena una instantánea de todo el estado del juego; Al recibir la entrada de los clientes, el servidor crea una nueva instantánea que refleja los cambios. Luego, calcula las diferencias entre la nueva instantánea y la última y las envía a los clientes, para que puedan estar sincronizadas.

Este enfoque me parece realmente sólido: si el cliente y el servidor tienen una conexión estable, solo se enviará la cantidad mínima necesaria de datos para mantenerlos sincronizados. Si el cliente no se sincroniza, también se puede solicitar una instantánea completa.

Sin embargo, no puedo encontrar una buena manera de implementar el sistema de instantáneas. Me resulta muy difícil alejarme de la arquitectura de programación para un solo jugador y pensar en cómo podría almacenar el estado del juego de tal manera que:

  • Todos los datos están separados de la lógica.
  • Se pueden calcular las diferencias entre la instantánea de los estados del juego
  • Las entidades del juego aún pueden manipularse fácilmente a través del código

¿Cómo se implementa una clase de instantánea ? ¿Cómo se almacenan las entidades y sus datos? ¿Cada entidad cliente tiene una ID que coincida con una ID en el servidor?

¿Cómo se calculan las diferencias de instantánea?

En general: ¿cómo se implementaría un sistema de instantáneas del estado del juego?

Vittorio Romeo
fuente
44
+1. Esto es un poco demasiado amplio para una sola pregunta, pero en mi opinión es un tema interesante que puede tratarse más o menos en una respuesta.
Kromster
¿Por qué no solo almacena 1 Instantánea (el mundo real), guarda todos los cambios entrantes en este estado mundial normal Y almacena el cambio en una lista o algo así? Luego, cuando sea el momento de enviar los cambios a todos los clientes, simplemente envíe el contenido de la lista a todos ellos y borre la lista, comience desde cero (cambios). Tal vez esto no sea tan bueno como para almacenar 2 instantáneas, pero con este enfoque no necesita preocuparse por los algoritmos sobre cómo difuminar rápidamente 2 instantáneas.
tkausl
¿Ha leído esto: fabiensanglard.net/quake3/network.php ? La revisión del modelo de red del terremoto 3 incluye una discusión sobre la implementación.
Steven
¿Qué tipo de juego estás intentando construir? La configuración de la red depende en gran medida del tipo de juego que estés haciendo. Un RTS no se comporta como un FPS en términos de redes.
AturSams

Respuestas:

3

Puede calcular el delta de instantáneas (cambios en su estado sincronizado anterior) manteniendo dos instancias de instantáneas: la actual y la última sincronizada.

Cuando llega la entrada del cliente, modifica la instantánea actual. Luego, cuando llegue el momento de enviar delta a los clientes, calcule la última instantánea sincronizada con un campo por campo actual (recursivamente) y calcule y serialice el delta. Para la serialización, puede asignar una identificación única a cada campo en el alcance de su clase (en oposición al alcance del estado global). El cliente y el servidor deben compartir la misma estructura de datos para el estado global para que el cliente comprenda a qué se aplica un ID en particular.

Luego, cuando se calcula delta, clona el estado actual y lo convierte en el último sincronizado, por lo que ahora tiene idéntico estado actual y último sincronizado pero diferentes instancias para que pueda modificar el estado actual y no afectar al otro.

Este enfoque puede ser más fácil de implementar, especialmente con la ayuda de la reflexión (si tiene ese lujo), pero puede ser lento, incluso si optimiza mucho la parte de reflexión (al construir su esquema de datos para almacenar en caché la mayoría de las llamadas de reflexión). Principalmente porque necesita comparar dos copias de estado potencialmente grande. Por supuesto, depende de cómo implemente la comparación y su idioma. Puede ser rápido en C ++ con un comparador codificado pero no tan flexible: cualquier cambio en su estructura de estado global requiere la modificación de este comparador, y estos cambios son muy frecuentes en las etapas iniciales del proyecto.

Otro enfoque es usar banderas sucias. Cada vez que llega la entrada del cliente, la aplica a su única copia del estado global y marca los campos correspondientes como sucios. Luego, cuando llega el momento de sincronizar clientes, serializa campos sucios (recursivamente) utilizando los mismos ID únicos. El inconveniente (menor) es que a veces envía más datos de los estrictamente necesarios: por ejemplo int field1, inicialmente fue 0, luego se le asignó 1 (y se marcó como sucio) y luego se le asignó 0 nuevamente (pero permanece sucio). La ventaja es que al tener una estructura de datos jerárquica enorme no es necesario analizarla por completo para calcular delta, solo rutas sucias.

En general, esta tarea puede ser bastante complicada, depende de cuán flexible debe ser la solución final. Por ejemplo, Unity3D 5 (próximo) usará atributos para especificar datos que deberían sincronizarse automáticamente con los clientes (enfoque muy flexible, no necesita hacer nada excepto agregar un atributo a su campo (s)) y luego generar código como Paso posterior a la construcción. Más detalles aquí.

Andriy Tylychko
fuente
2

Primero debe saber cómo representar sus datos relevantes de una manera que cumpla con el protocolo. Esto depende de los datos relevantes para el juego. Usaré un juego RTS como ejemplo.

Para fines de redes, se enumeran todas las entidades en el juego (por ejemplo, camionetas, unidades, edificios, recursos naturales, elementos destructibles).

Los jugadores deben tener los datos relevantes para ellos (por ejemplo, todas las unidades visibles):

  • ¿Están vivos o muertos?
  • ¿De qué tipo son?
  • ¿Cuánta salud les queda?
  • Posición actual, rotación, velocidad (velocidad + dirección), trayectoria en el futuro cercano ...
  • Actividad: atacar, caminar, construir, arreglar, sanar, etc.
  • efectos de estado de mejora / desventaja
  • y posiblemente otras estadísticas como maná, escudos y qué no?

Al principio, el jugador debe recibir el estado completo antes de poder ingresar al juego (o alternativamente toda la información relevante para ese jugador).

Cada unidad tiene una identificación entera. Los atributos se enumeran y, por lo tanto, también tienen identificadores integrales. Las ID de las unidades no tienen que tener 32 bits de longitud (podría ser si no somos frugales). Bien podría ser de 20 bits (dejando 10 bits para los atributos). La identificación de las unidades tiene que ser única, bien podría ser asignada por un contador cuando la unidad se instancia y / o se agrega al mundo del juego (los edificios y recursos se consideran una unidad inmóvil y los recursos se pueden asignar una identificación cuando el mapa está cargado).

El servidor almacena el estado global actual. El estado actualizado más reciente de cada jugador está representado por un puntero a uno listde los cambios recientes (todos los cambios después de que el puntero aún no se haya enviado a ese jugador). Los cambios se agregan a listcuando ocurren. Una vez que el servidor termina de enviar la última actualización, puede comenzar a iterar sobre la lista: el servidor mueve el puntero del jugador a lo largo de la lista hasta su cola, recolectando todos los cambios en el camino y colocándolos en un búfer que se enviará a el jugador (es decir, el formato del protocolo puede ser algo como esto: unit_id; attr_id; new_value) Las nuevas unidades también se consideran cambios y se envían con todos sus valores de atributo a los jugadores receptores.

Si no está utilizando un idioma con un recolector de basura, deberá configurar un puntero perezoso que se retrasará y luego alcanzará el puntero de jugador más desactualizado de la lista, liberando objetos en el camino. Puede recordar qué jugador está más desactualizado dentro de un montón prioritario o simplemente iterar y liberar hasta que el puntero perezoso sea igual (es decir, apunta al mismo elemento que uno de los punteros de los jugadores).

Algunas preguntas que no planteó y creo que son interesantes son:

  1. ¿Deben los clientes recibir una instantánea con todos los datos en primer lugar? ¿Qué pasa con los artículos fuera de su línea de visión? ¿Qué pasa con la niebla de guerra en los juegos de estrategia en tiempo real? Si envía todos los datos, el cliente podría ser pirateado para mostrar datos que no deberían estar disponibles para el jugador (dependiendo de otras medidas de seguridad que tome). Si solo envía datos relevantes, el problema se resuelve.
  2. ¿Cuándo es vital enviar cambios en lugar de enviar toda la información? Teniendo en cuenta el ancho de banda disponible en las máquinas modernas, ¿ganamos algo enviando un "delta" en lugar de enviar toda la información? De ser así, ¿cuándo?
AturSams
fuente