display scaling
continuous-integration/drone/push Build is passing Details

main
Evan Hemsley 2020-07-17 20:16:03 -07:00
parent 800ef53ac8
commit 7323f3bce7
2 changed files with 174 additions and 3 deletions

View File

@ -0,0 +1,167 @@
---
title: "Display Scaling"
date: 2020-07-17T13:04:22-07:00
weight: 41
---
In our **PongFEGame** constructor, we have the following lines:
```cs
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.
```cs
using Encompass;
namespace PongFE.Components
{
public struct PlayAreaComponent : IComponent
{
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**:
```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.
```cs
public override void Render()
{
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
);
}
}
```
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.
```cs
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.

View File

@ -141,6 +141,8 @@ namespace PongFE.Renderers
{ {
public SpriteBatch SpriteBatch { get; } public SpriteBatch SpriteBatch { get; }
public DynamicSpriteFont Font { get; } public DynamicSpriteFont Font { get; }
public int SpacingFromCenter { get; } = 240;
public int SpacingFromTop { get; } = 20;
public ScoreRenderer(SpriteBatch spriteBatch, DynamicSpriteFont font) public ScoreRenderer(SpriteBatch spriteBatch, DynamicSpriteFont font)
{ {
@ -173,7 +175,7 @@ namespace PongFE.Renderers
SpriteBatch.DrawString( SpriteBatch.DrawString(
Font, Font,
playerOneScore.Value.ToString(), playerOneScore.Value.ToString(),
new Vector2(400, 20), new Vector2(640 - SpacingFromCenter, SpacingFromTop),
Color.White Color.White
); );
} }
@ -183,7 +185,7 @@ namespace PongFE.Renderers
SpriteBatch.DrawString( SpriteBatch.DrawString(
Font, Font,
playerTwoScore.Value.ToString(), playerTwoScore.Value.ToString(),
new Vector2(880 - 64, 20), new Vector2(640 + SpacingFromCenter - (Font.Size / 2), SpacingFromTop),
Color.White Color.White
); );
} }
@ -205,7 +207,7 @@ Let's add our ScoreRenderer to the WorldBuilder. First we need to set up the fon
... ...
public override void LoadContent() protected override void LoadContent()
{ {
... ...
@ -219,3 +221,5 @@ Let's add our ScoreRenderer to the WorldBuilder. First we need to set up the fon
``` ```
Now you should be able to see the game score. This is starting to look and feel like a more complete game now. Now you should be able to see the game score. This is starting to look and feel like a more complete game now.
Hang on a sec though - what's that magic value *640* in our ScoreRenderer? So far we've been assuming throughout our code that our UI and our play area are locked to 1280x720. What if that isn't true?