Load Times for xTile Maps?

Oct 19, 2011 at 2:37 AM
Edited Oct 19, 2011 at 4:58 AM

I'm debugging on Xbox and I've noticed that smaller maps have next to no load time, which is to be expected. Larger maps, however, take an oddly long time to load. Even though post-load performance is fine, it's something like 30 seconds for a 128x128 tile map to load. Is this normal? I'm just doing a regular "map = Content.Load<Map>("filename")" call. Is there something else I should be doing?

 

Edit: Uh oh, I found it. It's my own code causing the exponential load times. That's what I get for populating my door and NPC locations by scanning every tile of every layer when the map loads.... The actual map load time is next to nothing. I have to recode my data structures, but on the bright side, I can make my maps REALLY BIG now! I wonder, what's the maximum map size the 360 can run without slowdown?

Oct 19, 2011 at 8:38 AM
Edited Oct 19, 2011 at 8:40 AM

That's quite odd. A 128 x 128 map is fairly small. As a workaround could you try saving a copy of the map in .tbin format and include that as your content instead of the one in .tide format? It could be a case that the XML processing requiring for .tide is slow - I had to rewrite some of this since the .NET Frameworks's XML deserialisation functionality that I originally used for the Windows version is not available in Xbox 360.

Oct 19, 2011 at 12:42 PM

Well, I figured that the actual map loads weren't causing the delay, it was my two functions that check where doors and NPCs are. I did try saving as tbin, but I realized that map loads weren't the problem when I tried loading one of my big maps in the xTile 360 demo and it loaded immediately. The problem is what I'm doing with the maps immediately after loading them.

As soon as I load a map, I have it scan through every tile of every layer to identify where doors and NPCs have been placed, and interpret the tag values of the relevant tiles:

 

        public void PopulateDoors()
        {
            #region LandingDirection
            foreach (TileSheet i in Globals.GameMap.TileSheets)
            {
                for (int j = 0; j < 100; j++)
                {
                    if (i.TileIndexProperties[j].ContainsKey("LandingDirection"))
                    {
                        MapDoor[j].LandingDirection = i.TileIndexProperties[j]["LandingDirection"];
                        Console.WriteLine("Door " + j + "'s LandingDirection is " + MapDoor[j].LandingDirection);
                    }
                }
            }
            #endregion

            #region DestinationDoor
            foreach (TileSheet i in Globals.GameMap.TileSheets)
            {
                for (int j = 0; j < 100; j++)
                {
                    if (i.TileIndexProperties[j].ContainsKey("DestinationDoor"))
                    {
                        MapDoor[j].DestinationDoor = i.TileIndexProperties[j]["DestinationDoor"];
                        Console.WriteLine("Door " + j + "'s destination door is " + MapDoor[j].DestinationDoor);
                    }
                    else
                        MapDoor[j].DestinationDoor = 0;
                }
            }
            #endregion

            #region Door ID
            foreach (Layer i in Globals.GameMap.Layers)
            {
                for (int j = 0; j < 100; j++)
                {
                    for (int y = 0; y < (Globals.TileMapHeight); y++)
                    {
                        for (int x = 0; x < (Globals.TileMapWidth); x++)
                        {
                            TempLocation.X = x;
                            TempLocation.Y = y;
                            if (i.Tiles[TempLocation] != null && i.Tiles[TempLocation].TileIndexProperties.ContainsKey("Door") && i.Tiles[TempLocation].TileIndexProperties["Door"] == j)
                            {
                                MapDoor[j].X = x;
                                MapDoor[j].Y = y;
                                Console.WriteLine("Door " + j + " is at " + MapDoor[j].X + " " + MapDoor[j].Y);
                            }
                        }
                    }
                }
            }
            #endregion

        }

        public void PopulateNPCs()
        {
            //reset positions
            foreach (MapCharacter i in Globals.CollisionCharacters)
            {
                i.X = 0;
                i.Y = 0;
                i.TilePosX = 0;
                i.TilePosY = 0;
            }

            for (int j = 0; j < 99; j++)
            {
                Globals.npcCharacter[j].X = 0;
                Globals.npcCharacter[j].Y = 0;
                Globals.npcCharacter[j].Visible = false;
                Globals.npcCharacter[j].IsWandering = false;
                Globals.npcCharacter[j].CountToMove = 0;
                Globals.npcCharacter[j].isMoving = false;
                Globals.npcCharacter[j].DestinationTileX = -1;
                Globals.npcCharacter[j].DestinationTileY = -1;
            }

            for (int j = 0; j < 99; j++) 
            {
                

                foreach (Layer i in Globals.GameMap.Layers)
                {
                    for (int y = 0; y < (Globals.TileMapHeight); y++)
                    {
                        for (int x = 0; x < (Globals.TileMapWidth); x++)
                        {
                            TempLocation.X = x;
                            TempLocation.Y = y;
                            if (i.Tiles[TempLocation] != null && i.Tiles[TempLocation].TileIndexProperties.ContainsKey("NPC") && i.Tiles[TempLocation].TileIndexProperties["NPC"] == j)
                            {
                                Globals.npcCharacter[j].X = x * Globals.TileSize;
                                Globals.npcCharacter[j].Y = y * Globals.TileSize;
                                Globals.npcCharacter[j].Visible = true;
                                Globals.npcCharacter[j].IsWandering = true;
                                if (i.Tiles[TempLocation].TileIndexProperties.ContainsKey("DialogBox"))
                                    Globals.npcCharacter[j].DialogBox = i.Tiles[TempLocation].TileIndexProperties["DialogBox"];
                            }
                        }
                    }
                }
            }
        }

 

 

