G
Guest
Guest
Archived from groups: rec.games.roguelike.development (More info?)
I've been racking my brain looking for the best way I can think of to
handle object persistence, so I offer here my latest attempt in the
hopes that others may benefit and improve on my thoughts.
In any object oriented roguelike design that I have considered, there
is a large number of objects that are interconnected by a complicated
web of references. This web is not a tree; many objects will be
referenced from several places. From a physical perspective, it seems
that it should be a tree: Each level contains a map, items, and
monsters, then items and monsters contain other items. But somehow it
never seems to work out to a strict tree, so I reject simply
iterating the tree and writing out it's contents. Instead I need
something similar to Java's serialization, a way of writing a data
structure that remembers each object that it writes and instead of
writing any object a second time it writes a reference back to the
first time it wrote that object. In that way the web of references is
preserved exactly.
Another issue is that our complex structure of objects will contain a
mixture of variable data that changes during a game and constant data
that stays the same for each game. We don't want to write the
constant data to our save file, but we still need the save file to
record which bit of constant data was in each of our objects. For
example: each of your monsters will have constant data such as it's
name, perhaps a brief text description of what it is and some numbers
to indicate its abilities. We don't want to put that into the save
file, we just want to put what type of monster it was and then look
up that information somehow when we restore the game.
Hopefully, constant data is kept in separate objects and not mixed
with variable data so that the constant data may be shared and not
duplicated during play. If that's the case, then we have two broad
groups of objects: variable and constant.
Finally, here is my design, expressed in Java interfaces:
/** This is the base class for all objects we might deal with,
constant and variable. Constant objects are free to do nothing for
write() and read().*/
interface Savable
{
/** This is were you write out all of your encapsulated data. Do
not include anything to distinguish your class from any other,
it is assumed that by the time this is read, the object to hold
this data has already been constructed by SaveFileFormat. You
may want to assist SaveFileFormat by including a name() method
for this class, according to taste.*/
public void write(SaveOutput out);
/** Here, a blank object takes the shape of a game object from the
past by reading in exactly what write() wrote. */
public void read(SaveInput in);
/** We will need equals and hashCode to implement a hash table to
find the ID code of each of our objects. */
public boolean equals(Object o);
public int hashCode();
}
/** This is the heart of the design, the registry. It is a one-to-one
mapping from our Savable objects to the ID codes that represent them
in our save file. It will probably be implemented as a hash table. We
could store the ID code in the game objects themselves, but that's
not game data and our game objects are already complicated enough. */
interface Registry
{
/** Create a new registry that has everything in this registry. We
don't actually need to copy this registry: the new registry can
merely refer to this registry for anything that it can't handle
itself. */
public Registry copy();
/** Assign an ID code to an object so that the ID code will be
written to the save file instead of calling write() on this
object. It is vital that we give the same ID to each object
when restoring it that we did when writing it, but we can count
on the objects coming in the same order. We can either use some
kind of naming system or we can use ints that are incremented
with each registration, or a mixture of both.
When we register constant objects, we will need them to get
consistent IDs for every game. If we ID them with incrementing
ints, we will have a fragile save file that will probably be
broken with the slightest change in order of number of constant
objects. */
public void register(Savable object);
/** Check if this registry has the given object so that we don't
have to write it to the save file. */
public boolean has(Savable object);
/** Write the ID code of this object to the save file. Give this
object an ID code if it doesn't have one already. */
public void write(Savable object, SaveOutput out);
/** Read an ID code from the save file and then return the object
that matches it. If the ID code has no object, throw an
exception. We could return null, but we are likely to want to
jump right out of the entire restoration process if the save
file has any errors.*/
public Savable read(SaveInput in);
}
/** This is where we keep knowledge of every class in our game. When
we add new classes the implementation of this will have to
change. Short of using introspection, code that must change with
each new savable class added is inevitable, so I put it here.
We don't have to worry about the given object being constant or
having been writen already, only data that needs writing will be
given to this object.*/
interface SaveFileFormat
{
/** Write a variable object to the save file. The write() method of
Savable will only be called here, so it can be left empty if
our file format doesn't need it to do anything. This method
will probably involve writing out some naming string or ID
number for the class of object being writen, then calling
object.write() */
public void write(Savable object, SaveOutput out);
/** Read in some object writen by write(), create an instance of it
and initialize it according to what you have read. This is the
only place the read() method of Savable will be called. */
public Savable read(SaveInput in);
}
/** This is where we isolate the actual messy IO activity of writing
the file. We will want to include a bunch of methods for writing
primitive data such as ints, floats, chars or whatever our game
might need to save itself. I include only ints here, to be
concise.
An implementation will need to hold a Registry and a
SaveFileFormat, one to write out brief IDs to keep track of
references and the other to write data. We handle constant
objects by pre-registering them and then copying the registry
when we create our SaveOutput.*/
interface SaveOutput
{
/** Actually put something to a file! */
public void writeInt(int value);
/** To keep our SaveFileFormat classes simple, we may be able to
use more than one. With this method, we can switch between
them. */
public void setFormat(SaveFileFormat f);
/** First we check our registry, if the given object is there, we
write a code then use the registry to write the ID of the
object. Otherwise, we write a different code and then use our
SaveFileFormat to write this object, then register this
object.*/
public void write(Savable object);
public void close();
}
/** Parallel to SaveOutput, we will need a copy of the constant
object registry and the same SaveFileFormats that were used for
writing. */
interface SaveInput
{
public int readInt();
public void setFormat(SaveFileFormat f);
/** Here we use the same codes from ArchiveOutput to know if we are
reading an ID or some data, then we use either our Registry or
our SaveFileFormat to read in the object. Each time we use
SaveFileFormat, we register the new object. Since we are
registering in the same order as we did when writing, each
restored object will get the same ID that it had. */
public Savable read();
public void close();
}
The idea behind this design is to take all the effort out of writing
a saved game from your game classes and moving it all into these four
specialized classes. Unfortunately, I have cheated quite blatantly
with SaveFileFormat, where most of the effort goes and for which I
have provided little implementation suggestion, but fortunately it at
least doesn't have to worry that it's pointers might not be handled
correctly or that constant objects might be writen to the file.
You can keep the information about which objects are constant and
which are variable only in the code that generates the constant
objects. Even constant objects do not need to know that about
themselves.
I can't think of how this might be applied to a non-object oriented
design.
I have never actually implemented this design in a roguelike, so try
it at your own risk.
I've been racking my brain looking for the best way I can think of to
handle object persistence, so I offer here my latest attempt in the
hopes that others may benefit and improve on my thoughts.
In any object oriented roguelike design that I have considered, there
is a large number of objects that are interconnected by a complicated
web of references. This web is not a tree; many objects will be
referenced from several places. From a physical perspective, it seems
that it should be a tree: Each level contains a map, items, and
monsters, then items and monsters contain other items. But somehow it
never seems to work out to a strict tree, so I reject simply
iterating the tree and writing out it's contents. Instead I need
something similar to Java's serialization, a way of writing a data
structure that remembers each object that it writes and instead of
writing any object a second time it writes a reference back to the
first time it wrote that object. In that way the web of references is
preserved exactly.
Another issue is that our complex structure of objects will contain a
mixture of variable data that changes during a game and constant data
that stays the same for each game. We don't want to write the
constant data to our save file, but we still need the save file to
record which bit of constant data was in each of our objects. For
example: each of your monsters will have constant data such as it's
name, perhaps a brief text description of what it is and some numbers
to indicate its abilities. We don't want to put that into the save
file, we just want to put what type of monster it was and then look
up that information somehow when we restore the game.
Hopefully, constant data is kept in separate objects and not mixed
with variable data so that the constant data may be shared and not
duplicated during play. If that's the case, then we have two broad
groups of objects: variable and constant.
Finally, here is my design, expressed in Java interfaces:
/** This is the base class for all objects we might deal with,
constant and variable. Constant objects are free to do nothing for
write() and read().*/
interface Savable
{
/** This is were you write out all of your encapsulated data. Do
not include anything to distinguish your class from any other,
it is assumed that by the time this is read, the object to hold
this data has already been constructed by SaveFileFormat. You
may want to assist SaveFileFormat by including a name() method
for this class, according to taste.*/
public void write(SaveOutput out);
/** Here, a blank object takes the shape of a game object from the
past by reading in exactly what write() wrote. */
public void read(SaveInput in);
/** We will need equals and hashCode to implement a hash table to
find the ID code of each of our objects. */
public boolean equals(Object o);
public int hashCode();
}
/** This is the heart of the design, the registry. It is a one-to-one
mapping from our Savable objects to the ID codes that represent them
in our save file. It will probably be implemented as a hash table. We
could store the ID code in the game objects themselves, but that's
not game data and our game objects are already complicated enough. */
interface Registry
{
/** Create a new registry that has everything in this registry. We
don't actually need to copy this registry: the new registry can
merely refer to this registry for anything that it can't handle
itself. */
public Registry copy();
/** Assign an ID code to an object so that the ID code will be
written to the save file instead of calling write() on this
object. It is vital that we give the same ID to each object
when restoring it that we did when writing it, but we can count
on the objects coming in the same order. We can either use some
kind of naming system or we can use ints that are incremented
with each registration, or a mixture of both.
When we register constant objects, we will need them to get
consistent IDs for every game. If we ID them with incrementing
ints, we will have a fragile save file that will probably be
broken with the slightest change in order of number of constant
objects. */
public void register(Savable object);
/** Check if this registry has the given object so that we don't
have to write it to the save file. */
public boolean has(Savable object);
/** Write the ID code of this object to the save file. Give this
object an ID code if it doesn't have one already. */
public void write(Savable object, SaveOutput out);
/** Read an ID code from the save file and then return the object
that matches it. If the ID code has no object, throw an
exception. We could return null, but we are likely to want to
jump right out of the entire restoration process if the save
file has any errors.*/
public Savable read(SaveInput in);
}
/** This is where we keep knowledge of every class in our game. When
we add new classes the implementation of this will have to
change. Short of using introspection, code that must change with
each new savable class added is inevitable, so I put it here.
We don't have to worry about the given object being constant or
having been writen already, only data that needs writing will be
given to this object.*/
interface SaveFileFormat
{
/** Write a variable object to the save file. The write() method of
Savable will only be called here, so it can be left empty if
our file format doesn't need it to do anything. This method
will probably involve writing out some naming string or ID
number for the class of object being writen, then calling
object.write() */
public void write(Savable object, SaveOutput out);
/** Read in some object writen by write(), create an instance of it
and initialize it according to what you have read. This is the
only place the read() method of Savable will be called. */
public Savable read(SaveInput in);
}
/** This is where we isolate the actual messy IO activity of writing
the file. We will want to include a bunch of methods for writing
primitive data such as ints, floats, chars or whatever our game
might need to save itself. I include only ints here, to be
concise.
An implementation will need to hold a Registry and a
SaveFileFormat, one to write out brief IDs to keep track of
references and the other to write data. We handle constant
objects by pre-registering them and then copying the registry
when we create our SaveOutput.*/
interface SaveOutput
{
/** Actually put something to a file! */
public void writeInt(int value);
/** To keep our SaveFileFormat classes simple, we may be able to
use more than one. With this method, we can switch between
them. */
public void setFormat(SaveFileFormat f);
/** First we check our registry, if the given object is there, we
write a code then use the registry to write the ID of the
object. Otherwise, we write a different code and then use our
SaveFileFormat to write this object, then register this
object.*/
public void write(Savable object);
public void close();
}
/** Parallel to SaveOutput, we will need a copy of the constant
object registry and the same SaveFileFormats that were used for
writing. */
interface SaveInput
{
public int readInt();
public void setFormat(SaveFileFormat f);
/** Here we use the same codes from ArchiveOutput to know if we are
reading an ID or some data, then we use either our Registry or
our SaveFileFormat to read in the object. Each time we use
SaveFileFormat, we register the new object. Since we are
registering in the same order as we did when writing, each
restored object will get the same ID that it had. */
public Savable read();
public void close();
}
The idea behind this design is to take all the effort out of writing
a saved game from your game classes and moving it all into these four
specialized classes. Unfortunately, I have cheated quite blatantly
with SaveFileFormat, where most of the effort goes and for which I
have provided little implementation suggestion, but fortunately it at
least doesn't have to worry that it's pointers might not be handled
correctly or that constant objects might be writen to the file.
You can keep the information about which objects are constant and
which are variable only in the code that generates the constant
objects. Even constant objects do not need to know that about
themselves.
I can't think of how this might be applied to a non-object oriented
design.
I have never actually implemented this design in a roguelike, so try
it at your own risk.