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

261 lines
7.1 KiB
Markdown

---
title: "Title"
date: 2019-06-09T16:51:13-07:00
weight: 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**:
```cs
public enum GameState
{
Title,
Game
}
```
In **PongFE/Components/GameStateComponent.cs**:
```cs
using Encompass;
using PongFE.Enums;
namespace PongFE.Components
{
public struct GameStateComponent : IComponent
{
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**:
```cs
var gameStateEntity = WorldBuilder.CreateEntity();
WorldBuilder.SetComponent(gameStateEntity, new GameStateComponent(GameState.Title));
```
Let's make a new Engine.
In **PongFE/Engines/GameStateEngine.cs**:
```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 GeneralRenderer.
In **PongFE/Renderers/TitleRenderer.cs**:
```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 : GeneralRenderer
{
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()
{
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.
```cs
public override void Render()
{
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**.
```cs
...
DynamicSpriteFont InstructionFont { get; set; }
...
InstructionFont = DynamicSpriteFont.FromTtf(
File.ReadAllBytes(@"Content/Fonts/SquaredDisplay.ttf"),
48
);
...
WorldBuilder.AddEngine(new GameStateEngine());
...
WorldBuilder.AddGeneralRenderer(new TitleRenderer(SpriteBatch, ScoreFont, InstructionFont), 0);
...
```
Let's try it!
![pong title](/images/pong_title.png)
Nice!