¿Cómo puedo mantener la compatibilidad con versiones anteriores de juegos guardados?


8

Tengo un juego de simulación complejo al que quiero agregar la funcionalidad de guardar juego. Lo actualizaré con nuevas características continuamente después del lanzamiento.

¿Cómo puedo asegurarme de que mis actualizaciones no rompan los juegos guardados existentes? ¿Qué tipo de arquitectura debo seguir para hacer esto posible?


No conozco una arquitectura genérica para este objetivo, pero haría que el proceso de parcheo también actualice / convierta los juegos guardados para garantizar la compatibilidad con las nuevas funciones.
loodakrawa

Respuestas:


9

Un enfoque fácil es mantener las viejas funciones de carga. Solo necesita una única función de guardado que escriba solo la última versión. La función de carga detecta la función de carga versionada correcta para invocar (generalmente escribiendo un número de versión en algún lugar al comienzo de su formato de archivo guardado). Algo como:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

Puedes hacer esto para todo el archivo, para secciones individuales del archivo, para objetos / componentes individuales del juego, etc. Exactamente qué división es mejor dependerá de tu juego y la cantidad de estado que estés serializando.

Tenga en cuenta que esto solo lo lleva tan lejos. En algún momento, podrías cambiar tu juego lo suficiente como para que los datos guardados de versiones anteriores simplemente no tengan sentido. Por ejemplo, un juego de rol puede tener diferentes clases de personajes que el jugador puede elegir. Si elimina una clase de personaje, no hay mucho que pueda hacer con guardar los personajes que tienen esa clase. Tal vez podría convertirlo a una clase similar que todavía existe ... tal vez. Lo mismo ocurre si cambias otras partes del juego lo suficiente como para que no se parezca mucho a las versiones anteriores.

Ten en cuenta que una vez que envías tu juego está "hecho". Puede lanzar DLC u otras actualizaciones con el tiempo, pero no serán cambios particularmente importantes en el juego en sí. Tome la mayoría de los MMO, por ejemplo: WoW se ha mantenido durante muchos años con nuevas actualizaciones y cambios, pero todavía es más o menos el mismo juego que cuando salió por primera vez.

Para el desarrollo temprano, simplemente no me preocuparía. Los guardados son efímeros en las primeras pruebas. Sin embargo, es otra historia una vez que llegas a la versión beta pública.


1
Esta. Desafortunadamente, esto rara vez funciona tan bonito como se anuncia. Por lo general, esas funciones de carga dependen de funciones auxiliares ( ReadCharacterpueden llamar ReadStat, que pueden o no cambiar de una versión a la siguiente), por lo que necesitaría mantener versiones para cada una de ellas, lo que dificulta cada vez más el mantenimiento. Como siempre, no hay una bala de plata, y mantener viejas funciones de carga es un buen punto de partida.
Panda Pyjama

5

Una manera simple de lograr una apariencia de control de versiones es dar sentido a los miembros de los objetos que está serializando. Si su código comprende los diversos tipos de datos que se van a serializar, puede obtener cierta solidez sin hacer demasiado trabajo.

Digamos que tenemos un objeto serializado que se ve así:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

Debería ser fácil ver que el tipo ObjectTypetiene miembros de datos llamados m_name, m_sizey m_someStruct. Si puede recorrer o enumerar miembros de datos durante el tiempo de ejecución (de alguna manera), al leer este archivo, puede leer el nombre de un miembro y compararlo con un miembro real dentro de su instancia de objeto.

Durante esta fase de búsqueda, si no encuentra un miembro de datos coincidente, puede ignorar con seguridad esta parte del archivo guardado. Por ejemplo, la versión 1.0 de SomeStructtenía un m_namemiembro de datos. Luego parches y este miembro de datos se eliminó por completo. Al cargar su archivo guardado, encontrará m_nameun miembro coincidente y no encontrará ninguna coincidencia. Su código simplemente puede pasar al siguiente miembro del archivo sin fallar. Esto le permite eliminar miembros de datos sin preocuparse por romper viejos archivos guardados.

Del mismo modo, si agrega un nuevo tipo de miembro de datos e intenta cargar desde un archivo de guardado anterior, es posible que su código no inicialice al nuevo miembro. Esto se puede utilizar para una ventaja: los nuevos miembros de datos se pueden insertar en los archivos guardados durante la aplicación de parches manualmente, quizás introduciendo valores predeterminados (o por medios más inteligentes).

Este formato también permite manipular o modificar fácilmente los archivos guardados a mano; El orden en que los miembros de datos realmente no tiene mucho que ver con la validez de la rutina de serialización. Cada miembro se busca e inicializa de forma independiente. Esto podría ser un detalle que agrega un poco de robustez adicional.

Todo esto se puede lograr a través de alguna forma de introspección de tipo. Deberá poder consultar a un miembro de datos mediante la búsqueda de cadenas y poder saber cuál es el tipo de datos real del miembro de datos. Esto se puede lograr en C ++ utilizando una forma de introspección personalizada, y otros lenguajes pueden tener incorporadas instalaciones de introspección.


Esto será útil para hacer que los datos y las clases sean más robustos. (En .NET, la característica se llama "reflexión"). Me pregunto acerca de las colecciones ... mi IA es complicada y utiliza muchas colecciones temporales para procesar datos. ¿Debo tratar de evitar guardarlos ...? Quizás limite el ahorro a "puntos seguros" donde el procesamiento ha terminado.
Pan de centeno el

