¿Cómo creo un archivo guardado para un juego de C ++?

33

Estoy codificando mi final para un curso de Programación de videojuegos, y quiero saber cómo crear un archivo guardado para mi juego, para que un usuario pueda jugar y luego volver más tarde. Alguna idea de cómo se hace esto, todo lo que he hecho antes ha sido programas de ejecución única.

Tucker Morgan
fuente
2
También puede usar SQLite
Nick Shvelidze
1
@Shvelo Aunque podrías hacer eso, parece que agregaría mucha complejidad que no es necesariamente necesaria.
Nate

Respuestas:

38

Necesita usar la serialización para guardar sus variables en la memoria de su disco duro. Hay muchos tipos de serialización, en .NET XML es un formato común, aunque hay serializadores binarios y JSON disponibles. No soy un gran programador de C ++, pero una búsqueda rápida arrojó un ejemplo de serialización en C ++:

Hay bibliotecas que ofrecen funcionalidades de serialización. Algunos se mencionan en otras respuestas.

Las variables que le interesarán probablemente estarán relacionadas con el estado del juego. Por ejemplo, probablemente querrá saber este tipo de información

  1. El jugador estaba jugando nivel 3
  2. El jugador estaba en las coordenadas mundiales X, Y
  3. El jugador tiene tres artículos en su mochila.
    1. Arma
    2. Armadura
    3. Comida

Realmente no le importará qué texturas se están utilizando (a menos que su jugador pueda cambiar su apariencia, ese es un caso especial), porque generalmente son las mismas. Debes concentrarte en guardar datos importantes del estado del juego.

Cuando comienzas tu juego, comienzas normalmente para un "nuevo" juego (esto carga tus texturas, modelos, etc.) pero en el momento apropiado cargas los valores de tu archivo guardado de nuevo en el objeto de estado del juego reemplazando el nuevo "predeterminado" estado del juego Luego permites que el jugador continúe jugando.

Lo he simplificado enormemente aquí, pero debes tener una idea general. Si tiene una pregunta más específica, haga una nueva pregunta aquí y podemos intentar ayudarlo.

Nate
fuente
Entiendo lo que necesito guardar, pero me gustaría saber cuál es la forma exacta, ¿lo guarda en un archivo .txt en el proyecto, esas variables modificadas o de alguna otra manera
Tucker Morgan
Sí, si tu juego es simple, un archivo de texto puede ser suficiente; debes tener en cuenta que cualquiera puede editar un archivo de texto y así crear sus propios juegos guardados ...
Nate
Los archivos de texto guardados no son solo para juegos simples. Paradox usó un formato de texto estructurado para guardar archivos para todos los juegos que crearon usando el mismo motor que el motor insignia de Europa Universalis. Especialmente al final del juego, estos archivos podrían ser enormes.
Dan Neely
1
@DanNeely Un punto justo, no hay razón para que no pueda usar un formato de texto para almacenar muchos datos complicados, pero en general, cuando sus datos son tan complicados, los beneficios de otro formato (binario, xml, etc.) se vuelven más pronunciados.
Nate
1
@NateBross De acuerdo. Los juegos de Paradox eran muy amigables con los mods y usaban un formato similar (¿idéntico?) Para los datos del escenario. Almacenar la mayoría de sus datos como texto significaba que no necesitaban invertir en herramientas de edición de escenarios para uso público.
Dan Neely
19

Por lo general, esto es específico de tu juego. Estoy seguro de que hasta ahora has aprendido a escribir y leer archivos en tus clases. La idea básica es:

  1. Al salir del juego, escriba los valores que desea guardar en un archivo.
  2. Al cargar el juego, verifica si existe un archivo guardado; si es así, carga los valores de lectura en tu programa. Si el archivo no existe, continúe como lo hace ahora y establezca los valores en sus valores iniciales / predeterminados.

Lo que escribes depende de ti, depende de tu juego. Una forma de escribir es escribir las variables que desea en un orden específico como una secuencia de bytes. Luego, al cargar, léalos en su programa en el mismo orden.

Por ejemplo (en pseudocódigo rápido):

