Fitting pixel art game to screen

So I have a pixel art based game that is designed for a 320x180 resolution. and is scaled up to the largest multiple that would fit within the viewport.

So a viewport of 925x522 will have a “gameport” of 640x360. The next multiple would be 940x540 and that’s too big so we stop at 640x360 and use black bars to center the gameport in the viewport.

This is what that looks like

This strategy works fine when the viewport is pretty close to the gameport. But as you can see for viewports of this size it doesn’t quite work well enough for my taste. Ideally I’d like the gameport to not be so much smaller than the viewport.

This is what I have for drawing the 640x360 gameport to the main buffer/viewport.

    void DrawToMainBuffer()
    {
        Graphics.GraphicsDevice.SetRenderTarget(null);
        Graphics.GraphicsDevice.Clear(Color.Black);

        spriteBatch.Begin(
            sortMode: SpriteSortMode.Immediate,
            samplerState: SamplerState.PointClamp,
            transformMatrix: Camera.ViewportTransformation,
            blendState: BlendState.AlphaBlend
        );

        spriteBatch.Draw(
           texture: gameportRenderTarget,
           destinationRectangle: new Rectangle(
               Gameport.X,
               Gameport.Y,
               Gameport.Width,
               Gameport.Height
           ),
           color: Color.White
       );

        spriteBatch.End();
     }

I’ve seen other games, like Celeste, that will scale the game to the actual viewport. So I know there must be a way to do this, I just don’t know how.

One thing I’m trying is scaling the gameport up one additional multiple and then in the spritebatch draw call I’m scaling up the rendertarget/texture to fit the viewport. My results are not what I want though. With the LinearClamp sampler state the result is a little blurry. With the PointClamp sampler state it distorts the pixels a bit.

Blurry pixel art?

This is how I’m doing that

    void DrawToMainBuffer()
    {
        Graphics.GraphicsDevice.SetRenderTarget(null);
        Graphics.GraphicsDevice.Clear(Color.Black);

        spriteBatch.Begin(
            sortMode: SpriteSortMode.Immediate,
            samplerState: SamplerState.LinearClamp,
            transformMatrix: Camera.ViewportTransformation,
            blendState: BlendState.AlphaBlend
        );

        float factor = (float)ViewportDimensions.X / (float)Gameport.Width;

        // Draw gameport to screen
        spriteBatch.Draw(
            texture: gameportRenderTarget,
            position: new Vector2(
                (ViewportDimensions.X - (Gameport.Width * factor)) / 2f, 
                (ViewportDimensions.Y - (Gameport.Height * factor)) / 2f
            ),
            sourceRectangle: null,
            color: Color.White,
            rotation: 0f,
            origin: new Vector2(0, 0),
            scale: new Vector2(factor, factor),
            effects: SpriteEffects.None,
            layerDepth: 0f
        );

        spriteBatch.End();
    }

I feel like I’m pretty close, but at the same time I’m moving outside of my knowledge with this stuff so I really have no idea what to do. Does anyone have any tips for me?

Thanks!

I would recommend using SamplerState.PointClamp for pixel art in your spriteBatch.Begin instead of LinearClamp, as this is most likely causing the blurry pixel art.

So for drawing to the render target that later gets drawn to the main buffer I am already using PixelClamp. The final step where I’m drawing the rendertarget to the main buffer I’m trying make it fit to the entire viewport and I’m using the LinerClamp sampler state, which doesn’t work because it’s blurring the art too much. When I use the PixelClamp sampler state on this final step it distorts the art instead, as seen in the gif below.

This is what rendering the stretched render target to the main buffer with the PixelClamp sampler state looks like
1061

In the final step I’m stretching the rendertarget, which has dimensions which are a multiple of the native resolution of 320x180, to the viewport. I’m not sure if this is how I’m supposed to accomplish this, but I’ve seen it done in similar projects.

What’s happening is for example I have a 1200x700 viewport, the user can resize the viewport to whatever they want. I have a native resolution of 320x180 so I take a resolution of one multiple bigger than the viewport can fit (1280x720) and I render to that. In the final step I stretch/shrink the rendertarget to the viewport and draw it like that to the main buffer. The issue is that with either the LinerClamp sampler state (blurry) or the PixelClamp sampler state (distorted) I’m not getting the results I want.

I am able to draw the render target to the viewport without stretching and it’ll work just fine, but in that case it won’t always take up the whole viewport. What I’m trying to figure out is how do I make sure it takes up the entire viewport? Perhaps stretching the final render target isn’t the answer?

