DEV Community

Erick for Blue Fire

Posted on

How to avoid ghost lines and render artifacts in Flame

If you ever developed a 2d game, you might have had to deal with “strange rendering artifacts” when using sprites from a sprite sheet.

Consider the screenshot below:

If you have a good eye, you might already have noticed the artifacts that I am talking about, but since this screenshot was compressed and scaled down to be presented here in a web page, it might be hard to spot them both, so let me zoom in in the issues:

Note this “ghost line” between the tiles.

Note how a thin line appears on the left side of the knight

You make sure that your src position and src size is exact, you make sure that your tiles are correctly aligned with each other, yet, you can't get rid of these annoying lines.

And that is not really your fault! These artifacts can happen in a variety of different contexts, they are common to happen when any camera transformation is used, be it zoom or translation, it can happen if you scale up or down the sprite, or even when you simply have it positioned with long decimal values.

Still, no matter the context, the root cause is the same: computers have a limited precision on float numbers and their decimal values.

That imprecision can make sprites that are supposed to be perfectly aligned, as in if they are positioned edge to edge on each other, to be rendered with a very thin line between them, causing what we call ghost lines.

The issue we see in the knight is a bit different; first, take a look at the whole sprite sheet that we are using:

Let's zoom in a bit in our knight:

Note how we have the sprite of another knight on the left, and that sprite touches the border of the knight that we are rendering.

Even though we set the src position correctly to start on the exact pixel, when rendering the whole scene, when all the underlying transformations are applied, that imprecision will make the rendering of our sprite accidentally “grabbing a small section” of the rest of the sprite sheet, and a thin line from the sword of the other sprite will be rendered alongside our own sprite. This is what we call a “texture leak”.

Solutions

There are several ways to fix these problems, in this article we will suggest a couple of them which are the ways that in our experience works the best and are the ones we recommend.

We presented two different issues, so let's tackle them individually.

Ghost lines busting! Who you gonna call?

A reliable way of fixing those ghost lines that appear between aligned tiles is to use a technique known in the game development world as “pixel bleeding”.

The idea is that we render the sprites a little bit bigger than its original size, that way, the added size will cover that thin line, but it is so small that will not be noticeable by the human eye.

Starting on Flame v1.3.0 all sprites and sprite components can be rendered with this bleeding effect:

    const bleedValue = 0.04;

    final spriteImages = await images.load('little-adventures.png');
    world.add(
      SpriteComponent.fromImage(
        spriteImages,
        srcSize: Vector2.all(8),
        srcPosition: Vector2.zero(),
        position: Vector2(-(resX / 2), (resY / 2) - 8),
        size: Vector2.all(8),
        bleed: bleedValue,
      ),
    );
Enter fullscreen mode Exit fullscreen mode

Note the bleed attribute on the SpriteComponent.fromImage and how we are passing a value of 0.04 to it. That will make the sprite be rendered with 0.04 pixels on each border, covering the ghost line!

Note that the exact bleed value depends on multiple factors: The size of the sprite, the zoom level or the current viewport, bigger sprites might require larger values and so on, before settling in a bleed value, be sure to test the game in different resolutions and zoom levels, and depending on how dynamic the zoom of the game is, consider making the bleed value proportional to the applied zoom.

Texture leaking plumbing!

A simple technique to fix this problem is to “isolate” that sprite in the memory. This means that when rendering the sprite in the game we will not be rendering a portion of an image, but rather be rendering an image that has only the sprite we want to render.

Of course you could get rid of the sprite sheet and make every single texture of your game a single image, that could cause many other issues though like increased IO operations etc.

So, starting on Flame v1.30.0 we provide a new feature that allows you to still use sprite sheets, while being able to isolate sprite in memory to avoid the memory leaks: Raster Sprites.

Raster sounds like a fancy word, but a raster image, or raster sprite simply means “an image in the memory”. When using raster sprites, Flame will under the hood extract the sprite from your sprite sheet and make an image in memory that represents that single sprite, avoiding the possibility of textures leaking into each other.

To use Raster sprites in flame, simply use the RasterSpriteComponent instead of the SpriteComponent. Both components share the same API.

    world.add(
      RasterSpriteComponent.fromImage(
        spriteImages,
        srcSize: Vector2.all(8),
        srcPosition: Vector2(8, 88),
        position: Vector2(-(resX / 2) + 16, (resY / 2) - 16),
        size: Vector2.all(8),
      ),
    );
Enter fullscreen mode Exit fullscreen mode

Mix and match

Note that if you have multiple tiles in the same texture atlas, whereas their edges are side to side, you might need to combine both techniques and use the bleedValue attribute in a RasterComponent in order to avoid both ghost lines and texture leaks.

Flame tiled

If you use Flame Tiled for your tiled map you might still notice ghost lines as these features have not yet been implemented in that package by the time of the writing of this article, but stay tuned for future updates.

Wrapping up!

With these two techniques you should be able to get rid of any unexpected rending artifacts in your game!

Check out the official docs for the Pixel Bleeding and Sprite Rasterization and check this repository for the full example used for this article.

If by any chance you find yourself in an edge case where neither of the presented solutions work, don't hesitate in reaching out to the Flame team on our Discord!

Top comments (0)