Hay muchos artículos sobre el desarrollo de juegos en Habré, pero entre ellos hay muy pocos artículos relacionados con temas "detrás de escena". Uno de estos temas es la organización de la entrega, de hecho, del juego a un gran número de usuarios durante mucho tiempo (uno, dos, tres). A pesar del hecho de que para algunos la tarea puede parecer trivial, decidí compartir mi experiencia de caminar el rake en este asunto para un proyecto específico. Alguien interesado, por favor.Una pequeña digresión sobre la divulgación de información. La mayoría de las empresas están muy celosas de que la "cocina interior" no sea accesible al público en general. Por qué, no sé, pero qué es eso. En este proyecto en particular, The Universim, tuve suerte y fui el CEO de Crytivo Inc. (anteriormente Crytivo Games Inc.) Alex Wallet resultó ser absolutamente cuerdo en este asunto, así que tengo la oportunidad de compartir experiencias con otros.Un poco sobre el parche en sí mismo
He estado involucrado en el desarrollo de juegos durante mucho tiempo. En algunos, como diseñador y programador de juegos, en otros, como una fusión de un administrador del sistema y un programador (no me gusta el término "devops", ya que no refleja con precisión la esencia de las tareas que realizo en tales proyectos).
A finales de 2013 (el horror de cómo pasa el tiempo), pensé en entregar nuevas versiones (compilaciones) a los usuarios. Por supuesto, en ese momento había muchas soluciones para tal tarea, pero ganó el deseo de hacer un producto y el deseo de "construir bicicletas". Además, quería aprender C # más profundamente, así que decidí hacer mi propio parcheador. Mirando hacia el futuro, diré que el proyecto fue un éxito, más de una docena de compañías lo usaron y lo usan en sus proyectos, algunos pidieron hacer una versión teniendo en cuenta exactamente sus deseos.
La solución clásica consiste en crear paquetes delta (o diffs) de una versión a otra. Sin embargo, este enfoque es inconveniente tanto para los jugadores de prueba como para los desarrolladores: en un caso, para obtener la última versión del juego, debes pasar por toda la cadena de actualizaciones. Es decir el jugador necesita reunir secuencialmente una cierta cantidad de datos que él (a) nunca usará, y el desarrollador debe almacenar en su servidor (o servidores) un montón de datos obsoletos que algunos de los jugadores podrían necesitar alguna vez.
En otro caso, debe descargar el parche para su versión a la última versión, pero el desarrollador debe mantener todo este zoológico de parches en casa. Algunas implementaciones de sistemas de parches requieren cierto software y cierta lógica en los servidores, lo que también crea un dolor de cabeza adicional para los desarrolladores. Además, a menudo los desarrolladores de juegos no quieren hacer nada que no esté directamente relacionado con el desarrollo del juego en sí. Diré aún más, la mayoría no son especialistas que pueden configurar servidores para la distribución de contenido, esto simplemente no es su área de actividad.
Con todo esto en mente, quería encontrar una solución que fuera lo más simple posible para los usuarios (que quieren jugar más rápido y no bailar con parches de diferentes versiones), así como para los desarrolladores que necesitan escribir un juego y no descubrir qué y por qué no actualizado por el siguiente usuario.
Sabiendo cómo funcionan algunos protocolos de sincronización de datos, cuando los datos se analizan en el cliente y solo se transmiten los cambios del servidor, decidí utilizar el mismo enfoque.
Además, en la práctica, de una versión a otra durante todo el período de desarrollo, muchos archivos del juego cambian ligeramente: la textura está ahí, el modelo en sí, algunos sonidos.
Como resultado, parecía lógico considerar cada archivo en el directorio del juego como un conjunto de bloques de datos. Cuando se lanza la próxima versión, se analiza la construcción del juego, se construye un mapa de bloques y los archivos del juego se comprimen bloque por bloque. El cliente analiza los bloques existentes y solo se descarga la diferencia.
Inicialmente, el parche fue planeado como un módulo en Unity3D, sin embargo, surgió un detalle desagradable que me hizo reconsiderar esto. El hecho es que Unity3D es una aplicación (motor) que es completamente independiente de su código. Y mientras el motor está funcionando, hay un montón de archivos abiertos, lo que crea problemas cuando desea actualizarlos.
En sistemas similares a Unix, sobrescribir un archivo abierto (a menos que esté específicamente bloqueado) no presenta ningún problema, pero en Windows, sin bailar con una pandereta, una "finta con orejas" no funciona. Es por eso que hice el parche como una aplicación separada que no carga nada más que las bibliotecas del sistema. De hecho, el parche en sí resultó ser una utilidad completamente independiente del motor Unity3D, que no impidió, sin embargo, agregarlo a la tienda Unity3D.
Algoritmo Patcher
Entonces, los desarrolladores lanzan nuevas versiones con cierta frecuencia. Los jugadores quieren obtener estas versiones. El objetivo del desarrollador es proporcionar este proceso con costos mínimos y un dolor de cabeza mínimo para los jugadores.
Del desarrollador
Al preparar un parche, el algoritmo para las acciones del parche se ve así:
○ Cree un árbol de archivos del juego con sus atributos y sumas de verificación SHA512
○ Para cada archivo:
► Divida el contenido en bloques.
► Guarde la suma de verificación SHA256.
► Comprima un bloque y agréguelo al mapa de bloques de archivos.
► Guarde la dirección del bloque en el índice.
○ Guarde el árbol de archivos con sus sumas de verificación.
○ Guarde el archivo de datos de la versión.
El desarrollador tiene que cargar los archivos recibidos en el servidor.
Lado del jugador
En el cliente, el parcheador hace lo siguiente:
○ Se copia en un archivo con un nombre diferente. Esto actualizará el archivo ejecutable del parche si es necesario. Luego, el control se transfiere a la copia y se completa el original.
○ Descargue el archivo de versión y compárelo con el archivo de versión local.
○ Si la comparación no reveló una diferencia, puedes jugar, tenemos la última versión. Si hay una diferencia, pase al siguiente elemento.
○ Descargue un árbol de archivos con sus sumas de verificación.
○ Para cada archivo en el árbol del servidor:
► Si hay un archivo, considera su suma de comprobación (SHA512). De lo contrario, lo considera, pero vacío (es decir, consiste en ceros sólidos) y también considera su suma de comprobación.
► Si la suma del archivo local no coincide con la suma de verificación del archivo de la última versión:
► Crea un mapa de bloques local y lo compara con el mapa de bloques del servidor.
► Para cada bloque local que difiere del remoto, descarga un bloque comprimido del servidor y lo sobrescribe localmente.
○ Si no hay errores, actualiza el archivo de versión.
Hice el tamaño del bloque de datos un múltiplo de 1024 bytes, después de un cierto número de pruebas, decidí que era más fácil operar con bloques de 64 KB. Aunque la universalidad en el código permanece:
#region DQPatcher class public class DQPatcher {
Si hace que los bloques sean pequeños, entonces el cliente requiere menos cambios cuando los cambios son pocos. Sin embargo, surge otro problema: el tamaño del archivo de índice aumenta inversamente con la disminución del tamaño del bloque, es decir Si operamos con bloques de 8 KB, entonces el archivo de índice será 8 veces más grande que con bloques de 64 KB.
Elegí SHA256 / 512 para archivos y bloques de las siguientes consideraciones: la velocidad en comparación con el (obsoleto) MD5 / SHA128 difiere ligeramente, y aún necesita leer bloques y archivos. Y la probabilidad de colisiones con SHA256 / 512 es significativamente menor que con MD5 / SHA128. Para ser completamente aburrido, es en este caso, pero es tan pequeño que esta probabilidad puede ser descuidada.
Además, el cliente tiene en cuenta los siguientes puntos:
► Los bloques de datos se pueden cambiar en diferentes versiones, es decir localmente, tenemos el bloque número 10, y en el servidor tenemos el bloque número 12, o viceversa. Esto se tiene en cuenta para no descargar datos adicionales.
► Los bloques se solicitan no uno a la vez, sino en grupos: el cliente intenta combinar los rangos de los bloques necesarios y los solicita al servidor utilizando el encabezado Range. Esto también minimiza la carga del servidor:
Por supuesto, resultó que el cliente puede ser interrumpido en cualquier momento y después del lanzamiento posterior, continuará de facto con su trabajo y no descargará todo desde cero.
Aquí puede ver un video que ilustra el trabajo del parcheador en el proyecto de ejemplo Angry Bots:
Sobre cómo se organizó el parche del universo del juego
En septiembre de 2015, Alex Koshelkov se puso en contacto conmigo y me ofreció unirse al proyecto; necesitaban una solución que proporcionara actualizaciones mensuales a 30 mil (con cola) de jugadores. El tamaño inicial del juego en el archivo es de 600 megabytes. Antes de contactarme, hubo intentos de hacer su propia versión usando Electron, pero todo se topó con el mismo problema de archivos abiertos (por cierto, la versión actual de Electron puede hacer esto) y algunos otros. Además, ninguno de los desarrolladores entendió cómo funcionaría esto: me proporcionaron varios diseños de bicicletas, la parte del servidor estaba ausente por completo, querían hacerlo después de que todas las otras tareas se hayan resuelto.
Además, era necesario resolver el problema de cómo evitar la fuga de las claves del jugador: el hecho es que las claves eran para la plataforma Steam, aunque el juego en Steam aún no estaba disponible públicamente. Distribuir el juego era estrictamente requerido por la clave, aunque existía la posibilidad de que los jugadores pudieran compartir la clave del juego con amigos, esto podría descuidarse, ya que si el juego aparecía en Steam, la clave solo podía activarse una vez.
En la versión normal del parche, el árbol de datos para el parche se ve así:
./
| - linux
El | | - 1.0.0
El | `- version.txt
| - macosx
El | | - 1.0.0
El | `- version.txt
`- ventanas
| - 1.0.0
`- version.txt
Necesitaba asegurarme de que solo aquellos con la clave correcta tuvieran acceso.
Se me ocurrió la siguiente solución: para cada clave obtenemos su hash (SHA1), luego la usamos como una ruta para acceder a los datos del parche en el servidor. En el servidor, transferimos los datos del parche a un nivel más alto que el docroot, y agregamos enlaces simbólicos al directorio con los datos del parche en el mismo docroot. Los enlaces simbólicos tienen los mismos nombres que los hashes clave, solo se dividen en varios niveles (para facilitar el funcionamiento del sistema de archivos), es decir. el hash 0f99e50314d63c30271 ... ... ade71963e7ff se representará como
./0f/99/e5/0314d63c30271.....ade71963e7ff -----> / full / path / to / patch-data /
Por lo tanto, no es necesario distribuir las claves a alguien que admitirá los servidores de actualización; es suficiente transferir sus hashes, que son absolutamente inútiles para los propios jugadores.
Para agregar nuevas claves (o eliminar las antiguas), simplemente agregue / elimine el enlace simbólico correspondiente.
Con esta implementación, la verificación de la clave en sí misma obviamente no se realiza en ninguna parte; recibir errores 404 en el cliente indica que la clave es incorrecta (o ha sido desactivada).
Cabe señalar que el acceso clave no es una protección DRM completa: estas son solo restricciones en la etapa de pruebas alfa y beta (cerradas). Y la búsqueda se corta fácilmente por medio del servidor web en sí (al menos en Nginx, que yo uso).
En el mes de lanzamiento, solo se entregaron 2.5 TB de tráfico solo el primer día, y en los días siguientes, se distribuye aproximadamente la misma cantidad en promedio por mes:

