[Solved] Help with Centering SpriteFont on Scaled RenderTarget with Aspect Ratio

My game has a virtual resolution of 640x480 and an actual resolution of 1280x720.
I’m drawing to a RenderTarget that is being scaled to fit the game window while maintaining its aspect ratio and uses letterboxing/pillarboxing where needed.

My problem is, while I’m using my virtual resolution for drawing, when I try to draw my text on the center of my virtual window, it gets drawn at a weird position:

Below is my code:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MGPlayground
{
    public class Game1 : Game
    {
        private const int VIRTUAL_WIDTH = 640;
        private const int VIRTUAL_HEIGHT = 480;
        private const int WINDOW_WIDTH = 1280;
        private const int WINDOW_HEIGHT = 720;

        private int renderXOffset;
        private int renderYOffset;
        private RenderSize renderSize;

        private GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;
        private RenderTarget2D renderTarget;

        private SpriteFont arialFont;

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

            // TODO: Once 3.8.1 comes out, use this and remove the version in `Initialize`.
            // graphics.PreferredBackBufferWidth = WINDOW_WIDTH;
            // graphics.PreferredBackBufferHeight = WINDOW_HEIGHT;
            // _graphics.SynchronizeWithVerticalRetrace = true; // Enable VSync.
        }

        protected override void Initialize()
        {
            // TODO: Once 3.8.1 comes out, use the version in `Game1()` and remove this.
            graphics.PreferredBackBufferWidth = WINDOW_WIDTH;
            graphics.PreferredBackBufferHeight = WINDOW_HEIGHT;
            graphics.SynchronizeWithVerticalRetrace = true; // Enable VSync.
            graphics.ApplyChanges();

            renderTarget = new RenderTarget2D(
                GraphicsDevice,
                GraphicsDevice.PresentationParameters.BackBufferWidth,
                GraphicsDevice.PresentationParameters.BackBufferHeight,
                false,
                GraphicsDevice.PresentationParameters.BackBufferFormat,
                DepthFormat.Depth24);

            renderSize = ScaleToFitAspectRatio(VIRTUAL_WIDTH, VIRTUAL_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT);
            renderXOffset = WINDOW_WIDTH / 2 - renderSize.Width() / 2;
            renderYOffset = WINDOW_HEIGHT / 2 - renderSize.Height() / 2;

            base.Initialize();
        }

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

            arialFont = Content.Load<SpriteFont>("ArialFont");
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            DrawToRenderTarget(renderTarget);
            DrawRenderTargetToScreen();

            base.Draw(gameTime);
        }

        private void DrawToRenderTarget(RenderTarget2D renderTarget)
        {
            GraphicsDevice.SetRenderTarget(renderTarget);
            GraphicsDevice.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true };
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Draw with Nearest Neighbor filtering:
            spriteBatch.Begin(blendState: BlendState.AlphaBlend, samplerState: SamplerState.PointClamp);

            spriteBatch.DrawString(arialFont, "Hello, World!", new Vector2(VIRTUAL_WIDTH / 2, VIRTUAL_HEIGHT / 2), Color.White);

            spriteBatch.End();
            GraphicsDevice.SetRenderTarget(null);
        }

        private void DrawRenderTargetToScreen()
        {
            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend,
                        SamplerState.LinearClamp, DepthStencilState.Default,
                        RasterizerState.CullNone);

            spriteBatch.Draw(renderTarget, new Rectangle(renderXOffset, renderYOffset, renderSize.Width(), renderSize.Height()), Color.White);

            spriteBatch.End();
        }

        private RenderSize ScaleToFitAspectRatio(int srcWidth, int srcHeight, int destWidth, int destHeight)
        {
            var ratio = Math.Min(Decimal.Divide(destWidth, srcWidth), Decimal.Divide(destHeight, srcHeight));
            decimal width = srcWidth * ratio;
            decimal height = srcHeight * ratio;
            return new RenderSize(Decimal.ToInt32(Math.Round(width, 0)), Decimal.ToInt32(Math.Round(height, 0)));
        }
    }

    public class RenderSize
    {
        private int _width;
        private int _height;

        public RenderSize(int width, int height)
        {
            _width = width;
            _height = height;
        }

        public int Width()
        {
            return _width;
        }

        public int Height()
        {
            return _height;
        }
    }
}

What puzzles me even more is that when I use my actual resolution instead of my virtual one, the text is displayed on the center of the screen:

Below is the edited code:

spriteBatch.DrawString(arialFont, "Hello, World!", new Vector2(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2), Color.White);

Shouldn’t using my virtual resolution be the one to actually center my text on the window?
What am I doing wrong?

I think its because sprite batch is using the native window coordinate system, so, the screen size, not your RT size. You probably need to offset it.

@Charles_Humphrey Would passing a transformMatrix to spriteBatch.Begin() help here?
Though I don’t quite know how to do so. :sweat_smile:

Yea, that could be one solution, not on my dev lappy today or I would try something out for you.

1 Like

No worries, I’m just trying out some things myself. :sweat_smile:
If I haven’t solved it by the time you get your lappy, please feel free to try some things out! :smile:

1 Like

Yea, either way, this might be another good sample to add to my public MG samples on git :slight_smile:

1 Like

I believe I’ve solved it!

Turns out I gave the RenderTarget the actual resolution instead of the virtual resolution in my Initialize method. :sweat_smile:

Original:

renderTarget = new RenderTarget2D(
    GraphicsDevice,
    GraphicsDevice.PresentationParameters.BackBufferWidth,
    GraphicsDevice.PresentationParameters.BackBufferHeight,
    false,
    GraphicsDevice.PresentationParameters.BackBufferFormat,
    DepthFormat.Depth24);

Fixed:

renderTarget = new RenderTarget2D(
    GraphicsDevice,
    VIRTUAL_WIDTH,
    VIRTUAL_HEIGHT,
    false,
    GraphicsDevice.PresentationParameters.BackBufferFormat,
    DepthFormat.Depth24);
1 Like

Lol!

Excellent, glad you sorted it, sorry I was not much help lol

1 Like

Eh, no worries.
What matters is that that’s one problem off my todo list, lol. :sweat_smile:

1 Like