How do I make full screen stretch to the entire screen and have black bars on the sides if the screen aspect ratio isn't 16:9

I don’t know how to word the title better, also I’ve searched online for this quite some times and I was quite surprised no one has asked this before. Currently my only full screen related code is
_graphics.IsFullScreen = !_graphics.IsFullScreen; _graphics.ApplyChanges();
How do I do what I asked for in the title? or how am I supposed to learn how to?

You’ll need to use a RenderTarget2D. The back buffer width and height should be set to the monitor resolution, and the RenderTarget2D should be sized to the game resolution. You then draw the game to the render target and then draw the render target centered in the screen.

I’ll come up with a code example in a bit.

Edit: Okay, I came up with a code example:

public class Game1 : Game
{
    Point gameResolution = new Point(480, 800); // This can be whatever resolution your game renders at

    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    RenderTarget2D renderTarget;
    Rectangle renderTargetDestination;

    Color letterboxingColor = new Color(0, 0, 0);

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
        IsMouseVisible = true;
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);

        graphics.PreferredBackBufferWidth = gameResolution.X;
        graphics.PreferredBackBufferHeight = gameResolution.Y;

        graphics.ApplyChanges();

        renderTarget = new RenderTarget2D(GraphicsDevice, gameResolution.X, gameResolution.Y);
        renderTargetDestination = GetRenderTargetDestination(gameResolution, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
    }

    protected override void Update(GameTime gameTime)
    {
        // Game update code

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.SetRenderTarget(renderTarget);
        GraphicsDevice.Clear(letterboxingColor);

        // Game draw code

        GraphicsDevice.SetRenderTarget(null);
        GraphicsDevice.Clear(letterboxingColor);

        spriteBatch.Begin();
        spriteBatch.Draw(renderTarget, renderTargetDestination, Color.White);
        spriteBatch.End();

        base.Draw(gameTime);
    }

    void ToggleFullScreen()
    {
        if (!graphics.IsFullScreen)
        {
            graphics.PreferredBackBufferWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
            graphics.PreferredBackBufferHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
        }
        else
        {
            graphics.PreferredBackBufferWidth = gameResolution.X;
            graphics.PreferredBackBufferHeight = gameResolution.Y;
        }
        graphics.IsFullScreen = !graphics.IsFullScreen;
        graphics.ApplyChanges();

        renderTargetDestination = GetRenderTargetDestination(gameResolution, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
    }

    Rectangle GetRenderTargetDestination(Point resolution, int preferredBackBufferWidth, int preferredBackBufferHeight)
    {
        float resolutionRatio = (float)resolution.X / resolution.Y;
        float screenRatio;
        Point bounds = new Point(preferredBackBufferWidth, preferredBackBufferHeight);
        screenRatio = (float)bounds.X / bounds.Y;
        float scale;
        Rectangle rectangle = new Rectangle();

        if (resolutionRatio < screenRatio)
            scale = (float)bounds.Y / resolution.Y;
        else if (resolutionRatio > screenRatio)
            scale = (float)bounds.X / resolution.X;
        else
        {
            // Resolution and window/screen share aspect ratio
            rectangle.Size = bounds;
            return rectangle;
        }
        rectangle.Width = (int)(resolution.X * scale);
        rectangle.Height = (int)(resolution.Y * scale);
        return CenterRectangle(new Rectangle(Point.Zero, bounds), rectangle);
    }

    static Rectangle CenterRectangle(Rectangle outerRectangle, Rectangle innerRectangle)
    {
        Point delta = outerRectangle.Center - innerRectangle.Center;
        innerRectangle.Offset(delta);
        return innerRectangle;
    }
}

This code starts off windowed but can be set to full screen by calling ToggleFullScreen.

2 Likes

Thanks a lot!

1 Like

No problem!

1 Like

I’ve got some questions:

  1. How do I detect cursor position? I know it’s supposed to be Mouse.GetState().X/Y but now that’s quite off
  2. Everything is slightly blurry, well probably due to scaling
  3. How do you come up with this?

And again, thanks a lot for helping me, I would have never figured this out myself

You’ll need to scale the location of the mouse with the same scaling you use to draw the render target. Luckily, I have a MouseStateWrapper class that will do this for you:

public class MouseStateWrapper
{
    MouseState mouseState;
    Vector2 scaledMousePosition;
    Rectangle renderTargetDestination;
    float screenScale;

    public bool Scaled { get; set; } = true;

    public Vector2 Position => Scaled ? scaledMousePosition : mouseState.Position.ToVector2();

    public MouseState MouseState => mouseState;

    public MouseStateWrapper(bool scaled) => Scaled = scaled;

    internal void SetMouseState(MouseState mouseState)
    {
        this.mouseState = mouseState;
        scaledMousePosition = mouseState.Position.ToVector2();
        scaledMousePosition.X -= renderTargetDestination.X;
        scaledMousePosition.Y -= renderTargetDestination.Y;
        scaledMousePosition /= screenScale;
    }

