Question

I've been working on a game project with a small group for a bit now and we've hit a block. One of the features of this game is the ability for users to generate their own levels through the use of an in-game editor. The editor creates a Level object which stores the length and width of the level and a two-dimensional array of Tile objects. We've successfully implemented the camera system and can edit together a simple concept level without too much difficulty, but the process of successfully saving the level and loading it back up later is a concept that's proving difficult, and I was hoping one of you could offer some guidance to get the intended functionality down.

In its current state, when the user presses the 'S' key, our LevelManager class runs the SaveLevel method below:

    public static void SaveLevel()
    {
        XmlWriterSettings settings = new XmlWriterSettings();
        settings.Indent = true;

        using (XmlWriter writer = XmlWriter.Create("example.xml", settings))
        {
            IntermediateSerializer.Serialize(writer, CurrentLevel, null);
        }
    }

Which serializes our level (CurrentLevel) into an XML file in the project (We'll worry about saving to different files after we get this basic setup working.) I ran the program, created a small map and saved it, and here's the output in the resulting XML file:

    <?xml version="1.0" encoding="utf-8"?>
    <XnaContent>
      <Asset Type="LevelEditorPrototype.Level">
        <TileGrid>
          <Item>
            <Item Type="LevelEditorPrototype.TileFloor">
              <X>0</X>
              <Y>0</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFFFFFF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileBlock">
              <X>0</X>
              <Y>32</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FF0000FF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileVoid">
              <X>0</X>
              <Y>64</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFF0000</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileFloor">
              <X>0</X>
              <Y>96</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFFFFFF</Tint>
            </Item>
          </Item>
          <Item>
            <Item Type="LevelEditorPrototype.TileVoid">
              <X>32</X>
              <Y>0</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFF0000</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileFloor">
              <X>32</X>
              <Y>32</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFFFFFF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileBlock">
              <X>32</X>
              <Y>64</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FF0000FF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileVoid">
              <X>32</X>
              <Y>96</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFF0000</Tint>
            </Item>
          </Item>
          <Item>
            <Item Type="LevelEditorPrototype.TileBlock">
              <X>64</X>
              <Y>0</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FF0000FF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileVoid">
              <X>64</X>
              <Y>32</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFF0000</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileFloor">
              <X>64</X>
              <Y>64</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFFFFFF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileBlock">
              <X>64</X>
              <Y>96</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FF0000FF</Tint>
            </Item>
          </Item>
          <Item>
            <Item Type="LevelEditorPrototype.TileFloor">
              <X>96</X>
              <Y>0</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFFFFFF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileBlock">
              <X>96</X>
              <Y>32</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FF0000FF</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileVoid">
              <X>96</X>
              <Y>64</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFF0000</Tint>
            </Item>
            <Item Type="LevelEditorPrototype.TileFloor">
              <X>96</X>
              <Y>96</Y>
              <Width>32</Width>
              <Height>32</Height>
              <Origin>0 0</Origin>
              <Depth>0</Depth>
              <Tint>FFFFFFFF</Tint>
            </Item>
          </Item>
        </TileGrid>
      </Asset>
    </XnaContent>

So at the very least we do have data information on the tiles generated in the level, so that's something. We'd like our users to be able to load up saved levels during runtime as well, so we mapped the 'L' key to load that saved XML file, and that's where the problem shows up. Our read looks like this:

    public static void LoadLevel()
    {
        using (FileStream stream = new FileStream("example.xml", FileMode.Open))
        {
            using (XmlReader reader = XmlReader.Create(stream))
            {

                currentLevel = IntermediateSerializer.Deserialize<Level>(reader, null);
            }
        }
    }

When we try testing that functionality, we get this error:

    System.MethodAccessException was unhandled
      HResult=-2146233072
      Message=Attempt by method 'DynamicClass.ReflectionEmitUtils(System.Object, System.Object)' to access method 'DynamicClass.ReflectionEmitUtils(System.Object, System.Object)' failed.

I have a sneaking suspicion that the IntermediateSerializer doesn't quite work how we want it to work, but I'm not sure how else to parse and store the data effectively. Is there a different setup I should be using here?

Was it helpful?

Solution

The most effective way to be 100% sure of saving and loading tile-based levels have, in my experience, to use BinaryWriter and BinaryReader.

Our level-structure has been a lot different from yours, though; We have many layers. Each layer uses a tileset and consists of instances of Tile. Tile holds its position (Vector2D) and a TileId (index of the tile in the tileset-texture).

The way we put objects in levels are with tilesets that are replaced by real objects on loading.

Anyways, a suitably generic way to save and load data is by letting your classes have a contructor that can take a BinaryReader as an argument, and a method to write itself to a BinaryWriter.

like this:

public Tile(BinaryReader reader)
{
    Position.X = reader.ReadFloat();
    Position.Y = reader.ReadFloat();
    TileId = reader.ReadInt32();
}

public void WriteToStream(BinaryWriter writer)
{
    writer.Write(Position.X);
    writer.Write(Position.Y);
    writer.Write(TileId);
}

if you only have one class to load, you can then simply:

for loading:

var tiles = new List<Tile>();
var reader = new BinaryReader(File.Open("level.bin"));

while (reader.BaseStream.Position < reader.BaseStream.Length)
{
    var tile = new Tile(reader);
    tiles.Add(tile);
}
reader.Close();

For saving:

var tiles; //lets pretend this is the level
var writer = new BinaryWriter(File.Create("level.bin"));

foreach (var tile in tiles)
{
    tile.WriteToStream(writer);
}

writer.Flush(); //IMPORTANT!!!
writer.Close();

If, however, your list contains items of different types, you need to store the type also. A pretty generic way to do this is by inserting:

writer.Write(tile.GetType().FullName);

before tile.WriteToStream(writer);

and then on loading you need to:

var tileType = Type.GetType(reader.ReadString()); //Read the type from the stream
var constructor = tileType.GetConstructor(new [] { typeof(BinaryReader)}); //get a constructor that can use a binaryreader
var tile = constructor.Invoke(new [] { reader }); //use said constructor to create an instance

Hope this helps. And note that I am writing this out of memory, so syntax errors are probable....

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top