@aman Si guarda una colección, puede escribir los datos reales en estas colecciones como en mi ejemplo original, excepto en un "formato de matriz", como en muchos de ellos en una fila. Todavía puede aplicar la misma idea a cada elemento individual de una matriz, o cualquier otro contenedor. Simplemente tendrá que escribir algún "serializador de matriz" genérico, "serializador de lista", etc. Si desea un "serializador de contenedor" genérico, probablemente necesitará un resumen SerializingIteratorde algún tipo, y este iterador se implementaría para cada tipo de contenedor.
RandyGaul

1
Ah, sí, debe intentar evitar guardar colecciones complicadas con punteros tanto como sea posible. Muchas veces esto se puede evitar con mucho pensamiento y un diseño inteligente. La serialización es algo que puede volverse muy complicado, por lo que valdrá la pena intentar simplificarlo tanto como sea posible. @aman
RandyGaul

También está el problema de deserializar un objeto cuando la clase ha cambiado ... Creo que el deserializador .NET se bloqueará en muchos casos.
Pan de centeno el

2

Este es un problema que existe no solo en los juegos, sino también en cualquier aplicación de intercambio de archivos. Ciertamente, no hay soluciones perfectas, y tratar de hacer un formato de archivo que se mantenga con cualquier tipo de cambio probablemente sea imposible, por lo que probablemente sea una buena idea prepararse para el tipo de cambios que puede estar esperando.

La mayoría de las veces, probablemente solo agregará / eliminará campos y valores, mientras mantiene intacta la estructura general de sus archivos. En ese caso, puede simplemente escribir su código para ignorar los campos desconocidos y usar valores predeterminados razonables cuando un valor no se puede entender / analizar. Implementar esto es bastante sencillo, y lo hago mucho.

Sin embargo, a veces querrá cambiar la estructura del archivo. Decir de texto a binario; o de campos fijos a tamaño-valor. En tal caso, lo más probable es que desee congelar la fuente del antiguo lector de archivos y crear uno nuevo para el nuevo tipo de archivo, como en la solución de Sean. Asegúrese de aislar todo el lector heredado, o puede terminar modificando algo que lo afecte. Recomiendo esto solo para cambios importantes en la estructura de archivos.

Estos dos métodos deberían funcionar para la mayoría de los casos, pero tenga en cuenta que no son los únicos cambios posibles que puede encontrar. Tuve un caso en el que tuve que cambiar el código de carga de nivel completo de lectura completa a transmisión (para la versión móvil del juego, que debería funcionar en dispositivos con ancho de banda y memoria significativamente reducidos). Un cambio como este es mucho más profundo y probablemente requerirá cambios en muchas otras partes del juego, algunas de las cuales requirieron cambios en la estructura del archivo en sí.


0

En un nivel superior: si está agregando nuevas características al juego, tenga una función "Adivinar nuevos valores" que puede tomar las características antiguas y adivinar cuáles serán los valores nuevos.

Un ejemplo podría aclarar esto. Supongamos un juego que modela ciudades, y que la versión 1.0 rastrea el nivel general de desarrollo de las ciudades, mientras que la versión 1.1 agrega edificios específicos similares a Civilization. (Personalmente, prefiero rastrear el desarrollo general, ya que es menos realista; pero me estoy desviando). GuessNewValues ​​() para 1.1, dado un archivo de guardado 1.0, comenzaría con una vieja figura de nivel de desarrollo, y supongo, basado en eso, qué los edificios se habrían construido en la ciudad, quizás mirando la cultura de la ciudad, su posición geográfica, el foco de su desarrollo, ese tipo de cosas.

Espero que esto pueda ser comprensible en general: que si está agregando nuevas funciones a un juego, cargar un archivo guardado que aún no tiene esas funciones requiere hacer una mejor suposición de cuáles serán los nuevos datos y combinarlos con los datos que cargó.

Para el lado de bajo nivel de las cosas, respaldaría la respuesta de Sean Middleditch (que he votado): mantener la lógica de carga existente, posiblemente incluso manteniendo versiones antiguas de las clases relevantes, y llamar primero a eso, luego a convertidor.


0

Sugeriría ir con algo como XML (si guarda los archivos son muy pequeños) de esa manera solo necesita 1 función para manejar el marcado sin importar lo que ponga en él. El nodo raíz de ese documento podría declarar la versión que guardó el juego y permitirle escribir código para actualizar el archivo a la última versión si es necesario.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

Esto también significa que puede aplicar una transformación si desea convertir los datos a un "formato de versión actual" antes de cargar los datos, por lo que en lugar de tener muchas funciones versionadas, simplemente tendrá un conjunto de archivos xsl entre los que puede elegir para hacer la conversión Sin embargo, esto puede llevar mucho tiempo si no está familiarizado con xsl.

Si sus archivos guardados son masivos, xml podría ser un problema, por lo general, los archivos guardados funcionan realmente bien donde simplemente descarga pares de valores clave en el archivo de esta manera ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Luego, cuando lee de este archivo, siempre escribe y lee una variable de la misma manera, si necesita una nueva variable, crea una nueva función para escribirla y leerla. simplemente podría escribir una función para tipos de variables para tener un "lector de cadenas" y un "lector de int", esto solo sería fial si cambiara un tipo de variable entre versiones, pero nunca debería hacerlo porque la variable significa algo más en este punto, por lo que debe crear una nueva variable en su lugar con un nombre diferente.

La otra forma, por supuesto, es utilizar un formato de tipo de base de datos o algo así como un archivo csv, pero depende de los datos que esté guardando.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.