SaveGame(FileInput file) {
    file.writeInt(playerLevel);
    file.writeInt(playerHealth);
    file.writeInt(gameProgress);
}

LoadGame(FileInput file) {
    if(file.exists()) {
        playerLevel= file.readInt();
        playerHealth = file.readInt();
        gameProgress = file.readInt();
    } else {
        playerLevel = 1;
        playerHealth = 100;
        gameProgress = 0;
    }
}
MichaelHouse
fuente
1
Este método es bueno y pequeño, aunque recomendaría poner algunas etiquetas simples para fragmentos de datos. De esta manera, si más adelante necesita cambiar algo que normalmente está en el medio del archivo, puede hacerlo y la única "conversión de lo antiguo" que tiene que hacer es dentro de ese bloque. No es tan importante para una tarea única, pero si continúa trabajando después de que las personas comienzan a guardar los archivos, es una pesadilla usar solo bytes directos con la posición como único identificador.
Lunin
1
Sí, esto no genera archivos guardados a prueba de futuro. Tampoco funciona para datos que tienen tamaños de bytes variables como cadenas. Este último es fácil de solucionar escribiendo primero el tamaño de los datos que se van a escribir y luego usándolo cuando se carga para leer la cantidad correcta de bytes.
MichaelHouse
6

Probablemente hay una gran cantidad de formas de hacer esto, pero la más simple que siempre encontré y he usado tanto personal como profesionalmente es hacer una estructura que contenga todos los valores que quiero guardar.

struct SaveGameData
{
    int              characterLevel; // Any straight up values from the player
    int              inventoryCount; // Number of items the player has on them or stored or what not
    int[STAT_COUNT]  statistics;     // This is usually a constant size (I am tracking X number of stats)
    // etc
}

struct Item
{
    int itemTypeId;
    int Durability; // also used as a 'uses' count for potions and the like
    int strength;   // damage of a weapon, protection of armor, effectiveness of a potion
    // etc
}

Luego simplemente escribo / descargo los datos hacia y desde un archivo usando los valores básicos de File IO. InventoryCount es el número de estructuras de elementos que se guardan después de la estructura principal SaveGameData en el archivo, así que sé cuántas de ellas leer después de recuperar esos datos. La clave aquí es que cuando quiero guardar algo nuevo, a menos que sea una lista de elementos o similares, todo lo que tengo que hacer es agregar un valor a la estructura en algún lugar. Si se trata de una lista de elementos, entonces tendré que agregar un pase de lectura como ya he implicado para los objetos Elemento, un contador en el encabezado principal y luego las entradas.

Esto tiene el inconveniente de hacer que las diferentes versiones de un archivo guardado sean incompatibles entre sí sin un manejo especial (incluso si solo se trata de valores predeterminados para cada entrada en la estructura principal). Pero en general, esto hace que el sistema sea fácil de extender simplemente agregando un nuevo valor de datos y poniéndole un valor cuando sea necesario.

Una vez más, hay varias maneras de hacer esto y esto podría conducir más a C que a C ++, ¡pero ha hecho el trabajo!

James
fuente
1
También vale la pena señalar que esto no es independiente de la plataforma, no funcionará para cadenas de C ++, o para objetos a los que se hace referencia a través de referencias o punteros, o cualquier objeto que contenga cualquiera de los anteriores.
Kylotan
¿Por qué esta plataforma no es independiente? Funcionó bien en la PC, los sistemas PS * y 360 .. fwrite (pToDataBuffer, sizeof (datatype), countOfElements, pToFile); funciona para todos esos objetos, suponiendo que pueda obtener un puntero a sus datos, y el tamaño del objeto y luego el número de ellos que desea escribir ... y leer coincide con eso ...
James
Que es independiente de la plataforma, simplemente no hay garantía de que los archivos guardados en una sola plataforma se pueden cargar en otro. Lo cual es bastante irrelevante para, por ejemplo, guardar los datos del juego. El material de puntero a datos y tamaño de memoria obviamente puede ser un poco incómodo, pero funciona.
Leftaroundabout
3
En realidad, no hay garantía de que siga funcionando para usted para siempre: ¿qué sucede si lanza una nueva versión que está construida con un nuevo compilador o incluso nuevas opciones de compilación que cambian el relleno de estructura? Recomiendo encarecidamente el uso de raw-struct fwrite () solo por esta razón (estoy hablando de la experiencia en este caso, por cierto).
esponjoso
1
No se trata de '32 bits de datos '. El póster original simplemente pregunta "cómo guardo mis variables". Si escribe la variable directamente, pierde información en las plataformas. Si tiene que preprocesar antes de la escritura, entonces ha omitido la parte más importante de la respuesta, es decir. cómo procesar los datos para que se guarden correctamente y solo incluyan el bit trivial, es decir. llamando a fwrite para poner algo en un disco.
Kylotan
3

