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?
fuente
Respuestas:
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í.
fuente
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):
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
list
de 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 alist
cuando 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:
fuente