Stratos: Punto de Encuentro de Desarrolladores

¡Bienvenido a Stratos!

Acceder

Foros





Serializando El Estado De Una Partida

Iniciado por CoLSoN2, 29 de Agosto de 2005, 04:16:22 PM

« anterior - próximo »

CoLSoN2

 Quiero incorporar un sistema para salvar/cargar partidas en cualquier momento serializando todos los objetos que haya en ese momento activos, pero tengo algunas dudas sobre cómo diseñarlo porque la verdad es que nunca había hecho nada parecido.

- Supongo que es mejor que todas las clases que me interese serializar implementen una interfaz común, pero no estoy muy seguro de cómo hacerlo, porque realmente no se como se suele cargar un objeto que ha sido serializado y guardado en disco.

Es algo así?

FileStream file("foo.save");

int classId = 0; // id no valido
while (classId = file.ReadInt())
{
   switch (classId)
   {
       case MyObject::ID:
           PutSomewhere(MyObject(file));
           break;
   }
}

o quizá al crear el objeto se podría hacer uso de los operadores >> y << para (de)serializar, ya que normalmente se utilizan para leer y escribir datos de streams.

MyObject obj;
file >> obj;
PutSomewhere(obj);


Es así como suele hacerse? Lo que más me interesa es saber si lo del switch para
determinar la clase que se va a cargar es algo normal, o se utiliza otra técnica
más elegante y fácil de extender. Vamos, si hay alguna solución que me evite
tener que ir añadiendo nuevas clases al switch cuando las cree.
Manuel F. Lara
Descargar juegos indie  - blog sobre juegos indie y casual
El Desarrollo Personal.com  - blog sobre productividad, motivación y espíritu emprendedor

ethernet

 Te recomiendo que eches un vistazo al código del unreal (la parte que dejan). Lo que viene a hacer es usar un Object base para todos los objetos en el que tiene sobrecargado el operator<<, el cual acepta un FArchive, que viene a ser un interfaz para la serialización. A partir de FArchive implementa, por ejemplo, los ficheros:

class FileReader: public FArchive
{
....
};
para despues;

FileReader r;
MyClass a;

r << a;

o para escribit:

FileWriter w;
MyClass a;
w << a;

Lo bueno que tiene es que no necesitas, como es lógico en un serializer, tener códigos diferentes para la carga y descarga de objetos.

Lo que es más complejo en el código es la forma de asignar ID a cada clase y sobretodo el proceso de des-serializar. Usa un sistema similar al de MFC, con un montón de macros. Muy recomendado echarle un vistazo.

Otra forma radicalmente diferente de serializer:

http://www.xyzw.de/c140.html



AK47

 Saludos
Puedes mirar  esto. El articulo va de como serializar las clases de forma mas o menos automatica para enviarlos por red, aunque sirve perfectamente tambien para serializarlos ;) Yo lo implemente para una cosa del trabajo y, despues de montarte la infraestructura y un par de macros para facilitarte las cosas, esta muy bien :D

Ta pronto

CoLSoN2

 AK47 eso está genial, seguramente sea lo que acabe usando. Gracias!
Manuel F. Lara
Descargar juegos indie  - blog sobre juegos indie y casual
El Desarrollo Personal.com  - blog sobre productividad, motivación y espíritu emprendedor

tiutiu

 Puedes usar mixins para implementar comportamientos en tus objetos. Asi defines una interfaz para serializacion:


class Serializable
{
public:
 virtual bool Load ( istream inStream ) = 0;
 virtual bool Save ( ostream outStream ) = 0;
}

class MyObject : public Serializable
{
 int m_iFoo;
 string m_strName;

public:
 virtual bool Load ( istream inStream )
 {
   istream >> m_iFoo;
   istream >> m_strName;
 }

 virtual bool Save ( ostream outStream )
 {
   ostream << m_iFoo;
   ostream << m_strName;
 }

}


Creo que es una bonita forma de implementarlo. Puedes hacer lo mismo con otras interfaces como p.e. reference counting, renderable y demas.
b>:: Pandora's Box project ::
Notas e ideas sobre desarrollo de engines para juegos