Primero debe decidir qué datos deben guardarse. Por ejemplo, esta podría ser la ubicación del personaje, su puntaje y la cantidad de monedas. Por supuesto, su juego probablemente será mucho más complejo, por lo que deberá guardar datos adicionales, como el número de nivel y la lista de enemigos.

A continuación, escriba el código para guardar esto en un archivo (use ofstream). Un formato relativamente simple que puede usar es el siguiente:

x y score coins

Y entonces el archivo se vería así:

14 96 4200 100

Lo que significaría que estaba en la posición (14, 96) con una puntuación de 4200 y 100 monedas.

También necesita escribir código para cargar este archivo (use ifstream).


Se puede salvar a los enemigos incluyendo su posición en el archivo. Podemos usar este formato:

number_of_enemies x1 y1 x2 y2 ...

Primero number_of_enemiesse lee y luego cada posición se lee con un bucle simple.

Pubby
fuente
1

Una adición / sugerencia sería agregar un nivel de encriptación a su serialización para que los usuarios no puedan editar sus valores de texto a "9999999999999999999". Una buena razón para hacerlo sería evitar desbordamientos de enteros (por ejemplo).

Styler
fuente
0

Para completar, quiero mencionar una biblioteca de serialización de c ++, que personalmente uso y que aún no mencioné: cereal .
Es fácil de usar y tiene una sintaxis agradable y limpia para la serialización. También ofrece múltiples tipos de formatos en los que puede guardar (XML, Json, Binario (incluida una versión portátil que respeta la resistencia)). Admite herencia y es solo de encabezado,

LukeG
fuente
0

Su juego comprometerá las estructuras de datos (¿con suerte?) Que necesita transformar en bytes (serializar) para que pueda almacenarlos. En el futuro, puede volver a cargar esos bytes y transformarlos nuevamente en su estructura original (deserialización). En C ++ no es tan complicado ya que la reflexión es muy limitada. Pero aún así, algunas bibliotecas pueden ayudarlo aquí. Escribí un artículo al respecto: https://rubentorresbonet.wordpress.com/2014/08/25/an-overview-of-data-serialization-techniques-in-c/ Básicamente, sugeriría que eche un vistazo a biblioteca de cereales si puede apuntar a compiladores de C ++ 11. No es necesario crear archivos intermedios como con protobuf, por lo que ahorrará algo de tiempo allí si desea resultados rápidos. También puede elegir entre binario y JSON, por lo que puede ayudar bastante a depurar aquí.

Además de eso, si la seguridad es una preocupación, es posible que desee cifrar / descifrar los datos que está almacenando, especialmente si está utilizando formatos legibles por humanos como JSON. Algoritmos como AES son útiles aquí.

Rubén Torres Bonet
fuente
-5

Debe usar fstreampara archivos de entrada / salida. La sintaxis es simple EX:

#include <fstream>
// ...
std::ofstream flux ; // to open a file in ouput mode
flux.open("myfile.whatever") ; 

O

#include <fstream>
// ...
std::ifstream flux ; // open a file in input mode
flux.open("myfile.whatever") ;

Otras acciones son posibles en su archivo: append , binary , trunc , etc. Debería usar la misma sintaxis que la anterior, en su lugar, ponemos std :: ios: :( flags), por ejemplo:

  • ios::out para operación de salida
  • ios::in para operación de entrada
  • ios::binary para operación binaria (byte sin formato) IO, en lugar de basada en caracteres
  • ios::app para comenzar a escribir al final del archivo
  • ios::trunc porque si el archivo ya existe, reemplace, elimine el contenido anterior y reemplácelo por uno nuevo
  • ios::ate - posicione el puntero de archivo "al final" para entrada / salida