Yea, those dropped pixels with point clamp (and blurriness with linear clamp) is definitely a thing that’s going to happen if you do a non-integer scale. Your source image is 320x180 but you’re mapping to a 1200x700 viewport. This gives you a ratio of 3.75 x 3.88.

The best option is to stick to an integer multiple, but from what I can gather in your OP, that’s what you were doing already, using black bars to fill in the rest. If you don’t want to do that, you’re going to have to use a better image scaling filter. There are definitely better ones you can use… I can’t remember their names, but if load up any NES/SNES emulator, I think you can use whatever it says in the video options as a guide.

I thiiiiiiink you can implement them in a shader when you draw your final texture to the viewport, scaling as you currently are now. Keep in mind that you’re still going to get blur and/or dropped pixels, depending on what you use, but i tshould hopefully be a lot better.

Having said all that, I would really suggest that you don’t allow your users to scale to an arbitrary resolution by resizing the window. Allow them to pick from a set of resolutions. Then allow windowed/fullscreen on only those.

A quick google finds this (I haven’t checked it, but this is the general idea)…

This is a Windows implementation though, if you wanted to support other platforms, you’ll need to abstract the resolution discovery, then implement on a per-platform basis. I’m not sure if there’s a built-in MonoGame/Xamarin way to do it.

1 Like

thanks for your reply, it has pointed me in the right direction a little bit

so after some fiddling i’ve settled on for now:

  1. drawing to a texture, a texture that is the multiple of my native resolution that is closest to the viewport
  2. and then stretching that texture further to the viewport using the LinearClamp sampler state when drawing to the main buffer.

it’s not perfect, but it’s work fine. i think i’d like to stick to letting the user resize the window to whatever they want because i find it pretty annoying when other games don’t let me do that. i’ve also found a lot of other modern pixel art games will let you resize to whatever you want (shovel knight, celeste, rivals of aether to name a few).

i did do some research and “bicubic scaling” sounds like it’s what i’m looking for, but i have absolutely no idea how to do that.

my first instinct was to do something like:

spriteBatch.Begin(
    sortMode: SpriteSortMode.Immediate,
    samplerState: SamplerState.BicubicClamp,
    transformMatrix: Camera.ViewportTransformation,
    blendState: BlendState.AlphaBlend
);

// Draw stuff

but BicubicClamp doesn’t exist on SamplerState. would i have to implement my own SamplerState instance? or am i right in believing this has anything to do with the samplerState argument?

Yea, you have to implement it yourself. I suspect in a pixel shader, but tbh I’m not actually sure how.

You could give users the option in your settings to either use integer scaling, and thus have letterboxing and/or pillarboxing, or use linear scaling to fill the whole screen but have some blurriness. Different users will probably have different preferences regarding that. In my case, I would prefer pixel-perfect integer scaling even if that meant letterboxing and/or pillarboxing.

good idea! i actually just got done adding an option for that before reading your message haha

1 Like

With non-integer scaling you will always sacrifice “pixel-perfectness” in some way.
Instead of stretching the texture to fill out the black bars, you could consider just drawing a bigger view of the world → Fill out the black bars by rendering extra tiles.

While that is a possibility, it does have its downsides.

  • It’s no longer possible to ensure a consistent experience; some players will have a wider “field of view” than others, and that could affect the balance of the gameplay (for instance, giving more time to react to enemies). If the game has any element of competition, like high scores, the playing field is no longer equal.
  • Code that depends on the viewport will now be inconsistent. For instance, if your game is designed to reset enemies once they go offscreen, players with a narrower field of view will have different gameplay (enemies resetting sooner) than players with a wider field of view.
  • Visual elements that were previously hidden, like the tiles abruptly ending in places that used to be guaranteed to be off-screen, might now be visible.

This could all mostly be accounted for, particularly for a single-player game with no competitive elements, but it would be considerably more work than just keeping the viewport or “field of view” consistent and scaling instead. It’s up to the individual to decide whether or not it’s worth it, of course.

I agree with all your points. I would only consider the first one as really problematic though.

I will get frustrated if your game has me die a lot because of the view distance being too short, but you’re right, there will always be at least some advatage to having a wider view, and that’s what makes this solution a bit problematic for a platformer game.

I would just solve this by having the game always run in “wide mode”, and then limit the view if neccessary.

A little bit more work, cause your levels need to be a few tiles wider, but probably not a big deal.