CoLSoN2

 Ehm.. ya, pero eso es sólo parte del problema, la parte fácil vamos XD Luego tienes el hecho de tener que ir manteniendo el switch que he mencionado antes, o algo similar, mientras que con el sistema de Pluggable Factories que aparece en el artículo de AK47 (de hecho en el que se basa éste) tienes todo solucionado elegantemente.

Seguramente tenga algún que otro bug porque todavía no lo he probado, lo acabo de hacer, pero el código que me he hecho basado en ese sistema es este:

Serialize.h:

#ifndef __SERIALIZE_H__
#define __SERIALIZE_H__

//////////////////////////////////////////////////////////////////////////
//       Serialize.h
//
// Cointains helper classes to help serialize/deserialize objects to/from disk.
//
// Uses the Pluggable Factories pattern described in this article:
// # http://www.adtmag.com/joop/crarticle.asp?ID=1520
//
// Usage of the whole system:
//
/// class SerializableClass : public Serializable
/// {
//  [...]
/// };
//
// // Here's our game entity class we want to serialize
/// class MyEntity : public SerializableClass
/// {
/// public:
///  MyEntity() : SerializableClass("MyEntity") {
//   [.. set attributes to default values ..]
///  }
///
///  MyEntity(Buffer& theParams) : SerializableClass("MyEntity") {
//   [.. read attributes' values from theParams in the same order as they were written in ::Serialize() ..]
///  }
///
///  virtual void Serialize(Buffer& theBuffer) {
///   SerializableClass::Serialize(theBuffer);
//   [.. add MyEntity own data to theBuffer here ..]
///  }
/// };
//
// // Then if I wanted to deserialize MyEntity objects..
/// class MyEntityDeserializer : public Deserializer<MyEntity>
/// {
/// private:
///  MyEntityDeserializer() : public Deserializer<MyEntity>("MyEntity") {}
///  
///  MyEntity* CreateObject(Buffer& theBuffer) const {
///   return new MyEntity(theBuffer);
///  }
///
//  // so it's automatically added to Deserializer<T>::mRegistry
///  static const MyEntityDeserializer registerThis;
/// };
//////////////////////////////////////////////////////////////////////////

#include <string>
#include <map>
#include "../SexyAppFramework/Buffer.h"

using namespace Sexy;
using namespace std;

namespace Sefrex
{
//////////////////////////////////////////////////////////////////////////
// Class: Deserializer
//
// Templated base class for all specific-class deserializers.
//////////////////////////////////////////////////////////////////////////
class Serializable
{
private:
string mClassName;

public:
Serializable(const string& theClassName);
virtual ~Serializable();

virtual void Serialize(Buffer& theBuffer) const;
};
//////////////////////////////////////////////////////////////////////////
// Class: Deserializer
//
// Templated base class for all specific-class deserializers.
//////////////////////////////////////////////////////////////////////////
template <class Object>
class Deserializer
{
public:
virtual ~Deserializer()
{
}

static Object* Deserialize(Buffer& theBuffer)
{
 string className = theBuffer.ReadString();
 Deserializer<Object> deserializer = (*mRegistry.find(className)).second;
 return deserializer->CreateObject(theBuffer);
}

protected:
Deserializer(const string& theClassName)
{
 mRegistry.insert(make_pair(theClassName, this));
}
virtual Object* CreateObject(Buffer& theBuffer) const = 0;

private:
typedef Deserializer<Object*>   DeserializerPtr;
typedef map<string, DeserializerPtr> DeserializerMap;

static DeserializerMap mRegistry;
};
}

#endif //__SERIALIZE_H__

y Serialize.cpp

#include "Serialize.h"
#include "SefrexUtils.h"
#include "SefrexAppBase.h"

using namespace Sexy;
using namespace Sefrex;

//////////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
Serializable::Serializable(const string& theClassName)
: mClassName(theClassName)
{
}

//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
Serializable::~Serializable()
{
}