Ex:

#include <fstream>
// ...
std::ifstream flux ;
flux.open("myfile.whatever" , ios::binary) ;

Aquí hay un ejemplo más completo pero simple.

#include <iostream>
#include <fstream>

using namespace std ;

int input ;
int New_Apple ;
int Apple_Instock ;
int Eat_Apple ;
int Apple ;

int  main()
{
  bool shouldQuit = false;
  New_Apple = 0 ;
  Apple_Instock = 0 ;
  Eat_Apple = 0 ;

  while( !shouldQuit )
  {
    cout << "------------------------------------- /n";
    cout << "1) add some apple " << endl ;
    cout << "2) check apple in stock " << endl ;
    cout << "3) eat some apple " << endl ;
    cout << "4) quit " << endl ;
    cout << "------------------------------------- /n";
    cin >> input ;

    switch (input)
    {
      case 1 :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << " how much apple do you want to add /n";
        cout << "------------------------------------ /n";      
        cin >> New_Apple ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ; 

        Apple = New_Apple + Apple_Instock ;

        ofstream apple_adder ;
        apple_adder.open("apple.apl") ;
        apple_adder << Apple ;
        apple_adder.close() ;

        cout << "------------------------------------ /n";
        cout << New_Apple << " Apple has been added ! /n";
        cout << "------------------------------------ /n";
        break;
      }

      case 2 :  
      {
        system("cls") ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ;

        cout << "------------------------------------ /n";
        cout << " there is " << Apple_Instock ;
        cout << "apple in stock /n" ;
        cout << "------------------------------------ /n";
        break;
      }

      case 3 :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << "How many apple do you want to eat /n" ;
        cout << "------------------------------------ /n";
        cin >> Eat_Apple ;

        ifstream apple_checker ;
        apple_checker.open("apple.apl") ;
        apple_checker >> Apple_Instock ;
        apple_checker.close() ;

        Apple = Apple_Instock - Eat_Apple ; 

        ofstream apple_eater ;
        apple_eater.open("apple.apl") ;
        apple_eater << Apple ;
        apple_eater.close() ;

        cout << "----------------------------------- /n";
        cout << Eat_Apple ;
        cout << " Apple has been eated! /n";
        cout << "----------------------------------- /n";
        cout << Apple << " Apple left in stock /n";
        cout << "----------------------------------- /n";
        break;
      }

      case 4 :
      {
        shouldQuit = true;
        break;
      }

      default :
      {
        system("cls") ;

        cout << "------------------------------------ /n";
        cout << " invalide choice ! /n";
        cout << "------------------------------------ /n"; 
        break;
      }
    }
  }
  return 0;
}
Francisco Forcier
fuente
44
-1 Esta es una muy mala respuesta. Debe formatear y mostrar correctamente el código y explicar lo que está haciendo, nadie quiere descifrar un fragmento de código.
Vaillancourt
Gracias Katu por el comentario que tienes razón, debería explicar mi código más bien, ¿puedes decirme cómo formateo mi fuente desde el sitio web porque soy nuevo en este tipo de cosas?
Francisco Forcier
¿Desde este sitio o para este sitio? Para obtener ayuda para formatear las publicaciones, puede visitar la página de ayuda de formateo . También hay un signo de exclamación junto al encabezado del área de texto que usa para publicar para ayudarlo.
Vaillancourt
Intenta documentar lo que se pide; No necesitas comentar todo. Y al no explicar lo que estaba haciendo, en mi comentario, quise decir que generalmente presenta la estrategia que sugiere con al menos un párrafo corto. (por ejemplo, "Una de las técnicas es utilizar un formato de archivo binario con un operador de flujo. Debe tener cuidado de leer y escribir en el mismo orden, bla bla lba").
Vaillancourt
2
Y al usar gotos, te lincharán en el lugar público. No uses gotos.
Vaillancourt