554 lines
16 KiB
Markdown
554 lines
16 KiB
Markdown
---
|
|
title: "Win Condition"
|
|
date: 2019-06-09T19:10:15-07:00
|
|
weight: 30
|
|
---
|
|
|
|
There's one critical element missing from our game. Right now, once the game starts, it keeps going until the player exits the program. We need a win condition, and to indicate when that win condition has been reached.
|
|
|
|
Our win condition is pretty simple: get x number of points. When one player wins the game, we want to display a message that says they won, and then go back to the title screen.
|
|
|
|
I've been avoiding it but I think it's time for a UI Text system. All we need to display text is a position, a font, and a string.
|
|
|
|
In **PongFE/Components/UITextComponent.cs**
|
|
|
|
```cs
|
|
using Encompass;
|
|
using SpriteFontPlus;
|
|
|
|
namespace PongFE.Components
|
|
{
|
|
public struct UITextComponent : IComponent
|
|
{
|
|
public DynamicSpriteFont Font { get; }
|
|
public string Text { get; }
|
|
|
|
public UITextComponent(DynamicSpriteFont font, string text)
|
|
{
|
|
Font = font;
|
|
Text = text;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
In **PongFE/Messages/UITextSpawnMessage.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using MoonTools.Structs;
|
|
using SpriteFontPlus;
|
|
|
|
namespace PongFE.Messages
|
|
{
|
|
public struct UITextSpawnMessage : IMessage
|
|
{
|
|
public Position2D Position { get; }
|
|
public string Text { get; }
|
|
public DynamicSpriteFont Font { get; }
|
|
|
|
public UITextSpawnMessage(Position2D position, DynamicSpriteFont font, string text)
|
|
{
|
|
Position = position;
|
|
Font = font;
|
|
Text = text;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
In **PongFE/Engines/Spawners/UITextSpawner.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using PongFE.Components;
|
|
using PongFE.Messages;
|
|
|
|
namespace PongFE.Spawners
|
|
{
|
|
public class UITextSpawner : Spawner<UITextSpawnMessage>
|
|
{
|
|
protected override void Spawn(in UITextSpawnMessage message)
|
|
{
|
|
var entity = CreateEntity();
|
|
|
|
AddComponent(entity, new PositionComponent(message.Position));
|
|
AddComponent(entity, new UITextComponent(message.Font, message.Text));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Now we need a way to render the text.
|
|
|
|
In **PongFE/Renderers/UITextRenderer.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using PongFE.Components;
|
|
using PongFE.Extensions;
|
|
using SpriteFontPlus;
|
|
|
|
namespace PongFE.Renderers
|
|
{
|
|
public class UITextRenderer : GeneralRenderer
|
|
{
|
|
private SpriteBatch SpriteBatch { get; }
|
|
|
|
public UITextRenderer(SpriteBatch spriteBatch)
|
|
{
|
|
SpriteBatch = spriteBatch;
|
|
}
|
|
|
|
public override void Render()
|
|
{
|
|
foreach (ref readonly var entity in ReadEntities<UITextComponent>())
|
|
{
|
|
ref readonly var uiTextComponent = ref GetComponent<UITextComponent>(entity);
|
|
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
|
|
|
|
SpriteBatch.DrawString(
|
|
uiTextComponent.Font,
|
|
uiTextComponent.Text,
|
|
positionComponent.Position.ToXNAVector(),
|
|
Color.White
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
|
|
With that out of the way, let's create a message for winning the game and a message for changing the game state.
|
|
|
|
In **PongFE/Messages/GameWinMessage.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using PongFE.Enums;
|
|
|
|
namespace PongFE.Messages
|
|
{
|
|
public struct GameWinMessage : IMessage
|
|
{
|
|
public PlayerIndex PlayerIndex { get; }
|
|
|
|
public GameWinMessage(PlayerIndex playerIndex)
|
|
{
|
|
PlayerIndex = playerIndex;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
In **PongFE/Messages/ChangeGameStateMessage.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using PongFE.Enums;
|
|
|
|
namespace PongFE.Messages
|
|
{
|
|
public struct ChangeGameStateMessage : IMessage
|
|
{
|
|
public GameState GameState { get; }
|
|
|
|
public ChangeGameStateMessage(GameState gameState)
|
|
{
|
|
GameState = gameState;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Now we can create an engine that handles winning the game.
|
|
|
|
In **PongFE/Engines/GameWinEngine.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using PongFE.Components;
|
|
using PongFE.Enums;
|
|
using PongFE.Messages;
|
|
using SpriteFontPlus;
|
|
|
|
namespace PongFE.Engines
|
|
{
|
|
[Reads(typeof(PlayAreaComponent))]
|
|
[Receives(typeof(GameWinMessage))]
|
|
[Sends(typeof(UITextSpawnMessage), typeof(ChangeGameStateMessage))]
|
|
public class GameWinEngine : Engine
|
|
{
|
|
public DynamicSpriteFont Font { get; }
|
|
private readonly string _playerOneWinText = "Player 1 Wins!";
|
|
private readonly string _playerTwoWinText = "Player 2 Wins!";
|
|
|
|
public GameWinEngine(DynamicSpriteFont font)
|
|
{
|
|
Font = font;
|
|
}
|
|
|
|
public override void Update(double dt)
|
|
{
|
|
if (SomeMessage<GameWinMessage>())
|
|
{
|
|
ref readonly var gameWinMessage = ref ReadMessage<GameWinMessage>();
|
|
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
|
|
|
|
string winText;
|
|
if (gameWinMessage.PlayerIndex == PlayerIndex.One)
|
|
{
|
|
winText = _playerOneWinText;
|
|
}
|
|
else
|
|
{
|
|
winText = _playerTwoWinText;
|
|
}
|
|
|
|
var textDimensions = Font.MeasureString(winText);
|
|
|
|
SendMessage(new UITextSpawnMessage(
|
|
new MoonTools.Structs.Position2D(
|
|
(playAreaComponent.Width - textDimensions.X) / 2,
|
|
playAreaComponent.Height / 4
|
|
),
|
|
Font,
|
|
winText
|
|
));
|
|
|
|
SendMessage(new ChangeGameStateMessage(GameState.Title), 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Before we move on, I would like to address one lingering concern. Let's try a little exercise. How can we express our game rule about spawning balls in a human sentence?
|
|
|
|
"We respawn a ball *x* amount of seconds after a ball is destroyed."
|
|
|
|
This was fine enough when we didn't have an actual scoring loop. Now we have a problem though - we don't want the ball to be served after the game is won.
|
|
|
|
What if instead we could think of the rule as being "A ball is served x seconds after a point is scored, unless that point is the winning point of the game." This implies that spawning the ball is the responsibility of the **ScoreEngine** and not the **DestroyEngine**.
|
|
|
|
I think it would be nice to have some component that stores our ball parameters, so we can retrieve them easily.
|
|
|
|
In **PongFE/Components/BallParametersComponent.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
|
|
namespace PongFE.Components
|
|
{
|
|
public struct BallParametersComponent : IComponent
|
|
{
|
|
public int Speed { get; }
|
|
public double Delay { get; }
|
|
|
|
public BallParametersComponent(int speed, double delay)
|
|
{
|
|
Speed = speed;
|
|
Delay = delay;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Now let's modify our **ScoreEngine**.
|
|
|
|
```cs
|
|
using Encompass;
|
|
using PongFE.Components;
|
|
using PongFE.Messages;
|
|
|
|
namespace PongFE.Engines
|
|
{
|
|
[Reads(
|
|
typeof(ScoreComponent),
|
|
typeof(PlayerComponent),
|
|
typeof(BallParametersComponent)
|
|
)]
|
|
[Receives(typeof(ScoreMessage))]
|
|
[Sends(typeof(GameWinMessage), typeof(BallSpawnMessage))]
|
|
[Writes(typeof(ScoreComponent))]
|
|
public class ScoreEngine : Engine
|
|
{
|
|
public override void Update(double dt)
|
|
{
|
|
foreach (ref readonly var scoreMessage in ReadMessages<ScoreMessage>())
|
|
{
|
|
if (HasComponent<ScoreComponent>(scoreMessage.Entity))
|
|
{
|
|
ref readonly var scoreComponent = ref GetComponent<ScoreComponent>(scoreMessage.Entity);
|
|
SetComponent(scoreMessage.Entity, new ScoreComponent(scoreComponent.Score + 1));
|
|
|
|
if (scoreComponent.Score + 1 >= 5)
|
|
{
|
|
ref readonly var playerComponent = ref GetComponent<PlayerComponent>(scoreMessage.Entity);
|
|
SendMessage(new GameWinMessage(playerComponent.PlayerIndex));
|
|
}
|
|
else
|
|
{
|
|
ref readonly var ballParametersComponent = ref ReadComponent<BallParametersComponent>();
|
|
|
|
SendMessage(
|
|
new BallSpawnMessage(
|
|
new MoonTools.Structs.Position2D(640, (int)MathHelper.RandomFloat(20, 700)),
|
|
ballParametersComponent.Speed,
|
|
16,
|
|
16
|
|
),
|
|
ballParametersComponent.Delay
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Notice how easy it was to move the responsibility of spawning the ball without having to worry about complex dependencies. This is a major benefit of loose coupling - we can make fairly fundamental logic changes to the game without having to disentangle functionality across the entire project.
|
|
|
|
It's always a good idea to take a step back and describe your game rules in human sentences. This can make it very clear how you should organize the responsibilities of your engines.
|
|
|
|
This means we can get rid of some stuff. There's no need for the **SpawnBallAfterDestroyComponent** any more, so we can delete that file and all references to that component.
|
|
|
|
Now let's handle the game state change. We have a UI Text system now, so we can get rid of the TitleRenderer and move that stuff to be handled by **GameStateEngine**.
|
|
|
|
In **GameStateEngine**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using Microsoft.Xna.Framework.Input;
|
|
using MoonTools.Structs;
|
|
using PongFE.Components;
|
|
using PongFE.Enums;
|
|
using PongFE.Messages;
|
|
using SpriteFontPlus;
|
|
|
|
namespace PongFE.Engines
|
|
{
|
|
[Reads(
|
|
typeof(PositionComponent),
|
|
typeof(GameStateComponent),
|
|
typeof(PlayAreaComponent),
|
|
typeof(UITextComponent)
|
|
)]
|
|
[Receives(typeof(ChangeGameStateMessage))]
|
|
[Sends(
|
|
typeof(BallSpawnMessage),
|
|
typeof(PaddleSpawnMessage),
|
|
typeof(BoundarySpawnMessage),
|
|
typeof(GoalBoundarySpawnMessage),
|
|
typeof(UITextSpawnMessage)
|
|
)]
|
|
[Writes(typeof(GameStateComponent))]
|
|
public class GameStateEngine : Engine
|
|
{
|
|
private DynamicSpriteFont TitleFont { get; }
|
|
private DynamicSpriteFont InstructionFont { get; }
|
|
|
|
public GameStateEngine(DynamicSpriteFont titleFont, DynamicSpriteFont instructionFont)
|
|
{
|
|
TitleFont = titleFont;
|
|
InstructionFont = instructionFont;
|
|
}
|
|
|
|
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))
|
|
{
|
|
EndTitle();
|
|
StartGame();
|
|
|
|
SetComponent(gameStateEntity, new GameStateComponent(GameState.Game));
|
|
}
|
|
}
|
|
|
|
if (SomeMessage<ChangeGameStateMessage>())
|
|
{
|
|
ref readonly var changeGameStateMessage = ref ReadMessage<ChangeGameStateMessage>();
|
|
|
|
if (changeGameStateMessage.GameState == gameStateComponent.GameState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (changeGameStateMessage.GameState == GameState.Title)
|
|
{
|
|
EndGame();
|
|
StartTitle();
|
|
|
|
SetComponent(gameStateEntity, new GameStateComponent(GameState.Title));
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
)
|
|
);
|
|
}
|
|
|
|
private void EndGame()
|
|
{
|
|
DestroyAllWith<PositionComponent>();
|
|
}
|
|
|
|
private void StartTitle()
|
|
{
|
|
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
|
|
|
|
var titleDimensions = TitleFont.MeasureString("PongFE");
|
|
var titlePosition = new Position2D(
|
|
(playAreaComponent.Width - titleDimensions.X) / 2,
|
|
(playAreaComponent.Height - titleDimensions.Y) / 4
|
|
);
|
|
|
|
SendMessage(new UITextSpawnMessage(
|
|
titlePosition,
|
|
TitleFont,
|
|
"PongFE"
|
|
));
|
|
|
|
var instructionDimensions = InstructionFont.MeasureString("Press Enter to begin");
|
|
var instructionPosition = new Position2D(
|
|
(playAreaComponent.Width - instructionDimensions.X) / 2,
|
|
playAreaComponent.Height * 2 / 3
|
|
);
|
|
|
|
SendMessage(new UITextSpawnMessage(
|
|
instructionPosition,
|
|
InstructionFont,
|
|
"Press Enter to play"
|
|
));
|
|
}
|
|
|
|
private void EndTitle()
|
|
{
|
|
DestroyAllWith<UITextComponent>();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
We have a new method showing up here, **DestroyAllWith**. This convenient method destroys all Entities that have a component of the given type. When we end gameplay, we destroy everything that has a **PositionComponent**. When we end the title screen, we destroy everything that has a **UITextComponent**. Easy.
|
|
|
|
One last little bit of business to take care of - we have moved the title screen initialization code into this Engine, so let's create an Init game state and then send a message to transition to the Title game state to avoid duplication.
|
|
|
|
In **Enums.cs**:
|
|
|
|
```cs
|
|
public enum GameState
|
|
{
|
|
Init,
|
|
Title,
|
|
Game
|
|
}
|
|
```
|
|
|
|
In **PongFEGame**:
|
|
|
|
```cs
|
|
var ballParametersEntity = WorldBuilder.CreateEntity();
|
|
WorldBuilder.SetComponent(ballParametersEntity, new BallParametersComponent(500, 0.5));
|
|
|
|
var gameStateEntity = WorldBuilder.CreateEntity();
|
|
WorldBuilder.SetComponent(gameStateEntity, new GameStateComponent(GameState.Init));
|
|
|
|
WorldBuilder.SendMessage(new ChangeGameStateMessage(GameState.Title));
|
|
```
|
|
|
|
And don't forget to add our new Engines and Renderer.
|
|
|
|
```cs
|
|
WorldBuilder.AddEngine(new GameStateEngine(ScoreFont, InstructionFont));
|
|
WorldBuilder.AddEngine(new GameWinEngine(ScoreFont));
|
|
WorldBuilder.AddEngine(new UITextSpawner());
|
|
|
|
...
|
|
|
|
WorldBuilder.AddGeneralRenderer(new UITextRenderer(SpriteBatch), 0);
|
|
```
|
|
|
|
That's it... our re-implementation of Pong is complete! If you followed along to this point, give yourself a pat on the back.
|