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

6.8 KiB

title date weight
Drawing the Score 2019-06-04T17:21:52-07:00 40

Remember Renderers? Haven't thought about those in a while! All we need to draw new elements to the screen are Renderers.

But first, we're gonna need a font. I liked this font. But you can pick any font you like. It's your world and you can do whatever you like in it.

Place the font of your heart's desire into the directory PongFE/Content/Fonts.

Now we need to get our font in the game. There are a few different font rendering tools available. SpriteFontPlus has worked pretty well for me so far.

In your terminal, do the following commands:

git submodule add https://github.com/rds1983/SpriteFontPlus.git
git submodule update --init --recursive

In PongFE.Framework.csproj:

  <ItemGroup>
    <ProjectReference Include="..\FNA\FNA.csproj"/>
    <ProjectReference Include="..\encompass-cs\encompass-cs\encompass-cs.csproj"/>
    <ProjectReference Include="..\SpriteFontPlus\src\SpriteFontPlus.FNA.csproj" />
  </ItemGroup>

And in PongFE.Core.csproj:

  <ItemGroup>
    <ProjectReference Include="..\FNA\FNA.Core.csproj"/>
    <ProjectReference Include="..\encompass-cs\encompass-cs\encompass-cs.csproj" />
    <ProjectReference Include="..\SpriteFontPlus\src\SpriteFontPlus.FNA.Core.csproj" />
  </ItemGroup>

Now we can use SpriteFontPlus.

First, we need to be able to track which player's score is which. Why don't we add our new PlayerComponent to GoalBoundarySpawner?

In GoalBoundarySpawnMessage.cs:

using Encompass;
using MoonTools.Structs;
using PongFE.Enums;

namespace PongFE.Messages
{
    public struct GoalBoundarySpawnMessage
    {
        public PlayerIndex PlayerIndex { get; }
        public Position2D Position { get; }
        public int Width { get; }
        public int Height { get; }

        public GoalBoundarySpawnMessage(PlayerIndex playerIndex, Position2D position, int width, int height)
        {
            PlayerIndex = playerIndex;
            Position = position;
            Width = width;
            Height = height;
        }
    }
}

GoalBoundarySpawner.cs:

using Encompass;
using PongFE.Components;
using PongFE.Messages;

namespace PongFE.Spawners
{
    public class GoalBoundarySpawner : Spawner<GoalBoundarySpawnMessage>
    {
        protected override void Spawn(in GoalBoundarySpawnMessage message)
        {
            var entity = CreateEntity();

            AddComponent(entity, new PositionComponent(message.Position));
            AddComponent(entity, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, message.Width, message.Height)));
            AddComponent(entity, new CanDestroyComponent());
            AddComponent(entity, new ScoreComponent(0));
            AddComponent(entity, new PlayerComponent(message.PlayerIndex));
        }
    }
}

Now we can adjust the spawn messages in PongFEGame.cs:

    // right boundary
    WorldBuilder.SendMessage(
        new GoalBoundarySpawnMessage(
            Enums.PlayerIndex.One,
            new MoonTools.Structs.Position2D(1280, 0),
            6,
            720
        )
    );

    // left boundary
    WorldBuilder.SendMessage(
        new GoalBoundarySpawnMessage(
            Enums.PlayerIndex.Two,
            new MoonTools.Structs.Position2D(-6, 0),
            6,
            720
        )
    );

Scoring on the right goal increases the score of player one, and scoring on the left goal increases the score of player two.

Now let's write our ScoreRenderer.

In PongFE/Renderers/ScoreRenderer.cs:

using Encompass;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using PongFE.Components;
using PongFE.Enums;
using SpriteFontPlus;

namespace PongFE.Renderers
{
    public class ScoreRenderer : Renderer
    {
        public SpriteBatch SpriteBatch { get; }
        public DynamicSpriteFont Font { get; }
        public int SpacingFromCenter { get; } = 240;
        public int SpacingFromTop { get; } = 20;

        public ScoreRenderer(SpriteBatch spriteBatch, DynamicSpriteFont font)
        {
            SpriteBatch = spriteBatch;
            Font = font;
        }

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

            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(640 - SpacingFromCenter, SpacingFromTop),
                    Color.White
                );
            }

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

What's the question mark on those variables? This creates a Nullable value. That means the variable can either contain an int, or be null. We don't necessarily have a guarantee that the score values will exist in the world, so we provide a way for the renderer to fail gracefully if the search for the score value fails.

Basically, we find each entity with a score component and figure out which player the score belongs to. Then we draw the score component's value to the screen as a string.

Let's add our ScoreRenderer to the WorldBuilder. First we need to set up the font.

    ...

    DynamicSpriteFont ScoreFont { get; set; }

    ...

    protected override void LoadContent()
    {
        ...

        ScoreFont = DynamicSpriteFont.FromTtf(File.ReadAllBytes(@"Content/Fonts/SquaredDisplay.ttf"), 128);

        ...

        WorldBuilder.AddRenderer(new ScoreRenderer(SpriteBatch, ScoreFont));

        ...

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?