    internal void SetRenderTargetDestination(Rectangle renderTargetDestination) => this.renderTargetDestination = renderTargetDestination;

    internal void SetScreenScale(float scale) => screenScale = scale;

    internal void SetMouseLocation(Point location) => SetMouseState(new MouseState(location.X, location.Y, mouseState.ScrollWheelValue,
        mouseState.LeftButton, mouseState.MiddleButton, mouseState.RightButton, mouseState.XButton1, mouseState.XButton2));

    internal void SetMouseLocation(int x, int y) => SetMouseState(new MouseState(x, y, mouseState.ScrollWheelValue,
        mouseState.LeftButton, mouseState.MiddleButton, mouseState.RightButton, mouseState.XButton1, mouseState.XButton2));

    public Point ScalePositionUp(Vector2 position)
    {
        Vector2 unscaledMousePosition = position;
        unscaledMousePosition *= screenScale;
        unscaledMousePosition.X += renderTargetDestination.X;
        unscaledMousePosition.Y += renderTargetDestination.Y;
        return unscaledMousePosition.ToPoint();
    }
}

You can use this as a wrapper around MouseState that accounts for scaling. Whenever your scaling and render target destination changes (whenever you go into or out of full screen, for instance), call SetRenderTargetDestination and SetScreenScale. The value to pass to SetScreenScale is the same scale value from the GetRenderTargetDestination method I posted earlier.

Yeah, things will appear blurry unless you use integer scaling, meaning the resolution is scaled up only by integer values, like x2, x3, and not 2.178… There’s no way around that. You can set the sampler state of SpriteBatch to PointClamp and that will get rid of the blurriness, but instead you’ll have pixel distortion, where some pixels of your scaled image appear bigger than others. If you don’t want either blurriness or pixel distortion, you have to use either the native monitor resolution or use integer scaling.

Well, you’d have to know about what RenderTarget2D is and what it’s used for first. I’m pretty sure I learned about RenderTarget2D either on this forum years ago or via some tutorial. The rest was just figuring out how to use it to get what I wanted.

1 Like

Thanks a lot, again
Edit: The position property returns a 0,0 vector, can you think of anything I could’ve done wrong? I do call mouseStateWrapper.SetRenderTargetDestination(renderTargetDestination) and I do call SetScreenScale with the scale in GetRenderTargetDestination, or maybe you could say how it’s supposed to be used

Are you calling SetMouseState each frame? That method takes the MouseState you get from Mouse.GetState() and converts it into a scaled version. Without calling that method each update, the MouseStateWrapper class won’t work.

1 Like

If you don’t like default anti-aliasing use SamplerState.PointClamp. See below.

spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null);
1 Like

This does have the downside of causing image distortion for non-integer scaling values, as I mentioned above. Also, if you want to stick to the default values for Begin parameters, you can just do this:

spriteBatch.Begin(samplerState: SamplerState.PointClamp);

1 Like

This is cool “new” C# syntax that I’m completely unused to and always forget about :smiley:

1 Like

Oh thanks, everything’s working as intended now

Heh, you must have starting using C# much earlier than I did! Optional and named parameters were introduced with C# 4, which released alongside Visual Studio 2010.

Glad you got it working.

2 Likes

I did start using it prior to 2010, but I think it was just more that the various companies I worked for didn’t use it. Optional parameters yea, but named parameters I didn’t know existed until 3-4 years ago. If they came out in 2010, that’s quite the feat! :smiley:

Yep! At least according to the history on Microsoft Docs, which you can peruse here.

I’ve liked a lot of the new additions and changes to C# over the years. One that I’ve really wanted is being worked on and may be available when C# 11 is released: implicit backing variables for properties. So instead of having to explicitly declare a backing variable, like this:

int nonZeroValue;
public int NonZeroValue
{
    get => nonZeroValue;
    set
    {
        if (value == 0)
            throw new ArgumentException("The value cannot be zero.");
        nonZeroValue = value;
    }
}

You could use the implicit backing variable field, like this:

public int NonZeroValue
{
    get => field;
    set
    {
        if (value == 0)
            throw new ArgumentException("The value cannot be zero.");
        field = value;
    }
}

And that way, the class containing NonZeroValue wouldn’t have access to the backing variable outside of the property, and so it couldn’t accidentally be set to zero, as an example.

I’ve wanted this for years. You can read about it here on Github. Note: I wasn’t the OP of that issue. I posted a different issue a year or so later that I closed due to being a duplicate, and it was also in the wrong repository.

3 Likes

Oh cool. Yea I’ve used the auto implement get/set a lot over the years, but whenever you want to do something special on the set you have to then have a backing field. This alleviates that, nice!

1 Like