Por lo tanto, si planea distribuir una gran cantidad de contenido, es mejor calcular de antemano cuánto le costará. Según observaciones personales: el tráfico más barato de los hosteleros europeos, el más caro (diría "oro") de Amazon y Google.
En la práctica, los ahorros de tráfico por año en promedio en The Universim son enormes: compare los números anteriores. Por supuesto, si un usuario no tiene un juego en absoluto o si está muy desactualizado, no ocurrirá un milagro y tendrá que descargar una gran cantidad de datos del servidor; si es desde cero, entonces un poco más de lo que el juego toma en el archivo. Sin embargo, con actualizaciones mensuales, las cosas se ponen realmente bien. En menos de 6 meses, el espejo estadounidense dio un poco más de 10 TB de tráfico, sin el uso de un parche este valor habría crecido significativamente.
Así es como se ve el tráfico anual del proyecto:

Algunas palabras sobre el "rastrillo" más memorable que tuvimos que dar en el proceso de trabajar en un parche personalizado para el juego "The Universim":
● El mayor problema era esperarme de los antivirus. Bueno, no les gustan las aplicaciones que descargan algo de Internet allí, modifican archivos (incluidos los ejecutables) y luego también intentan ejecutar los archivos descargados. Algunos antivirus no solo bloquearon el acceso a los archivos locales, sino que también bloquearon las llamadas al servidor de actualización, ingresando directamente a los datos que el cliente descargó. La solución fue utilizar una firma digital válida para el parche, esto reduce drásticamente la paranoia de los antivirus, y el uso del protocolo HTTPS en lugar de HTTP, elimina rápidamente algunos de los errores asociados con la curiosidad de los antivirus.
● Actualización de progreso. Muchos usuarios (y clientes) desean ver el progreso de la actualización. Uno tiene que improvisar, ya que no siempre es posible mostrar de manera confiable el progreso sin tener que hacer un trabajo extra. Sí, y tampoco se puede mostrar el tiempo exacto del final del proceso de parche, ya que el parche en sí no tiene datos sobre qué archivos deben actualizarse por adelantado.
● Una gran cantidad de usuarios de EE. UU. Tienen velocidades de conexión a servidores de Europa no muy altas. La migración del servidor de actualización a los EE. UU. Resolvió este problema. Para los usuarios de otros continentes, dejamos el servidor en Alemania. Por cierto, el tráfico en los EE. UU. Es mucho más caro que el europeo, en algunos casos, varias docenas de veces.
● Apple no se siente muy cómodo con este método de instalación de aplicaciones. Política oficial: las aplicaciones deben instalarse solo desde su tienda. Pero el problema es que las aplicaciones en las etapas de prueba alfa y beta no están permitidas en la tienda. Y aún más, no hay nada de qué hablar sobre la venta de aplicaciones en bruto desde el acceso temprano. Por lo tanto, tienes que escribir instrucciones sobre cómo bailar en amapolas. La opción con AppAnnie (aún eran independientes) no se consideró debido al límite en el número de evaluadores.
● Las redes son bastante impredecibles. Para que la aplicación no se rindiera de inmediato, tuve que ingresar un contador de errores. 9 excepciones detectadas le permiten decirle firmemente al usuario que tiene problemas con la red.
● Los sistemas operativos de 32 bits tienen restricciones en el tamaño de los archivos que se muestran en la memoria (Archivos de mapa de memoria - MMF) para cada subproceso de ejecución y para el proceso en su conjunto. Las primeras versiones del parche utilizaban MMF para acelerar el trabajo, pero como los archivos de los recursos del juego pueden ser enormes, tuve que abandonar este enfoque y usar secuencias de archivos normales. Por cierto, no se observó una pérdida especial de rendimiento, probablemente debido a la lectura proactiva del sistema operativo.
● Debe estar preparado para que los usuarios se quejen. No importa cuán bueno sea su producto, siempre habrá quienes no estén satisfechos. Y cuantos más usuarios de su producto (en el caso de The Universim hay más de 50 mil en este momento), más cuantitativamente habrá quejas para usted. En términos porcentuales, este es un número muy pequeño, pero en términos cuantitativos ...
A pesar de que el proyecto en su conjunto fue un éxito, tiene algunos inconvenientes:
● Aunque inicialmente eliminé toda la lógica principal por separado, la parte de la GUI es diferente en la implementación para MAC y Windows. La versión de Linux no causó problemas: todos los problemas se debieron principalmente solo al usar una compilación monolítica que no requería el entorno de tiempo de ejecución mono (MRE). Pero dado que necesita tener una licencia adicional para distribuir dichos archivos ejecutables, se decidió abandonar las compilaciones monolíticas y simplemente requerir MRE. La versión de Linux difiere de la versión de Windows solo en la compatibilidad con los atributos de archivo específicos de los sistemas * nix. Para mi segundo proyecto, que será más que un simple parcheador, planeo usar un enfoque modular en forma de proceso de núcleo que se ejecute en segundo plano y permita administrar todo en la interfaz local. Y el control en sí puede llevarse a cabo desde una aplicación basada en Electron y similares (o simplemente desde un navegador). Con cualquier cosita. Antes de hablar sobre el tamaño de la distribución de tales aplicaciones, observe el tamaño de los juegos. Las versiones demo (!!!) de algunas ocupan 5 o más gigabytes en el archivo (!!!).
● Las estructuras que se usan ahora no ahorran espacio cuando se lanza el juego para 3 plataformas. De hecho, debes conservar 3 copias de datos casi idénticos, aunque comprimidos.
● La versión actual del parcheador no almacena en caché su trabajo, cada vez que se recalculan todas las sumas de verificación de todos los archivos. Sería posible reducir significativamente el tiempo si el parche almacenara en caché los resultados de aquellos archivos que ya están en el cliente. Pero hay un dilema: si el archivo está dañado (o falta), pero la entrada de caché para este archivo se guarda, entonces el parche se saltará, lo que causará problemas.
● La versión actual no puede funcionar simultáneamente con varios servidores (a menos que haga Round-robin usando DNS). Me gustaría cambiar a una tecnología "similar a torrent" para que pueda usar varios servidores al mismo tiempo. No se trata de utilizar clientes como fuente de datos, ya que esto plantea muchos problemas legales y es más fácil rechazarlo desde el principio.
● Si desea restringir el acceso a las actualizaciones, esta lógica deberá implementarse de forma independiente. En realidad, esto difícilmente se puede llamar un inconveniente, ya que todos pueden tener sus propios deseos con respecto a las restricciones. La restricción de teclas más simple, sin ninguna parte del servidor, se hace bastante simple, como mostré anteriormente.
● Se crea un parche para un solo proyecto a la vez. Si desea construir algo similar a Steam, entonces ya se requiere un sistema completo de entrega de contenido. Y este es un proyecto de un nivel completamente diferente.
Planeo poner el parche en el dominio público después de que se implemente la "segunda generación": un sistema de entrega de contenido del juego que incluirá no solo el parche evolucionado, sino también un módulo de telemetría (ya que los desarrolladores necesitan saber qué están haciendo los jugadores), Módulo de guardado en la nube y algunos otros módulos.
Si tiene un proyecto sin fines de lucro y necesita un parche, escríbame los detalles sobre su proyecto y le daré una copia gratis. No habrá enlaces aquí, ya que este no es el centro "I PR".
Estaré encantado de responder a sus preguntas.