//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
void
Serializable::Serialize(Buffer& theBuffer)
const
{
theBuffer.WriteString(theClassName);
}


EDIT: Vaya mierda de colorización y formateo de código que tiene este foro xDD
Manuel F. Lara
Descargar juegos indie  - blog sobre juegos indie y casual
El Desarrollo Personal.com  - blog sobre productividad, motivación y espíritu emprendedor

CoLSoN2

 Falta explicar algunas cosas:

1) Buffer se puede guardar o leer de un fichero en disco. Tal fichero tendría el formato:

para cada objeto serializado:
[STRING Clase
para cada atributo de la clase, empezando por la de las superclases, si las hay:
[$TIPO Miembro]
]

Luego podría ser algo como:

Sphere
23.5 // radio
ColoredSphere
61.7 // radio de Sphere
123 // r
50 // g
200 // b

2) El método que deserializa el objeto, el contrario a MyEntity::Serialize(), por si no queda claro, es el constructor que toma un Buffer& como parámetro.

3) Para serializar una serie de objetos a disco, sería (suponiendo que todos derivan de Serializable):


Buffer gameData;
for each entity in mGameEntities:
entity.Serialize(gameData);
gameData.SaveToFile("entities.txt");


y luego para deserializarlos:

Buffer gameData;
gameData.LoadFromFile("entities.txt");
MyEntity* entity = 0;
while (entity = MyEntityDeserializer::Deserialize(gameData))
mGameEntities.Add(entity);


Y ya tenemos la lista de entidades cargada de nuevo. Ahora que lo pienso tal como está ahora la clase Serializable y el ejemplo que hay, no podrías serializar todos los derivados de Serializable en un mismo fichero, sólo los derivados de MyEntity. Para ello debería añadir a la clase Serializable la funcionalidad de MyEntity y crear un SerializableDeserializer (que parece un trabalenguas), del cual todos los demás deserializadores deberían derivar. Y a eso voy!

EDIT: Lo único es que eso me daría como resultado Serializable*, que tampoco es demasiado útil, y castear a otro tipo de clase en base a su mClassName es más engorroso que dejarlo como está ahora, así que así se queda. Luego para guardarlo todo en un mismo fichero debería implementar un sistema de secciones o categorías, similar a la relación que hay ahora entre atributos y clases:

-Jerarquia
--Clase
---Miembro
---Miembro
---Miembro
--Clase
---Miembro
---Miembro
-Jerarquia
--Clase
---Miembro

como por ejemplo:


MyEntity  // tipo de deserializador que se usará, y por tanto de puntero que devolverá
Enemy // deriva de MyEntity
34 // x
14 // y
Player
25
13
EnvVars // otro tipo de objeto que se devolverá
IntVar // tipo de objeto a crear
GameType // atributo nombre de la variable
SP // atributo valor de la variable "GameType"

A picar teclas!
Manuel F. Lara
Descargar juegos indie  - blog sobre juegos indie y casual
El Desarrollo Personal.com  - blog sobre productividad, motivación y espíritu emprendedor

Grugnorr

 Si vas a serializar un grafo de objetos... piensa bien en los objetos que tengan punteros a otros  :lol: , lo suyo es asignar IDs donde los punteros, guardar sus datos una única vez y con mucho cuidadito reconstruir todo


PD: Como adivinarás, en C#, Java, Python y demás modernos y con un sistema de tipos unificado, todo ésto es automático, en C# que es el que conozco totalmente configurable y extensible


hat the hells!

