encompass-cs-docs/content/pong/polish/title.md

7.1 KiB

title date weight
Title 2019-06-09T16:51:13-07:00 20

It would be nice to have a title screen instead of launching right into gameplay. Let's make that happen.

You might be tempted to create a separate World for the title screen or different game modes. While this is possible, I don't really recommend it, because it is difficult to share information between different Worlds by design.

We could introduce a concept of game state here.

In Enums.cs:

public enum GameState
{
    Title,
    Game
}

In PongFE/Components/GameStateComponent.cs:

using Encompass;
using PongFE.Enums;

namespace PongFE.Components
{
    public struct GameStateComponent
    {
        public GameState GameState { get; }

        public GameStateComponent(GameState gameState)
        {
            GameState = gameState;
        }
    }
}

This component will be something we call a singleton component. Just one of them exists in the world to conveniently track some kind of global state.

In PongFEGame.cs:

var gameStateEntity = WorldBuilder.CreateEntity();
WorldBuilder.SetComponent(gameStateEntity, new GameStateComponent(GameState.Title));

Let's make a new Engine.

In PongFE/Engines/GameStateEngine.cs:

using Encompass;
using Microsoft.Xna.Framework.Input;
using PongFE.Components;
using PongFE.Enums;
using PongFE.Messages;

namespace PongFE.Engines
{
    public class GameStateEngine : Engine
    {
        public override void Update(double dt)
        {
            ref readonly var gameStateEntity = ref ReadEntity<GameStateComponent>();
            ref readonly var gameStateComponent = ref GetComponent<GameStateComponent>(gameStateEntity);

            if (gameStateComponent.GameState == GameState.Title)
            {
                if (Keyboard.GetState().IsKeyDown(Keys.Enter))
                {
                    StartGame();
                    SetComponent(gameStateEntity, new GameStateComponent(GameState.Game));
                }
            }
        }
    }

    private void StartGame()
    {
        ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
        var playAreaWidth = playAreaComponent.Width;
        var playAreaHeight = playAreaComponent.Height;

        SendMessage(
            new PaddleSpawnMessage(
                new MoonTools.Structs.Position2D(20, playAreaHeight / 2 - 40),
                Enums.PlayerIndex.One,
                PaddleControl.Player,
                20,
                80
            )
        );

        SendMessage(
            new PaddleSpawnMessage(
                new MoonTools.Structs.Position2D(playAreaWidth - 45, playAreaHeight / 2 - 40),
                Enums.PlayerIndex.Two,
                PaddleControl.Computer,
                20,
                80
            )
        );

        SendMessage(
            new BallSpawnMessage(
                new MoonTools.Structs.Position2D(playAreaWidth / 2, playAreaHeight / 2),
                500,
                16,
                16
            ),
            0.5
        );

        // top boundary
        SendMessage(
            new BoundarySpawnMessage(
                new MoonTools.Structs.Position2D(0, -6),
                playAreaWidth,
                6
            )
        );

        // bottom boundary
        SendMessage(
            new BoundarySpawnMessage(
                new MoonTools.Structs.Position2D(0, playAreaHeight),
                playAreaWidth,
                6
            )
        );

        // right boundary
        SendMessage(
            new GoalBoundarySpawnMessage(
                Enums.PlayerIndex.One,
                new MoonTools.Structs.Position2D(playAreaWidth, 0),
                6,
                playAreaHeight
            )
        );

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

Notice how we took most of the StartGame stuff from the PongFEGame.LoadContent method. Make sure to take those message sends out of that method.

Now we need to draw the title screen. There's many different approaches. Generic UI text rendering elements would probably be a good idea. But I'm lazy, so let's just set up a very basic renderer.

In PongFE/Renderers/TitleRenderer.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 TitleRenderer : Renderer
    {
        private SpriteBatch SpriteBatch { get; }
        private DynamicSpriteFont TitleFont { get; }
        private DynamicSpriteFont InstructionFont { get; }

        public TitleRenderer(SpriteBatch spriteBatch, DynamicSpriteFont titleFont, DynamicSpriteFont instructionFont)
        {
            SpriteBatch = spriteBatch;
            TitleFont = titleFont;
            InstructionFont = instructionFont;
        }

        public override void Render(double dt, double alpha)
        {
            ref readonly var gameStateComponent = ref ReadComponent<GameStateComponent>();
            ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();

            if (gameStateComponent.GameState == GameState.Title)
            {
                var titleDimensions = TitleFont.MeasureString("PongFE");
                var titlePosition = new Vector2(
                    (playAreaComponent.Width - titleDimensions.X) / 2,
                    (playAreaComponent.Height - titleDimensions.Y) / 4
                );

                var instructionDimensions = InstructionFont.MeasureString("Press Enter to begin");
                var instructionPosition = new Vector2(
                    (playAreaComponent.Width - instructionDimensions.X) / 2,
                    playAreaComponent.Height * 2 / 3
                );

                SpriteBatch.DrawString(TitleFont, "PongFE", titlePosition, Color.White);
                SpriteBatch.DrawString(InstructionFont, "Press Enter to begin", instructionPosition, Color.White);
            }
        }
    }
}

And let's tweak the CenterLineRenderer.Render method a bit.

public override void Render(double dt, double alpha)
{
    ref readonly var gameStateComponent = ref ReadComponent<GameStateComponent>();

    if (gameStateComponent.GameState == GameState.Game)
    {
        ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();

        DrawDottedLine(playAreaComponent.Width / 2, 0, playAreaComponent.Width / 2, playAreaComponent.Height, 20, 20);
    }
}

Let's set everything up in PongFEGame.

    ...

    DynamicSpriteFont InstructionFont { get; set; }

    ...

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

    ...

    WorldBuilder.AddEngine(new GameStateEngine());

    ...

    WorldBuilder.AddRenderer(new TitleRenderer(SpriteBatch, ScoreFont, InstructionFont));

    ...

Let's try it!

pong title

Nice!