encompass-cs-docs/content/pong/scoring/display_scaling.md

5.4 KiB

title date weight
Display Scaling 2020-07-17T13:04:22-07:00 41

In our PongFEGame constructor, we have the following lines:

    graphics.PreferredBackBufferWidth = 1280;
    graphics.PreferredBackBufferHeight = 720;

This means that FNA will try to create a window with a 1280x720 size. Right now we have been building all our game logic around the assumption that the game's play area and UI size will be the same as the display area. It turns out, this isn't a very good assumption to make. Sometimes users will run at lower or higher resolutions. The player's display might not even have the same aspect ratio of 16x9!

There are many different ways you can handle this situation. What I typically do for 2D games is define a fixed-size play area and scale to the display size, employing letterboxing if the ratios do not match.

I have written a convenient script for handling the scaling transformation, which you can find here: https://gitea.moonside.games/MoonsideGames/MoonTools.ResolutionScaler

I recommend placing this script in the Utility folder.

Let's create a component to store our play area data.

using Encompass;

namespace PongFE.Components
{
    public struct PlayAreaComponent
    {
        public int Width { get; }
        public int Height { get; }

        public PlayAreaComponent(int width, int height)
        {
            Width = width;
            Height = height;
        }
    }
}

Now we are going to want to draw the game to a render target so it can be scaled properly to the final display.

In PongFEGame.cs:

    ...

    RenderTarget2D GameRenderTarget { get; set; }

    ...

    const int PLAY_AREA_WIDTH = 1280;
    const int PLAY_AREA_HEIGHT = 720;

    ...

    protected override void LoadContent()
    {
        ResolutionScaler.Init(
            GraphicsDevice.PresentationParameters.BackBufferWidth,
            GraphicsDevice.PresentationParameters.BackBufferHeight,
            PLAY_AREA_WIDTH,
            PLAY_AREA_HEIGHT
        );

        GameRenderTarget = new RenderTarget2D(GraphicsDevice, PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT);

        ...

        var playAreaEntity = WorldBuilder.CreateEntity();
        WorldBuilder.SetComponent(playAreaEntity, new PlayAreaComponent(PLAY_AREA_WIDTH, PLAY_AREA_HEIGHT));

        ...
    }

Go through the rest of PongFEGame.cs and replace the magic values 1280 and 720 with the PLAY_AREA constants.

Let's revise our ScoreRenderer too.

        public override void Render(double dt, double alpha)
    {
        int? playerOneScore = null;
        int? playerTwoScore = null;

        ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();

        foreach (ref readonly var entity in ReadEntities<ScoreComponent>())
        {
            ref readonly var scoreComponent = ref GetComponent<ScoreComponent>(entity);
            ref readonly var playerComponent = ref GetComponent<PlayerComponent>(entity);

            if (playerComponent.PlayerIndex == Enums.PlayerIndex.One)
            {
                playerOneScore = scoreComponent.Score;
            }
            else if (playerComponent.PlayerIndex == Enums.PlayerIndex.Two)
            {
                playerTwoScore = scoreComponent.Score;
            }
        }

        if (playerOneScore.HasValue)
        {
            SpriteBatch.DrawString(
                Font,
                playerOneScore.Value.ToString(),
                new Vector2(playAreaComponent.Width / 2 - SpacingFromCenter, SpacingFromTop),
                Color.White
            );
        }

        if (playerTwoScore.HasValue)
        {
            SpriteBatch.DrawString(
                Font,
                playerTwoScore.Value.ToString(),
                new Vector2(playAreaComponent.Width / 2 + SpacingFromCenter - (Font.Size / 2), SpacingFromTop),
                Color.White
            );
        }
    }

We have introduced a new method above, ReadComponent. ReadComponent allows us to read a single arbitrary component of a given type. It is useful when we have a single component of a certain type in the world and we want to just grab data from it.

Now, in our draw method, we will have the Encompass World draw to our GameRenderTarget, and then we will draw that render target using the transform matrix we get from ResolutionScaler.

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

        SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
        World.Draw();
        SpriteBatch.End();

        GraphicsDevice.SetRenderTarget(null);
        GraphicsDevice.Clear(Color.Black);

        SpriteBatch.Begin(
            SpriteSortMode.Deferred,
            null,
            null,
            null,
            null,
            null,
            ResolutionScaler.TransformMatrix
        );

        SpriteBatch.Draw(
            GameRenderTarget,
            Vector2.Zero,
            Color.White
        );

        SpriteBatch.End();

        base.Draw(gameTime);
    }

Now if you go to the PongFEGame constructor method, and change the preferred backbuffer sizes to different values, you should see that the game scales appropriately.

It is not uncommon for games to also feature a separate render target for scaling UI, and the UI might be a different size from the play area. It's up to you how you want to design things.