ethernet

 Python es una verdadera gozada. En el juego que ando codeando ni me preocupo en formatos de fichero ni leches, directamente guardos los objetos y punto. Hace un tiempo puse un cotw que empaquetaba todo de maravilla ( http://www.stratos-ad.com/forums/index.php...=ST&f=28&t=3476 ) haciendo uso de pickle, que es el módulo serializer de python.

Por cierto, con permiso de los autores tomaré de este post algunas cosas para hacer un COTW.


CoLSoN2

 Sí, Grugnor, lo tengo presente. Es otro problema que tengo, pero supongo que el asignar IDs únicos a cada objeto es la única solución, me temo.

La verdad es que en cuanto a persistencia uno de los motores más cojonudos es Nebula2. Puedes guardar toda la escena en un fichero con un sólo comando. El fichero en sí es un conjunto de instrucciones que reconstruyen la escena tal como estaba, y puede estar en binario (como si fueran instrucciones de ASM), TCL, Lua, Python y creo que también Ruby. La caña. Pero algo así sería demasiado para un engine 2D que va a usarse para juegos tipo puzzle.

Más que un grafo, el problema lo tengo con triggers. Un trigger en mi engine es algo que ocurre en/durante/al cabo de un tiempo, tiene una interfaz similar a:

class Trigger
{
Trigger(float theDelay, float theDuration);

virtual void Start();
virtual void Update(float theFrameTime);
virtual void End();
};

Entonces se crean subclases para muchas cosas, como mover un sprite de un sitio a otro en un determinado tiempo, hacer una transición rara entre menús, explotar una bomba después de agotarse un contador, etc. Y por supuesto, estos triggers tienen punteros a objetos que se crean, gestionan y destruyen en otro sitio, lo cual es un problema.

Quizá bastaría con que la clase Serializable tuviera un miembro static int que fuera incrementandose en cada llamada al constructor de esa clase, así como un miembro privado int con el ID único de ese objeto. Aunque esto sólo se haría si se utilizara el constructor básico y no el que toma un Buffer& (que deserializa un objeto), ya que en ese caso se leería el ID del Buffer.

¿Creéis que es buena idea?

EDIT: También haría falta un std::map o vector static en Serializable para poder transformar de ID a puntero fácilmente (de puntero a ID no hace falta porque cada objeto tiene su ID como atributo).
Manuel F. Lara
Descargar juegos indie  - blog sobre juegos indie y casual
El Desarrollo Personal.com  - blog sobre productividad, motivación y espíritu emprendedor

fiero

 Yo siempre trabajo con ficheros a lo cruto, no serializando, así que cuando cambio alguna clase, los ficheros guardados anteriormente no me funcionarían. Tengo una duda con este sistema que comentais. Si guardas los datos del juego, y luego haces una nueva versión del ejecutable añadiendo o quitando alguna variable de las clases serializadas, ¿se cargarían los viejos datos?
www.videopanoramas.com Videopanoramas 3D player

zupervaca

 si quereis usar ya un fichero formateado usar el estandar xml

ethernet

Cita de: "fiero"Yo siempre trabajo con ficheros a lo cruto, no serializando, así que cuando cambio alguna clase, los ficheros guardados anteriormente no me funcionarían. Tengo una duda con este sistema que comentais. Si guardas los datos del juego, y luego haces una nueva versión del ejecutable añadiendo o quitando alguna variable de las clases serializadas, ¿se cargarían los viejos datos?
Yo para eso siempre tengo una variable llamada version que serializo. De esa manera siempre cargo esa variable la primera y compruebo si es la versión correcta.

fiero

Cita de: "ethernet"
Cita de: "fiero"Yo siempre trabajo con ficheros a lo cruto, no serializando, así que cuando cambio alguna clase, los ficheros guardados anteriormente no me funcionarían. Tengo una duda con este sistema que comentais. Si guardas los datos del juego, y luego haces una nueva versión del ejecutable añadiendo o quitando alguna variable de las clases serializadas, ¿se cargarían los viejos datos?
Yo para eso siempre tengo una variable llamada version que serializo. De esa manera siempre cargo esa variable la primera y compruebo si es la versión correcta.
Ah, vale, entonces todo esto de serializar las clases es para programar más cómodo. Gracias, duda resuelta.

un saludo
www.videopanoramas.com Videopanoramas 3D player






Stratos es un servicio gratuito, cuyos costes se cubren en parte con la publicidad.
Por favor, desactiva el bloqueador de anuncios en esta web para ayudar a que siga adelante.
Muchísimas gracias.