4

I am rendering a tile map in my MonoGame project using a sprite batch.

Here is the rendering code:

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.Black);

        SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, CameraManager.pInstance.pFinalTransform);

        foreach( TmxLayer Layer in mCurrentTileMap.Layers)
        {
            for (int y = 0; y < mCurrentTileMap.Height; y++)
            {
                for (int x = 0; x < mCurrentTileMap.Width; x++)
                {
                    TmxLayerTile Tile = Layer.Tiles[mCurrentTileMap.Width * y + x];
                    int TileID = -1;
                    Texture2D Texture = FindTileMapTextureForTileID(Tile.Gid, out TileID);
                    if (TileID > 0)
                    {
                        int atlasX = TileID % (Texture.Width / TileSize);
                        int atlasY = TileID / (Texture.Width / TileSize);

                        SpriteEffects FX = SpriteEffects.None;
                        if (Tile.HorizontalFlip)
                        {
                            FX = SpriteEffects.FlipHorizontally;
                        }

                        SpriteBatch.Draw(Texture, new Vector2(x * TileSize, y * TileSize), new Microsoft.Xna.Framework.Rectangle(atlasX * TileSize, atlasY * TileSize, TileSize, TileSize), Color.White, 0, Vector2.Zero, 1, FX, 0);
                    }
                }
            }
        }

Here's what I think are the important lines of code:

SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, 
  null, null, null, CameraManager.pInstance.pFinalTransform);

SpriteBatch.Draw(Texture, new Vector2(x * TileSize, y * TileSize), 
  new Microsoft.Xna.Framework.Rectangle(atlasX * TileSize, atlasY * TileSize, TileSize, TileSize), 
  Color.White, 0, Vector2.Zero, 1, FX, 0);

As you can see, I am using SamplerState.PointClamp. However, as I move around the world, I see this kind of thing once in a while; below some of the tiles you can see a single line of pixels, which are the pixels below it in the sprite atlas.

Enlarge to see issue better Click to enlarge to see issue clearly...

I am pretty sure this is happening because the transform applied to the sprite batch (CameraManager.pInstance.pFinalTransform) is using floating point percision. If I forcefully clamp the scale portion of that transform to whole numbers, the issues does not occur. However, doing that causes very jittery camera movement (my game is very scaled up so each pixel is about 10 pixels on the monitor), so it is not a good solution.

Any ideas on how this effect can be avoided?

Here is what the source texture sprite sheet looks like, for reference.

enter image description here

Goose
  • 1,307
  • 2
  • 14
  • 28

1 Answers1

2

I am pretty sure this is happening because the transform applied to the sprite batch (CameraManager.pInstance.pFinalTransform) is using floating point percision.

Yep, this sounds about right. We had a similar issue when we wrote the Tiled map renderer in MonoGame.Extended after the map tiles get scaled by the camera.

The solution is to render the unscaled map to a RenderTarget2D first, then scale the whole thing in a second pass. This way you're not scaling each individual tile, but the map as a whole.

You can see how this works with MonoGame.Extended in the TiledMap class.

In your case, the code might look something like this:

private readonly RenderTarget2D _renderTarget;

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.SetRenderTarget(_renderTarget); 

    GraphicsDevice.Clear(Color.Black);

    SpriteBatch.Begin(sortMode: SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);

    foreach( TmxLayer Layer in mCurrentTileMap.Layers)
    {
        for (int y = 0; y < mCurrentTileMap.Height; y++)
        {
            for (int x = 0; x < mCurrentTileMap.Width; x++)
            {
                TmxLayerTile Tile = Layer.Tiles[mCurrentTileMap.Width * y + x];
                int TileID = -1;
                Texture2D Texture = FindTileMapTextureForTileID(Tile.Gid, out TileID);
                if (TileID > 0)
                {
                    int atlasX = TileID % (Texture.Width / TileSize);
                    int atlasY = TileID / (Texture.Width / TileSize);

                    SpriteEffects FX = SpriteEffects.None;
                    if (Tile.HorizontalFlip)
                    {
                        FX = SpriteEffects.FlipHorizontally;
                    }

                    SpriteBatch.Draw(Texture, new Vector2(x * TileSize, y * TileSize), new Microsoft.Xna.Framework.Rectangle(atlasX * TileSize, atlasY * TileSize, TileSize, TileSize), Color.White, 0, Vector2.Zero, 1, FX, 0);
                }
            }
        }
    }

    SpriteBatch.End();

    GraphicsDevice.SetRenderTarget(null);

    SpriteBatch.Begin(sortMode: SpriteSortMode.Immediate, blendState: BlendState.NonPremultiplied,
        samplerState: SamplerState.PointClamp, transformMatrix: CameraManager.pInstance.pFinalTransform);
    SpriteBatch.Draw(_renderTarget, Vector2.Zero, Color.White);
    SpriteBatch.End();
}

Note that I have moved the camera transform to the second SpriteBatch.Begin call and introduced a new _renderTarget member variable. Don't forget to declare the the _renderTarget somewhere before calling Draw. Something like this:

_renderTarget = new RenderTarget2D(graphicsDevice, width*tileWidth, height*tileHeight);

It needs to be big enough to render your map. Ideally, it would be optimized to only render the visible portion of the map, but that's another story.

craftworkgames
  • 9,437
  • 4
  • 41
  • 52
  • Thanks for the detailed response. I have given you suggestion a shot, and it does fix the issue but in the process I have lost "sub pixel movement". Before, a game pixel is ultimately rendered over 4 or more screen pixels, so characters (and cameras) could move (and be rendered at) distances less than a pixel wide. Now, because everything it being rendered in "real" pixel space, if a character moves (for example) 0.25 pixels right, they do not actually move on screen. The end result is a very jittery movement. – Goose Jan 11 '16 at 06:53
  • Interesting... Obviously I don't know the details so I'm not sure how to help you there. I still think this the the right solution to this problem though. Perhaps you can post a new question about the sub pixel movement. – craftworkgames Jan 11 '16 at 06:59
  • Yah, it's kind of confusing to explain. It's likely present in MonoGame.Extended based on what I saw in there. Try scaling your final camera way up, say 10x (so everything is super zoomed in and blocky). Where a sprite moves one pixel in world space, it will now move 10 pixels in screen space (10x the original), which looks broken. Anyway, it isn't really something to *solve*, it's just a limitation of the rendertarget approach (which again, thanks for the detailed answer!!). – Goose Jan 12 '16 at 06:47
  • I don't think it happens in MonoGame.Extended, but i could be wrong. If I'm understanding correctly, you're rendering everything to the render target? Have you tried just rendering the map tiles and render the sprites as a second render pass? Also, try turning off point clamp and see if that helps? – craftworkgames Jan 12 '16 at 07:21