I'm going to have to rewrite those two aspects of the engine to use a list of doors and NPCs instead of tile tags. I liked this system because I could define properties as tags on a tile index and just place the tile on the map, but the time it takes to look for tiles with the right properties isn't reasonable on Xbox. I could replace this with map properties, but that's not as flexible.

Oct 19, 2011 at 1:11 PM

Before you do any drastic changes, may I suggest the following:

In PopulateDoors(), do not loop 3 times over all the tiles - you can do the checks for LandingDirection and DestinationDoor properties in one loop. Also, you can place your Door ID code within that same loop - and you don't need the for (j = 0..99) outer loop for that - just keep an element index for the next door to assign in MapDoor, and whenever you encounter a tile with the "Door" property in it, just assign it to the current element index and then increment it by one for the next round. 

You can use the same principle for assigning NPCs to the npcCharacter layer and stick that within the same nested tile loop. I think you should be able to bring execution of your code down to a couple of seconds or so.

 

Finally, if you need to handle really huge maps with lots of doors, NPCs etc., you could instead scan the region around the viewport during the game loop and activate the entities that are in the vicinity only, and conversely disable those that get out of range. A typical area to scan for would be a region 2 or 3 times the width and height of the viewport, so you can give time to your entities to scroll into view and do their own thing. Anyhow, this may or may not apply to your game as you may want to have all your entities active all the time for all I know.

 

Hope that helps.

Oct 19, 2011 at 1:32 PM

Not sure why I didn't nest those first two properties before, that's fixed now. The Door ID, however, is how I determine where on the map a door is. This isn't just for checking if you're on a door, but for coming through a door as well. On a map load, it has a "landing door" number, and it positions the player at that door's coordinates. I don't set those coordinates as properties, but rather, by placing the Door ID XX tile on a map layer.

I've gotten it down to a few seconds to load a 128^2 map, but it limits me to scanning through one layer for my doors. That's how I store them anyway, but I liked being able to have an indefinite number of layers and not use a specific layer number.

#region Door ID
            //foreach (Layer i in Globals.GameMap.Layers)
            {
                //for (int j = 0; j < 100; j++)
                {
                    for (int y = 0; y < (Globals.TileMapHeight); y++)
                    {
                        for (int x = 0; x < (Globals.TileMapWidth); x++)
                        {
                            TempLocation.X = x;
                            TempLocation.Y = y;
                            if (Globals.GameMap.Layers[1].Tiles[TempLocation] != null && Globals.GameMap.Layers[1].Tiles[TempLocation].TileIndexProperties.ContainsKey("Door"))
                            {
                                MapDoor[Globals.GameMap.Layers[1].Tiles[TempLocation].TileIndexProperties["Door"]].X = x;
                                MapDoor[Globals.GameMap.Layers[1].Tiles[TempLocation].TileIndexProperties["Door"]].Y = y;
                                //Console.WriteLine("Door " + j + " is at " + MapDoor[j].X + " " + MapDoor[j].Y);
                            }
                        }
                    }
                }
            }

Oct 19, 2011 at 1:55 PM

I may be missing something subtle in what you're trying to accomplish, but what I'm saying is that I think you can plug the door location assignment code within your inner loop above into the loop that is also handling LandingDirection and Destination door. So basically, you just scan your map layer (or layers) once. That is, each tile gets checked once for any applicable properties and you map your door and NPC data in one pass.

 

Oct 19, 2011 at 2:15 PM

The thing is that the LandingDirection and DestinationDoor properties are scanned from the TileSheets, not the Layers. I get locations from where I place a tile on the layer. If I stored locations as properties on the TileSheet's tiles, I could do it all in one pass, but I'd lose the ability to place doors on the map in the GUI. I'd be using TileIndex properties like DoorPosX and DoorPosY that I have to type manually for each door.

Oct 19, 2011 at 3:00 PM

That was the thing I missed then :)

Oct 19, 2011 at 11:48 PM
colinvella wrote:
Also, you can place your Door ID code within that same loop - and you don't need the for (j = 0..99) outer loop for that - just keep an element index for the next door to assign in MapDoor, and whenever you encounter a tile with the "Door" property in it, just assign it to the current element index and then increment it by one for the next round. 

 

I couldn't test it until I got home, but after thinking about it all day, I realized that this was the key to the issue. Thanks for pointing it out!

Instead of working through the full 0-99 index of NPCs and Doors, I just made it scan one layer and pull the NPC or Door number from the TileIndexProperty instead of the current counting number. Where the counting number i would have been, I instead use the tile's TileIndex. TileIndex 0 on the Door sheet has the properties for Door 0, and it continues through 99, so there's no need for a counting number.

Now, it goes through the designated layer once to look for for existing Doors and once for NPCs instead of going through every layer 100 times to check for each potential entry. After fixing both the Door and NPC populate functions, load times are instant on Xbox (or close enough), ALMOST instant on the phone emulator, and I could probably even combine the two functions into one call for even more optimization. This means I don't have to change how I place doors and NPCs. I haven't tried going back to scanning each layer yet, but I'm guessing that scanning 3-4 layers once is a lot faster than scanning 3-4 layers 100 times.

Oct 20, 2011 at 9:28 AM

There are some other minor optimisations you can do like storing the value of Globals.GameMap.Layers[1].Tiles[TempLocation].TileIndexProperties["Door"] in an variable and using that to index into MapDoor[] instead of invoking the full expression multiple times per loop, as in:

int mapDoorIndex = Globals.GameMap.Layers[1].Tiles[TempLocation].TileIndexProperties["Door"];
MapDoor[mapDoorIndex].X = x;
MapDoor[mapDoorIndex].Y = y;

It will not drastically speed up your code, but every little bit helps.

Oct 20, 2011 at 1:14 PM

I'm actually completely doing away with the "Door" TileIndex Property, so I don't have to set it for each tile. That gets tedious when I make new maps and change tilesets. Instead, I'm switching all of my layer references to do a foreach loop through the layers to find the layer with a specific ID, because I name my layers consistently between maps. That way, Layer 1 on map A can be fringe tiles, and on another map I can have Doors on Layer 1. Here's the cleaned up (and tested) PopulateDoors():

public void PopulateDoors()
        {
            foreach (Layer i in Globals.GameMap.Layers)
            {
                if (i.Id == "Function")
                {
                    for (int y = 0; y < (Globals.TileMapHeight); y++)
                    {
                        for (int x = 0; x < (Globals.TileMapWidth); x++)
                        {
                            Tile TempTile = i.Tiles[x, y];

                            //Door ID
                            if (TempTile != null && TempTile.TileSheet.Id == "Function - Doors")
                            {
                                int MapDoorIndex = TempTile.TileIndex;
                                MapDoor[MapDoorIndex].X = x;
                                MapDoor[MapDoorIndex].Y = y;

                                //Landing Direction
                                if (TempTile.TileIndexProperties.ContainsKey("LandingDirection"))
                                    MapDoor[MapDoorIndex].LandingDirection = TempTile.TileIndexProperties["LandingDirection"];

                                //DestinationDoor
                                if (TempTile.TileIndexProperties.ContainsKey("DestinationDoor"))
                                    MapDoor[MapDoorIndex].DestinationDoor = TempTile.TileIndexProperties["DestinationDoor"];
                                else
                                    MapDoor[MapDoorIndex].DestinationDoor = 0;
                            }
                        }
                    }
                }
            }
        }

I can probably put the NPC loop inside this function as well. The Door loop starts with "if (TempTile != null && TempTile.TileSheet.Id == "Function - Doors")," and contains all of the door checks, so I could make a loop just below it for NPCs and use the same TempTile.