4
\$\begingroup\$

I have many types of entities, each type has a lot of if instanceof Spider (for example ) then do this in the game code... Now I got to the part where I want to save each type.

They all are based on the same class, Entity but implement some of their own methods. When I save them, lets say for now I just want their position and their health.

So I do write position the write health, to a file with all the entities.

Now here is the problem, When I want to load it, How will I know it's a spider and then create a Spider instance?

An easy fix is to create toString method then writing it before the position and health. But the problem with this is that when I load I will have to have this big switch statement for each type and I will have to manually add the case for a new type that I create. This is not very mod friendly.

What would be a way to read a name of a class and then create an instance of that class without having a big switch case?

\$\endgroup\$

closed as off-topic by Sean Middleditch, John McDonald, bummzack, Nicol Bolas, msell Aug 28 '13 at 8:35

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Programming questions that aren't specific to game development are off-topic here, but can be asked on Stack Overflow. A good rule of thumb is to ask yourself "would a professional game developer give me a better/different/more specific answer to this question than other programmers?"" – Sean Middleditch, John McDonald, bummzack, Nicol Bolas, msell
If this question can be reworded to fit the rules in the help center, please edit the question.

  • 6
    \$\begingroup\$ Is this scenario not covered by standard Java serialization? \$\endgroup\$ – Patrick Hughes Aug 27 '13 at 17:12
  • 1
    \$\begingroup\$ I agree with @PatrickHughes. I have only used the Java 'Serializable' interface for network messages, and that was a while ago, but I do believe it is exactly what you want, and most of the work has been done. Unfortunately, I can't at this time give you more advice than "Look up 'Java Serializeable', I think it is exactly what you want." \$\endgroup\$ – TASagent Aug 27 '13 at 18:05
  • \$\begingroup\$ As a side comment, in Java don't use toString() for other purposes than debugging \$\endgroup\$ – msell Aug 28 '13 at 5:29
8
\$\begingroup\$

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. Players are always 1, Spiders are always 2, Boxes 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.
\$\endgroup\$
  • \$\begingroup\$ This is quite a lot of work, and you've discussed the limitations of this approach. Can you identify any particular advantages to it? For reference, reflection utilities aren't going to have the CommonSpider/CaveSpider problem. \$\endgroup\$ – Katana314 Aug 27 '13 at 15:01
  • 1
    \$\begingroup\$ I've added a few advantages I could come up with, though these may not be particular to this approach. \$\endgroup\$ – Shotgun Ninja Aug 27 '13 at 15:33
  • 2
    \$\begingroup\$ If you dont need versioning, reflection should be used instead of writing custom ISaveable.load / save methods. You wont gain anything by not using it (except for a little performance of loading/saving, which could be gained again by emiting bytecode/code generation). Also your savehunk will actually complicate more complex strctures (like class inside class) you should use something like write(this.getPosition()) and custom formaters would override write method - but once you decide on format you probably wont change it anyways. \$\endgroup\$ – Kikaimaru Aug 27 '13 at 18:05
  • \$\begingroup\$ Great answer, in-depth a lot of examples and my favorite! :D \$\endgroup\$ – gopgop Aug 27 '13 at 18:11
  • \$\begingroup\$ Keep in mind, I only put this together on-the-spot - I give no claims to have tried this out and tested it. While this is a solution, it probably isn't the best solution. \$\endgroup\$ – Shotgun Ninja Aug 27 '13 at 18:15
4
\$\begingroup\$

The toString() part on its own doesn't seem so bad. I think one way of handling the re-loading of the objects would be through use of Reflection.

// an example of a forName argument: "java.util.Map"
Class loadedObjectClass = Class.forName(xml.getElementValue("clazzName"));
Object loadedObject = loadedObjectClass.newInstance();

I'll also note some people are going to be concerned over excessive use of type-checking; that's eventually going to lead you to the massive switch statements you were trying to avoid. Example: Instead of having a player collision method that checks if something is a spider, and damages the player, try having an onTouch() method for each object, that does nothing for most objects, and damages the touch-er inside the Spider class. (then, if the touch-er can't be damaged, you can just have their damage() method do nothing)

Finally, be wary of savvy users hacking the game if they see class names inside of the plaintext save files. There's no need to forcibly make single-player hacking impossible, but you might just want to obfuscate the save files a little bit to make it difficult.

\$\endgroup\$
  • \$\begingroup\$ This is a good way too, It seems simple. I will give it a shot and decide between the two methods. \$\endgroup\$ – gopgop Aug 27 '13 at 18:11
0
\$\begingroup\$

first of java has good OO let it shine

each time you used instanceof create a new instance method and reimplement in the subclass

you can also use getter methods that return parameters like base movement speed for use in standard methods

do the same for the saving and loading (like @shotgunninja does but each entity is ISavable)

and when loading have the hashmap (or just an array if you can ensure the order will remain the same ;) ) contain the Entity factories that creates a blank entity on which you can call load(SaveChunk) which will configure it (as a bonus this allows pooling when GC pauzes becomes too much for ya)

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.