I would avoid using reflection for something like this, and use a language-agnostic tagged blob format, or something like this (just one possible method for entity serialization):
Have an ISaveable
interface with a method that produces an Entity
given a hunk of save data, and produces a hunk of save data given an Entity
.
public interface ISaveable {
public SaveHunk save(Entity e);
public Entity load(SaveHunk sh);
public GUID getClassID();
}
Create anonymous instances of ISaveable
for each entity type (ie. Spider
, Player
, Box
, etc.) and have these know how to consume a hunk of save data (raw string, XML, JSON, etc.) and create the appropriate entity, how to convert that entity back into a hunk of save data.
ISaveable spiderSaveable = new ISaveable() {
@Override
public SaveHunk save(Entity e) {
if (e == null) return null;
if (!(e instanceof Spider)) {
return null;
}
SaveHunk hunk = new SaveHunk(getClassID(), e.getEntityID());
Spider spider = (Spider)e;
hunk.put("pos", spider.getPosition().toString());
hunk.put("vel", spider.getVelocity().toString());
// and so on
return hunk;
}
@Override
public Entity load(SaveHunk hunk) {
if (hunk== null) return null;
if (!hunk.getClassID().equals(getClassID())) {
// The GUIDs don't match, may want to handle this in the manager
return null;
}
Spider spider = new Spider(hunk.getEntityID());
spider.setPosition(Vector3.parse(hunk.get("pos")));
spider.setVelocity(Vector3.parse(hunk.get("vel")));
// and so on
return spider;
}
@Override
public GUID getClassID() {
return Spider.UNIQUE_CLASS_ID;
}
};
Have a manager class which can have ISaveable
instances attached to it for a globally unique ID that represents a single class (ie. Player
s are always 1, Spider
s are always 2, Box
es are always 3, so on.)
saveManager.addSaveable(spiderSaveable);
When you save, pass all entities in the world into the manager, fetch the ISaveable
corresponding with their class ID, and pass them into that ISaveable
's SaveHunk save(Entity e)
method, which downcasts the entity and converts its important values to a hunk of save data, and write that hunk to a file following the class ID.
for (Entity e : world.getEntities()) {
ISaveable saveable = saveManager.getSaveable(e.getClassID());
if (saveable != null) {
saveFileWriter.writeHunk(saveable.save(e));
} else {
// Log a warning
}
}
When you load, read the file starting from the class ID, fetch the corresponding ISaveable
for that class, then pass the hunk of save data from the file into the ISaveable
's Entity load(SaveHunk sh)
method.
for (SaveHunk hunk : saveHunksFromFile) {
ISaveable saveable = saveManager.getSaveable(hunk.getClassID());
if (saveable != null) {
world.loadEntity(saveable.load(hunk));
} else {
// Log a warning
}
}
LIMITATIONS
Note that this is only one single approach, and has its (drastic) limitations. Hooking all of the ISaveable
instances for each class type is nasty, and takes time to iterate across all entity classes. Plus, it has to be done at some point before creating the world, or you won't be able to load stuff into said world. In addition, if you change the ID for a class (say you want to remove an enemy type), then your save files all become broken or corrupted if they have that enemy type (which may be desirable or undesirable, depending on how well you handle that; my code just does nothing special in that situation).
This also limits how you use base classes and subclasses for entities; if you break out Spider
into CaveSpider
and FieldSpider
, for example, then which ISaveable
do you use? It effectively means you can't/shouldn't have branch classes be instantiated in your world, or that if you want a Spider
, then it has to have its own leaf class, like CommonSpider
, instead of using the more generic Spider
, otherwise you end up complicating your loaders and duplicating code between them. You could have a multi-step ISaveable
hierarchy, where stuff passes the rest of a hunk of save data down to more generic ISaveables
for base classes, but it raises the complexity of the system dramatically, since entities now have to know all of their own base classes.
ADVANTAGES
With this approach, or an approach similar to this, there are a number of advantages over using straight reflection.
- By using a factory-style approach to loading and saving, you avoid the need for a standard initialization procedure for all entity classes, since the knowledge of a class' creation and initialization is left up to the
ISaveable
instance for that class.
- You use a GUID and avoid using the fully-qualified class names in your save files, which allows for obfuscation and reduced save file size, not to mention easier porting of save files between languages (if needed for external development tools).
- There is a separation of concerns being observed between the Entity classes (which should only be concerned with the game itself) and the save/load system (which should only be concerned with saving and loading data).
- This method is easily adaptable to other architectures (ie. Component-Entity-System model) with only minor modification.
- By using a uniform construct for representing save data (ie.
SaveHunk
), you can easily switch the format of the actual save file, and retain the same code base.
- The switch-case is avoided in favor of a map lookup.