Question

I've been having trouble trying to implement this for a couple of days now. I've searched extensively on similar questions in regards to what I'm trying to do but I haven't come across a question that helps my issues directly.

Basically I'm rendering tiles onto a grid on my UserControl class. This is for my Tile Engine based world editor I'm developing. Here is a screenshot of an open world document and some tiles brushed on.

enter image description here

Initially, I was going to use a Bitmap in my control that would be the world's preview canvas. Using a brush tool for example, when you move your mouse and have the left button down, it sets the nearest tile beneath your cursor to the brush's tile, and paints it on the layer bitmap. The control's OnPaint method is overridden to where the layer bitmap is draw with respect to the paint event's clipping rectangle.

The issue with this method is that when dealing with large worlds, the bitmap will be extremely large. I need this application to be versatile with world sizes, and it's quite obvious there are performance issues when rendering large bitmaps onto the control each time it's invalidated.

Currently, I'm drawing the tiles onto the control directly in my control's overridden OnPaint event. This is great because it doesn't require a lot of memory. For example, a (1000, 1000) world at (20, 20) per tile (total canvas size is (20000, 20000)) runs at about 18mb of memory for the whole application. While not memory intensive, it's pretty processor intensive because every time the control is invalidated it iterates through every tile in the viewport. This produces a very annoying flicker.

What I want to accomplish is a way to meet in the middle as far as memory usage and performance. Essentially double buffer the world so that there isn't flickering when the control is redrawn (form resize, focus and blur, scrolling, etc). Take Photoshop for example - how does it render the open document when it overflows the container viewport?

For reference, here's my control's OnPaint override that is using the direct draw method mentioned above.

getRenderBounds returns a rectangle relative to PaintEventArgs.ClipRectangle that is used to render visible tiles, instead of looping through all the tiles in the world and checking if it's visible.

protected override void OnPaint(PaintEventArgs e)
{
    WorldSettings settings = worldSettings();

    Rectangle bounds = getRenderBounds(e.ClipRectangle),
        drawLocation = new Rectangle(Point.Empty, settings.TileSize);

    e.Graphics.InterpolationMode = 
        System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    e.Graphics.SmoothingMode = 
        System.Drawing.Drawing2D.SmoothingMode.None;
    e.Graphics.PixelOffsetMode = 
        System.Drawing.Drawing2D.PixelOffsetMode.None;
    e.Graphics.CompositingQuality = 
        System.Drawing.Drawing2D.CompositingQuality.HighSpeed;

    for (int x = bounds.X; x < bounds.Width; x++)
    {
        for (int y = bounds.Y; y < bounds.Height; y++)
        {
            if (!inWorld(x, y))
                continue;

            Tile tile = getTile(x, y);

            if (tile == null)
                continue;

            drawLocation.X = x * settings.TileSize.Width;
            drawLocation.Y = y * settings.TileSize.Height;

            e.Graphics.DrawImage(img, 
                drawLocation, 
                tileRectangle, 
                GraphicsUnit.Pixel);
        }
    }
}

Just comment if you need some more context from my code.

Was it helpful?

Solution

The trick is to not use a big bitmap for this at all. You only need a bitmap covering the visible area. Then you draw whatever is visible.

To achieve this you will need to maintain the data separately from the bitmap. This can be a simple array or an array/list with a simple class holding information for each block such as world position.

When your block is within the visible area then you draw it. You may or may not have to iterate through the whole array, but that isn't really a problem (you can also calculate the visible array on a separate thread). You can also make the function more intelligent by creating region indexes so you don't iterate all blocks.

To add a new block to the array, calculate it's canvas position to world coordinates, add it and then render the array again (or the area where the block is drawn).

This is how controls with scrollable areas are drawn by the system too.

Enable double-buffering will keep it clear and flicker-less.

In this case I would also use a panel with separate scroll bars and calculate the scroll-bars' relative position.

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