¿Cómo puedo mantener la compatibilidad guardada del juego hacia atrás?

Tengo un juego de sim complejo al que quiero agregar la funcionalidad save juego. Lo actualizaré continuamente con nuevas funciones después del lanzamiento.

¿Cómo puedo asegurarme de que mis actualizaciones no rompan los juegos guardados existentes? ¿Qué tipo de architecture debería seguir para que esto sea posible?

Un enfoque fácil es mantener alnetworkingedor las funciones de carga antiguas. Solo necesita una única function de guardado que solo escriba la última versión. La function de carga detecta la function de carga versionada correcta para invocar (generalmente escribiendo un número de versión en algún lugar al comienzo de su formatting de file de 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) 

Puede hacer esto para todo el file, para secciones individuales del file, para objects / componentes de juegos individuales, etc. Exactamente qué split es la mejor va a depender de su juego y la cantidad de estado que está serializando.

Tenga en count que esto solo lo lleva tan lejos. En algún momento puede cambiar su 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 classs de personajes que el jugador puede elegir. Si eliminas una class de personaje, no hay mucho que puedas hacer con las salvaciones de personajes que tienen esa class. Tal vez podrías convertirlo a una class similar que aún 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.

Tenga en count que una vez que envía su juego está "hecho". Puede que publique contenido descargable u otras actualizaciones a lo largo del time, pero no serán cambios particularmente importantes en el juego. Tomemos 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 less el mismo juego que tenía cuando salió por primera vez.

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

Una forma simple de lograr una apariencia de control de versiones es darle sentido a los miembros de los objects que está serializando. Si su código tiene una comprensión de los diversos types de datos que se serializarán, puede get cierta solidez sin hacer demasiado trabajo.

Digamos que tenemos un object 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 ObjectType tiene miembros de datos llamados m_name , m_size y m_someStruct . Si puede recorrer o enumerar miembros de datos durante el time de ejecución (de alguna manera), al leer este file puede leer un nombre de miembro y asociarlo a un miembro real dentro de su instancia de object.

Durante esta fase de búsqueda, si no encuentra un miembro de datos coincidente, puede ignorar con security esta parte del file guardado. Por ejemplo, decir que la versión 1.0 de SomeStruct tenía un miembro de datos m_name . Luego parche y este miembro de datos fue eliminado por completo. Cuando cargue su file guardado, encontrará m_name y m_name un miembro correspondiente y no encontrará ninguna coincidencia. Su código simplemente puede pasar al siguiente miembro en el file sin bloquearse. Esto le permite eliminar miembros de datos sin preocuparse por romper files guardados antiguos.

De forma similar, si agrega un nuevo tipo de miembro de datos e intenta cargar desde un file guardado anterior, es posible que su código no pueda inicializar el nuevo miembro. Esto se puede utilizar con ventaja: los nuevos miembros de datos se pueden insert en los files de guardado durante el parcheado manualmente, quizás introduciendo valores pnetworkingeterminados (o por medios más inteligentes).

Este formatting también permite manipular o modificar fácilmente los files guardados a mano; el order en el que los miembros de los datos realmente no tienen mucho que ver con la validez de la rutina de serialization. Cada miembro se busca e inicializa de forma independiente. Esto podría ser una exquisitez que agrega un poco de solidez extra.

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

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

La mayoría de las veces, probablemente solo agregue / elimine campos y valores, mientras mantiene intacta la estructura general de sus files. En ese caso, puede simplemente escribir su código para ignorar los campos desconocidos, y usar valores pnetworkingeterminados razonables cuando un valor no puede ser entendido / analizado. Implementar esto es bastante sencillo, y lo hago mucho.

Sin embargo, a veces querrás cambiar la estructura del file. Diga de text-based a binary; o desde campos fijos a tamaño-valor. En tal caso, lo más probable es que desee congelar la fuente del viejo lector de files y crear uno nuevo para el nuevo tipo de file, como en la solución de Sean. Asegúrese de aislar todo el lector henetworkingado, o puede terminar modificando algo que lo afecta. Lo recomiendo solo para los principales cambios en la estructura de files.

Estos dos methods deberían funcionar para la mayoría de los casos, pero tenga en count 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 todo el nivel de lectura completa a transmisión (para la versión mobile del juego, que debería funcionar en dispositivos con ancho de banda y memory significativamente networkingucidos). Un cambio como este es mucho más profundo y probablemente requerirá cambios en muchas otras partes del juego, algunas de las cuales requieren cambios en la estructura del file.

En un nivel superior: si agrega nuevas funciones al juego, tenga una function "Adivinar nuevos valores" que puede tomar las funciones anteriores y adivinar cuáles serán los valores nuevos.

Un ejemplo podría aclarar esto. Supongamos que un juego modela las 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, por ser less realist, pero estoy divagando.) GuessNewValues ​​() para 1.1, dado un file de guardado 1.0, comenzaría con una vieja figura de nivel de desarrollo, y supongo, basado en eso, qué se habrían construido edificios en la ciudad, quizás mirando la cultura de la ciudad, su position geográfica, el enfoque de su desarrollo, ese tipo de cosas.

Espero que esto pueda ser comprensible en general, que si está agregando nuevas características a un juego, cargar un file de almacenamiento que aún no tiene esas características requiere hacer mejores conjeturas sobre cuáles serán los nuevos datos y combinarlos. con los datos que cargó

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

Sugiero ir con algo como XML (si guardas files son muy pequeños) de esa manera solo necesitas 1 function para manejar el marcado sin importar lo que pongas en él. El nodo raíz de ese documento podría declarar la versión que guardó el juego y le permitirá escribir código para actualizar el file 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 en un "formatting de versión actual" antes de cargar los datos, de modo que en lugar de tener muchas funciones versionadas, simplemente tenga un set de files xsl que elija de para hacer la conversión Sin embargo, esto puede consumir mucho time si no está familiarizado con xsl.

Si tus files guardados son masivos, xml podría ser un problema, por lo general, los files guardados funcionan muy bien donde solo puedes volcar los pares de valores key en el file como este …

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

Luego, cuando lee de este file, siempre escribe y lee una variable de la misma manera; si necesita una nueva variable, crea una nueva function para escribirla y leerla. podrías simplemente escribir una function para types de variables para que tengas un "lector de cadenas" y un "lector int", esto solo sería fial si cambiaste un tipo de variables entre versiones, pero nunca debes hacer eso porque la variable significa algo más en este punto, por lo que debe crear una nueva variable con un nombre diferente.

La otra forma, por supuesto, es utilizar un formatting de tipo de database o algo así como un file csv, pero depende de los datos que está guardando.

    Intereting Posts