win condition
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
ac67f06793
commit
f7ecefc1c2
|
@ -6,253 +6,548 @@ 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.
|
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.
|
||||||
|
|
||||||
First let's create two separate components - one that holds information about the victory, and one that counts down to when the game should return to the title screen.
|
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.
|
||||||
|
|
||||||
```ts
|
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.
|
||||||
import { Component } from "encompass-ecs";
|
|
||||||
|
|
||||||
export class WinDisplayComponent extends Component {
|
In **PongFE/Components/UITextComponent.cs**
|
||||||
public player_index: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
import { Component } from "encompass-ecs";
|
using Encompass;
|
||||||
|
using SpriteFontPlus;
|
||||||
|
|
||||||
export class FinishGameTimerComponent extends Component {
|
namespace PongFE.Components
|
||||||
public time_remaining: number;
|
{
|
||||||
}
|
public struct UITextComponent : IComponent
|
||||||
```
|
{
|
||||||
|
public DynamicSpriteFont Font { get; }
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
Now we'll create an engine that checks the game score and emits a message if one player has reached that score.
|
public UITextComponent(DynamicSpriteFont font, string text)
|
||||||
|
{
|
||||||
```ts
|
Font = font;
|
||||||
import { Emits, Engine } from "encompass-ecs";
|
Text = text;
|
||||||
import { GoalOneComponent } from "game/components/goal_one";
|
|
||||||
import { GoalTwoComponent } from "game/components/goal_two";
|
|
||||||
import { ScoreComponent } from "game/components/score";
|
|
||||||
import { WinDisplayComponent } from "game/components/win_display";
|
|
||||||
import { WinMessage } from "game/messages/win";
|
|
||||||
|
|
||||||
@Emits(WinMessage)
|
|
||||||
export class CheckScoreEngine extends Engine {
|
|
||||||
private winning_score: number;
|
|
||||||
|
|
||||||
public initialize(winning_score: number) {
|
|
||||||
this.winning_score = winning_score;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update() {
|
|
||||||
if (this.read_component(WinDisplayComponent)) { return; }
|
|
||||||
|
|
||||||
const goal_one_component = this.read_component(GoalOneComponent);
|
|
||||||
const goal_two_component = this.read_component(GoalTwoComponent);
|
|
||||||
|
|
||||||
if (goal_one_component && goal_two_component) {
|
|
||||||
const goal_one_entity = this.get_entity(goal_one_component.entity_id);
|
|
||||||
const goal_two_entity = this.get_entity(goal_two_component.entity_id);
|
|
||||||
|
|
||||||
if (goal_one_entity && goal_two_entity) {
|
|
||||||
const score_one_component = goal_one_entity.get_component(ScoreComponent);
|
|
||||||
const score_two_component = goal_two_entity.get_component(ScoreComponent);
|
|
||||||
|
|
||||||
const score_one = score_one_component.score;
|
|
||||||
const score_two = score_two_component.score;
|
|
||||||
|
|
||||||
if (score_one >= this.winning_score) {
|
|
||||||
const win_message = this.emit_message(WinMessage);
|
|
||||||
win_message.player_index = 1;
|
|
||||||
} else if (score_two >= this.winning_score) {
|
|
||||||
const win_message = this.emit_message(WinMessage);
|
|
||||||
win_message.player_index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we can create an Engine that receives the WinMessage and adds new components to the game world.
|
In **PongFE/Messages/UITextSpawnMessage.cs**:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
import { Engine, Reads } from "encompass-ecs";
|
using Encompass;
|
||||||
import { FinishGameTimerComponent } from "game/components/finish_game_timer";
|
using MoonTools.Structs;
|
||||||
import { WinDisplayComponent } from "game/components/win_display";
|
using SpriteFontPlus;
|
||||||
import { WinMessage } from "game/messages/win";
|
|
||||||
|
|
||||||
@Reads(WinMessage)
|
namespace PongFE.Messages
|
||||||
export class WinEngine extends Engine {
|
{
|
||||||
public update() {
|
public struct UITextSpawnMessage : IMessage
|
||||||
const win_messages = this.read_messages(WinMessage);
|
{
|
||||||
|
public Position2D Position { get; }
|
||||||
|
public string Text { get; }
|
||||||
|
public DynamicSpriteFont Font { get; }
|
||||||
|
|
||||||
for (const win_message of win_messages.values()) {
|
public UITextSpawnMessage(Position2D position, DynamicSpriteFont font, string text)
|
||||||
const entity = this.create_entity();
|
{
|
||||||
const component = entity.add_component(WinDisplayComponent);
|
Position = position;
|
||||||
component.player_index = win_message.player_index;
|
Font = font;
|
||||||
|
Text = text;
|
||||||
const timer_entity = this.create_entity();
|
|
||||||
const timer_component = timer_entity.add_component(FinishGameTimerComponent);
|
|
||||||
timer_component.time_remaining = 3;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
We want to see a win message display, so let's create a new GeneralRenderer that shows one.
|
In **PongFE/Engines/Spawners/UITextSpawner.cs**:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
import { GeneralRenderer } from "encompass-ecs";
|
using Encompass;
|
||||||
import { WinDisplayComponent } from "game/components/win_display";
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
export class WinRenderer extends GeneralRenderer {
|
namespace PongFE.Spawners
|
||||||
public layer = 2;
|
{
|
||||||
|
public class UITextSpawner : Spawner<UITextSpawnMessage>
|
||||||
|
{
|
||||||
|
protected override void Spawn(UITextSpawnMessage message)
|
||||||
|
{
|
||||||
|
var entity = CreateEntity();
|
||||||
|
|
||||||
private win_font: Font;
|
AddComponent(entity, new PositionComponent(message.Position));
|
||||||
private win_text: Text;
|
AddComponent(entity, new UITextComponent(message.Font, message.Text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
private x: number;
|
Now we need a way to render the text.
|
||||||
private y: number;
|
|
||||||
private padding: number;
|
|
||||||
|
|
||||||
public initialize(x: number, y: number, padding: number) {
|
In **PongFE/Renderers/UITextRenderer.cs**:
|
||||||
this.win_font = love.graphics.newFont("game/assets/fonts/Squared Display.ttf", 128);
|
|
||||||
this.win_text = love.graphics.newText(this.win_font, "");
|
|
||||||
|
|
||||||
this.x = x;
|
```cs
|
||||||
this.y = y;
|
using Encompass;
|
||||||
this.padding = padding;
|
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 render() {
|
public override void Render()
|
||||||
const win_component = this.read_component(WinDisplayComponent);
|
{
|
||||||
|
foreach (ref readonly var entity in ReadEntities<UITextComponent>())
|
||||||
|
{
|
||||||
|
ref readonly var uiTextComponent = ref GetComponent<UITextComponent>(entity);
|
||||||
|
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
|
||||||
|
|
||||||
if (win_component) {
|
SpriteBatch.DrawString(
|
||||||
this.win_text.set("Player " + (win_component.player_index + 1) + " wins");
|
uiTextComponent.Font,
|
||||||
|
uiTextComponent.Text,
|
||||||
const win_text_width = this.win_text.getWidth();
|
positionComponent.Position.ToXNAVector(),
|
||||||
const win_text_height = this.win_text.getHeight();
|
Color.White
|
||||||
|
|
||||||
love.graphics.setColor(0, 0, 0, 1);
|
|
||||||
love.graphics.rectangle(
|
|
||||||
"fill",
|
|
||||||
this.x - win_text_width * 0.5 - this.padding,
|
|
||||||
this.y - win_text_height * 0.5 - this.padding,
|
|
||||||
win_text_width + this.padding,
|
|
||||||
win_text_height + this.padding,
|
|
||||||
);
|
|
||||||
|
|
||||||
love.graphics.setColor(1, 1, 1, 1);
|
|
||||||
love.graphics.rectangle(
|
|
||||||
"line",
|
|
||||||
this.x - win_text_width * 0.5 - this.padding,
|
|
||||||
this.y - win_text_height * 0.5 - this.padding,
|
|
||||||
win_text_width + this.padding,
|
|
||||||
win_text_height + this.padding,
|
|
||||||
);
|
|
||||||
|
|
||||||
love.graphics.draw(
|
|
||||||
this.win_text,
|
|
||||||
this.x,
|
|
||||||
this.y,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
this.win_text.getWidth() * 0.5,
|
|
||||||
this.win_text.getHeight() * 0.5,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's also make it so the ball stops spawning if the game is over. In our BallSpawnTimerEngine component loop:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
...
|
With that out of the way, let's create a message for winning the game and a message for changing the game state.
|
||||||
if (this.read_component(WinDisplayComponent)) {
|
|
||||||
this.get_entity(component.entity_id)!.destroy();
|
In **PongFE/Messages/GameWinMessage.cs**:
|
||||||
continue;
|
|
||||||
}
|
```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));world_builder.add_engine(WinEngine);
|
||||||
|
WorldBuilder.AddEngine(new GameWinEngine(ScoreFont));
|
||||||
|
WorldBuilder.AddEngine(new UITextSpawner());
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
WorldBuilder.AddGeneralRenderer(new UITextRenderer(SpriteBatch), 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, we're going to need some logic to take us back to the title screen. We _could_ just pass the Game state to a FinishGameEngine... but then we would have a circular dependency. This is a prime candidate for an Interface.
|
That's it... our re-implementation of Pong is complete! If you followed along to this point, give yourself a pat on the back.
|
||||||
|
|
||||||
Let's create a new folder, **game/interfaces** and a new file, **game/interfaces/finishable.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export interface IFinishable { finished: boolean; }
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, in **game/states/game.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export class Game extends State implements IFinishable {
|
|
||||||
```
|
|
||||||
|
|
||||||
Now let's create our FinishGameEngine.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Engine, Mutates } from "encompass-ecs";
|
|
||||||
import { FinishGameTimerComponent } from "../components/finish_game_timer";
|
|
||||||
import { IFinishable } from "../interfaces/finishable";
|
|
||||||
|
|
||||||
@Mutates(FinishGameTimerComponent)
|
|
||||||
export class FinishGameEngine extends Engine {
|
|
||||||
private game_state: IFinishable;
|
|
||||||
|
|
||||||
public initialize(game_state: IFinishable) {
|
|
||||||
this.game_state = game_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(dt: number) {
|
|
||||||
const finish_game_timer_component = this.read_component_mutable(FinishGameTimerComponent);
|
|
||||||
|
|
||||||
if (finish_game_timer_component) {
|
|
||||||
finish_game_timer_component.time_remaining -= dt;
|
|
||||||
|
|
||||||
if (finish_game_timer_component.time_remaining <= 0) {
|
|
||||||
this.game_state.finished = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is pretty straightforward. Once the timer expires we change the game state to let the rest of the program know the state is finished. Now we can use this in our main loop. In **main.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
love.update = (dt) => {
|
|
||||||
if (current_state === title) {
|
|
||||||
if (love.keyboard.isDown("space")) {
|
|
||||||
current_state = game;
|
|
||||||
}
|
|
||||||
} else if (current_state === game) {
|
|
||||||
if (game.finished) {
|
|
||||||
game = new Game();
|
|
||||||
game.load();
|
|
||||||
|
|
||||||
current_state = title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current_state.update(dt);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Now when the game is finished, it creates a new Game and switches the current state back to the title menu.
|
|
||||||
|
|
||||||
Don't forget to add our new Engines to the WorldBuilder.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
world_builder.add_engine(CheckScoreEngine).initialize(winning_score);
|
|
||||||
world_builder.add_engine(WinEngine);
|
|
||||||
world_builder.add_engine(FinishGameEngine).initialize(this);
|
|
||||||
|
|
||||||
world_builder.add_engine(WinRenderer).initialize(play_area_width * 0.5, play_area_height * 0.5, 20);
|
|
||||||
```
|
|
||||||
|
|
||||||
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
|
|
||||||
<source src="/images/win.mp4" type="video/mp4">
|
|
||||||
</video>
|
|
||||||
|
|
||||||
That's it... we're done.
|
|
||||||
|
|
Loading…
Reference in New Issue