18
\$\begingroup\$

I want to develop a huge world map; at least 8000×6000 pixels in size. I have broken it into a 10×10 grid of 800×600-pixel PNG images. To avoid loading everything into memory, the images should be loaded and unloaded depending on the player's position in the grid.

For example, here is the player at position (3,3):

Player at (3,3)

As he moves to the right to (4,3), the three images at the far left are deallocated while the three images at the right are allocated:

Player moves to (4,3)

There should probably be a threshold inside each cell of the grid triggering the loading and unloading. The loading should probably happen in a separate thread.

How do I design such a system?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Quick suggestion: You've got two questions here, "how do I dynamically load tiles in a game" and "how do I do threading on XNA for XBox 360". Remove the OS-specific segments from this post - the tileloading code will be identical on XB360, Linux, and the Nintendo Wii - and make a new post for threading on XBox360. \$\endgroup\$
    – ZorbaTHut
    Jul 17, 2010 at 7:46
  • \$\begingroup\$ As for a question about your actual problem, is this a smooth-scrolling map, or a series of individual non-scrolling screens? \$\endgroup\$
    – ZorbaTHut
    Jul 17, 2010 at 7:47
  • \$\begingroup\$ If I understood "smooth-scrolling map", yes, it is. As for separating the question, I thought about it but kinda hesitated to ask to questions consequentially. But, I'll do it now. \$\endgroup\$
    – pek
    Jul 17, 2010 at 12:21

3 Answers 3

10
\$\begingroup\$

There are a few problems to solve here. The first is how to load and unload tiles. The ContentManager by default will not let you unload specific pieces of content. A custom implementation of this, however, will:

public class ExclusiveContentManager : ContentManager
{
    public ExclusiveContentManager(IServiceProvider serviceProvider,
        string RootDirectory) : base(serviceProvider, RootDirectory)
    {
    }

    public T LoadContentExclusive<T>(string AssetName)
    {
        return ReadAsset<T>(AssetName, null);
    }

    public void Unload(IDisposable ContentItem)
    {
        ContentItem.Dispose();
    }
}

The second issue is how to decide which tiles to load and unload. The following would solve this problem:

public class Tile
{
    public int X;
    public int Y;
    public Texture2D Texture;
}

int playerTileX;
int playerTileY;
int width;
int height;

ExclusiveContentManager contentEx;
Tile[,] tiles;

void updateLoadedTiles()
{
    List<Tile> loaded = new List<Tile>();

    // Determine which tiles need to be loaded
    for (int x = playerTileX - 1; x <= playerTileX + 1; x++)
        for (int y = playerTileY - 1; y <= playerTileY; y++)
        {
            if (x < 0 || x > width - 1) continue;
            if (y < 0 || y > height - 1) continue;

            loaded.Add(tiles[x, y]);
        }

    // Load and unload as necessary
    for (int x = 0; x < width; x++)
        for (int y = 0; y < height; y++)
        {
            if (loaded.Contains(tiles[x, y]))
            {
                if (tiles[x, y].Texture == null)
                    loadTile(x, y);
            }
            else
            {
                if (tiles[x, y].Texture != null)
                    contentEx.Unload(tiles[x, y].Texture);
            }
        }
}

void loadTile(int x, int y)
{
    Texture2D tex = contentEx.LoadEx<Texture2D>("tile_" + x + "_" + y);
    tiles[x, y].Texture = tex;
}

The final issue is how to decide when to load and unload. This is probably the easiest part. This could simply be done in the Update() method once the player's screen position was determined:

int playerScreenX;
int playerScreenY;
int tileWidth;
int tileHeight;

protected override void Update(GameTime gameTime)
{
    // Code to update player position, etc...

    // Load/unload
    int newPlayerTileY = (int)((float)playerScreenX / (float)tileWidth);
    int newPlayerTileX = (int)((float)playerScreenY / (float)tileHeight);

    if (newPlayerTileX != playerTileX || newPlayerTileY != playerTileY)
        updateLoadedTiles();

    base.Update(gameTime);
}

Of course you would also need to draw the tiles, but given tileWidth, tileHeight, and the Tiles[] array, this should be trivial.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you. Very nicely said. I would like to make a suggestion, if you know: could you describe how would the loading of tiles be done in a thread. I imagine that loading 3 800x600 tiles at once would pause slightly the game. \$\endgroup\$
    – pek
    Jul 18, 2010 at 17:15
  • \$\begingroup\$ The graphics card needs to be involved when creating a Texture2D object, so as far as I know it would still cause a skip even if loading were done on a second thread. \$\endgroup\$
    – Sean James
    Jul 18, 2010 at 18:41
0
\$\begingroup\$

What you want to do is pretty common. For a nice tutorial on this and other common techniques, check out this tile engine series.

If you haven't done anything like this before, I recommend watching the series. However, if you wanted you could get the last tutorials code. If you do the later, check out the draw method.

In a nutshell, you need to find your min and max X/Y points around the player. Once you have that you just loop through each one and draw that tile.

public override void Draw(GameTime gameTime)
{
    Point min = currentMap.WorldPointToTileIndex(camera.Position);
    Point max = currentMap.WorldPointToTileIndex( camera.Position +
        new Vector2(
            ScreenManager.Viewport.Width + currentMap.TileWidth,
            ScreenManager.Viewport.Height + currentMap.TileHeight));


    min.X = (int)Math.Max(min.X, 0);
    min.Y = (int)Math.Max(min.Y, 0);
    max.X = (int)Math.Min(max.X, currentMap.Width);
    max.Y = (int)Math.Min(max.Y, currentMap.Height + 100);

    ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend
                                    , SpriteSortMode.Immediate
                                    , SaveStateMode.None
                                    , camera.TransformMatrix);
    for (int x = min.X; x < max.X; x++)
    {
        for (int y = min.Y; y < max.Y; y++)
        {

            Tile tile = Tiles[x, y];
            if (tile == null)
                continue;

            Rectangle currentPos = new Rectangle(x * currentMap.TileWidth, y * currentMap.TileHeight, currentMap.TileWidth, currentMap.TileHeight);
            ScreenManager.SpriteBatch.Draw(tile.Texture, currentPos, tile.Source, Color.White);
        }
    }

    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

public Point WorldPointToTileIndex(Vector2 worldPoint)
{
    worldPoint.X = MathHelper.Clamp(worldPoint.X, 0, Width * TileWidth);
    worldPoint.Y = MathHelper.Clamp(worldPoint.Y, 0, Height * TileHeight);


    Point p = new Point();

    // simple conversion to tile indices
    p.X = (int)Math.Floor(worldPoint.X / TileWidth);
    p.Y = (int)Math.Floor(worldPoint.Y / TileWidth);

    return p;
}

As you can see there is a lot going on. You need a camera that is going to create your matrix, you need to convert your current pixel location to a tile index, you find your min/max points ( I add a little to my MAX so it draws beyond the visible screen a little bit), and then you can draw it.

I really suggest watching the tutorial series. He covers your current problem, how to create a tile editor, keeping the player within bounds, sprite animation, AI interaction, etc...

On a side note TiledLib has this built in. You could study their code as well.

\$\endgroup\$
4
  • \$\begingroup\$ While this is very helpful, it only covers the rendering of the tile map. What about loading and unloading relevant tiles? I cannot load 100 800x600 tiles in memory. I will take a look at the video tutorials; thanks. \$\endgroup\$
    – pek
    Jul 16, 2010 at 14:35
  • \$\begingroup\$ ahhh.. I misunderstood your question. So are you using a traditional tilemap, or do you have a big image for your level that you broke up into pieces? \$\endgroup\$
    – user159
    Jul 16, 2010 at 14:56
  • \$\begingroup\$ Just a thought....you could store the names of your tiles in an array, and use the same technique to know what to load and draw. Keep in mind to try to re-use any objects or you could create a lot of garbage \$\endgroup\$
    – user159
    Jul 16, 2010 at 15:12
  • \$\begingroup\$ The second: the map is 8000x6000 broken into 10x10 tiles of 800x600 px. As for reuse, I already have a content manager that is responsible for that. \$\endgroup\$
    – pek
    Jul 17, 2010 at 6:55
0
\$\begingroup\$

I've been working on something very similar for my current project. This is a quick run down of how I'm doing it, with some side notes on how to make things a bit easier on yourself.

For me, the first problem was breaking the world into smaller chunks that would be appropriate to load and unload on the fly. Since you're using a tile based map, that step becomes significantly easier. Rather than considering the positions of each 3D object in the level, you already have your level split nicely into tiles. This lets you simply break up the world into X by Y tile chunks, and load those.

You're going to want to do that automatically, rather than by hand. Since you're using XNA, you have an option of using the Content Pipeline with a custom exporter for your level content. Unless you know of some way to run the export process without recompiling, I'd honestly recommend against it. While C# isn't nearly as slow to compile as C++ tends to be, you still don't want to have to load Visual Studio and recompile your game every time you make a minor change to the map.

Another important thing here is to make sure you use a good naming convention for the files containing your level's chunks. You want to be able to know that you want to load or unload chunk C, and then generate the filename you need to load to do that at run time. Finally, think of any little things that might help you down the road. It's really nice to be able to change how large a chunk is, re-export, and then see the effects of that on performance immediately.

At run time, it's still pretty straightforward. You'll need some way to asynchronously load and unload chunks, but this is highly dependent on how your game or engine works. Your second image is exactly correct -- you need to determine what chunks should be loaded or unloaded, and make the appropriate requests to make that the case if it is not already. Depending on how many chunks you have loaded at one time, you could just do this whenever the player cross a boundary from one chunk to the next. After all, either way, you want to make sure that enough is loaded that even in the worst (reasonable) load time, the chunk is still loaded before the player can see it. You're probably going to want to play with this number a lot until you get a good balance between performance and memory consumption.

As far as actual architecture goes, you're going to want to abstract the process of actually loading and unloading the data from memory from the process of determining what should be loaded/unloaded. For your first iteration, I wouldn't even worry about performance of loading/unloading and just get the simplest thing that could possibly work, and ensure that you are generating the appropriate requests at the appropriate times. After that, you can look at optimizing how you load to minimize garbage.

I ran into a lot of additional complexity due to the engine I was using, but that's pretty implementation specific. If you've got any questions about what I did, please comment and I'll do what I can to help out.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ You can use msbuild to run the content pipeline outside of Visual Studio. The second WinForms sample has you covered: creators.xna.com/en-US/sample/winforms_series2 \$\endgroup\$ Jul 18, 2010 at 14:42
  • \$\begingroup\$ @Andrew !!!!! Thank you SO much!!! I was about to completely abandon the content pipeline for exactly that reason! \$\endgroup\$
    – pek
    Jul 18, 2010 at 16:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .