drawing score
continuous-integration/drone/push Build is passing Details

main
Evan Hemsley 2020-07-16 15:10:12 -07:00
parent f5baa1c093
commit 800ef53ac8
2 changed files with 496 additions and 161 deletions

View File

@ -4,84 +4,218 @@ date: 2019-06-04T17:21:52-07:00
weight: 40
---
Remember Renderers? Haven't thought about those in a while.
Remember Renderers? Haven't thought about those in a while!
All we need to draw new elements to the screen are Renderers. Let's create a new GeneralRenderer.
All we need to draw new elements to the screen are Renderers. Since displaying the score is a UI element, I think it would be best to do this with a GeneralRenderer.
But first, we're gonna need a font. I liked [this font](https://www.dafont.com/squared-display.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 **game/assets/fonts**. Then it can be used in your game.
Place the font of your heart's desire into the directory **PongFE/Content/Fonts**.
Let's write our ScoreRenderer.
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 **game/renderers/score.ts**:
In your terminal, do the following commands:
```ts
import { Component, GeneralRenderer, Type } from "encompass-ecs";
import { GoalOneComponent } from "game/components/goal_one";
import { GoalTwoComponent } from "game/components/goal_two";
import { ScoreComponent } from "game/components/score";
```sh
git submodule add https://github.com/rds1983/SpriteFontPlus.git
git submodule update --init --recursive
```
export class ScoreRenderer extends GeneralRenderer {
public layer = 1;
In **PongFE.Framework.csproj**:
private midpoint: number;
private score_font: Font;
private player_one_score_text: Text;
private player_two_score_text: Text;
```xml
<ItemGroup>
<ProjectReference Include="..\FNA\FNA.csproj"/>
<ProjectReference Include="..\encompass-cs\encompass-cs\encompass-cs.csproj"/>
<ProjectReference Include="..\SpriteFontPlus\src\SpriteFontPlus.FNA.csproj" />
</ItemGroup>
```
public initialize(midpoint: number) {
this.midpoint = midpoint;
this.score_font = love.graphics.newFont("game/assets/fonts/Squared Display.ttf", 128);
this.player_one_score_text = love.graphics.newText(this.score_font, "0");
this.player_two_score_text = love.graphics.newText(this.score_font, "0");
And in **PongFE.Core.csproj**:
```xml
<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**:
```cs
using Encompass;
using MoonTools.Structs;
using PongFE.Enums;
namespace PongFE.Messages
{
public struct GoalBoundarySpawnMessage : IMessage
{
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;
}
}
}
```
public render() {
this.render_score(GoalTwoComponent, this.player_two_score_text, this.midpoint - 200, 30);
this.render_score(GoalOneComponent, this.player_one_score_text, this.midpoint + 200, 30);
**GoalBoundarySpawner.cs**:
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Spawners
{
public class GoalBoundarySpawner : Spawner<GoalBoundarySpawnMessage>
{
protected override void Spawn(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));
}
}
}
```
private render_score(ComponentType: Type<Component>, score_text: Text, x: number, y: number) {
const goal_component = this.read_component(ComponentType);
if (goal_component) {
const entity = this.get_entity(goal_component.entity_id);
if (entity) {
const score_component = entity.get_component(ScoreComponent);
if (score_component) {
score_text.set(score_component.score.toString());
Now we can adjust the spawn messages in **PongFEGame.cs**:
love.graphics.draw(
score_text,
x,
y,
0,
1,
1,
score_text.getWidth() * 0.5,
score_text.getHeight() * 0.5,
);
```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**:
```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 : GeneralRenderer
{
public SpriteBatch SpriteBatch { get; }
public DynamicSpriteFont Font { get; }
public ScoreRenderer(SpriteBatch spriteBatch, DynamicSpriteFont font)
{
SpriteBatch = spriteBatch;
Font = font;
}
public override void Render()
{
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(400, 20),
Color.White
);
}
if (playerTwoScore.HasValue)
{
SpriteBatch.DrawString(
Font,
playerTwoScore.Value.ToString(),
new Vector2(880 - 64, 20),
Color.White
);
}
}
}
}
```
Basically, we find each goal component, grab its score component, and draw the score component's value to the screen as text.
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.
If we create new LOVE Text object every frame, this is very performance heavy. So we want to create a Text on initialization and then set its contents instead.
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.
It's also very expensive to create a new LOVE Font every frame. Like the Text objects, we store it on the Renderer.
Let's add our ScoreRenderer to the WorldBuilder. First we need to set up the font.
Let's add our ScoreRenderer to the WorldBuilder.
```cs
...
```ts
world_builder.add_renderer(ScoreRenderer).initialize(play_area_width * 0.5);
DynamicSpriteFont ScoreFont { get; set; }
...
public override void LoadContent()
{
...
ScoreFont = DynamicSpriteFont.FromTtf(File.ReadAllBytes(@"Content/Fonts/SquaredDisplay.ttf"), 128);
...
WorldBuilder.AddGeneralRenderer(new ScoreRenderer(SpriteBatch, ScoreFont), 0);
...
```
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
<source src="/images/score.webm" type="video/webm">
</video>
Look at that! It's starting to look and feel like a more complete game now.
Now you should be able to see the game score. This is starting to look and feel like a more complete game now.

View File

@ -6,124 +6,325 @@ weight: 30
Finally, we need to track the score and update it appropriately.
I think a scoring Component is appropriate.
How do we increase the score in Pong? When a ball collides with a goal, it increases the score of a player. That sounds like a collision response to me.
```ts
import { Component } from "encompass-ecs";
We will obviously need a ScoreComponent.
export class ScoreComponent extends Component {
public score: number;
}
```
In **PongFE/Component/ScoreComponent.cs**:
We should have two different scores, and they should update based on which specific goal is touched by the ball.
```cs
using Encompass;
I think we need another tag component.
namespace PongFE.Components
{
public struct ScoreComponent : IComponent
{
public int Score { get; }
```ts
import { Component } from "encompass-ecs";
export class GoalOneComponent extends Component {}
```
```ts
import { Component } from "encompass-ecs";
export class GoalTwoComponent extends Component {}
```
I know what you're thinking. Why not just have one GoalComponent with an index on it? Encompass lets us retrieve components from the game state by type, and it does so very quickly, so it is good to create structures that let us use that feature. Consider:
```ts
const goal_one_component = this.read_component(GoalOneComponent);
```
vs.
```ts
const goal_components = this.read_components(GoalComponent);
let goal_component;
for (const component of goal_components.values()) {
if (component.goal_index === 1) {
goal_component = component;
break;
}
}
```
The second one is way worse to read right? It's also much slower, performance-wise. Make use of Encompass's component retrieval structure wherever you can.
You know the drill by now.
Let's add a new property to our GoalSpawnMessage so we can tell which one is which.
```ts
public goal_index: number;
```
Let's add this in our GoalSpawner:
```ts
const score_component = entity.add_component(ScoreComponent);
score_component.score = 0;
if (message.goal_index === 0) {
entity.add_component(GoalOneComponent);
} else if (message.goal_index === 1) {
entity.add_component(GoalTwoComponent);
}
```
Now, we can tell which goal needs to have its score updated.
Let's create a score update message.
In **game/messages/score.ts**:
```ts
import { ComponentMessage, Message } from "encompass-ecs";
import { ScoreComponent } from "game/components/score";
export class ScoreMessage extends Message implements ComponentMessage {
public component: Readonly<ScoreComponent>;
public delta: number;
}
```
Now we can update our BallGoalCollisionEngine.
```ts
const score_component = message.goal_entity.get_component(ScoreComponent);
const score_message = this.emit_component_message(ScoreMessage, score_component);
score_message.delta = 1;
```
And let's create an engine to update the score.
In **game/engines/score.ts**:
```ts
import { Engine, Mutates, Reads } from "encompass-ecs";
import { ScoreComponent } from "game/components/score";
import { ScoreMessage } from "game/messages/score";
@Reads(ScoreMessage)
@Mutates(ScoreComponent)
export class ScoreEngine extends Engine {
public update() {
for (const score_message of this.read_messages(ScoreMessage).values()) {
const score_component = this.make_mutable(score_message.component);
score_component.score += score_message.delta;
public ScoreComponent(int score)
{
Score = score;
}
}
}
```
And add it to our WorldBuilder:
Now the question is: what do we attach this to? Well, the ball increases the score when it touches a goal boundary, right? So why not just put this component right on the goal boundary?
```ts
world_builder.add_engine(ScoreEngine);
In **GoalBoundarySpawner.cs**:
```cs
AddComponent(entity, new ScoreComponent(0));
```
Last, but not least, it would be nice to actually see the score being drawn on the screen.
Now let's create a collision response component.
In **PongFE/Components/IncreaseScoreAfterDestroyComponent.cs**:
```cs
using Encompass;
namespace PongFE.Components
{
public struct IncreaseScoreAfterDestroyComponent : IComponent { }
}
```
Let's take a step back for a second. **DestroyEngine** only knows the entity being destroyed. But it might be useful for it to also know the entity that is doing the destroying.
Let's revise **DestroyMessage.cs**:
```cs
using Encompass;
namespace PongFE.Messages
{
public struct DestroyMessage : IMessage
{
public Entity Entity { get; }
public Entity DestroyedBy { get; }
public DestroyMessage(Entity entity, Entity destroyedBy)
{
Entity = entity;
DestroyedBy = destroyedBy;
}
}
}
```
And in **CollisionEngine.cs**:
```cs
private void CheckDestroy(Entity a, Entity b)
{
if (HasComponent<CanDestroyComponent>(a))
{
if (HasComponent<CanBeDestroyedComponent>(b))
{
SendMessage(new DestroyMessage(b, a));
}
}
}
```
Let's create a mechanism for updating the score.
In **PongFE/Messages/ScoreMessage.cs**:
```cs
using Encompass;
namespace PongFE.Messages
{
public struct ScoreMessage : IMessage
{
public Entity Entity { get; }
public ScoreMessage(Entity entity)
{
Entity = entity;
}
}
}
```
And in **PongFE/Engines/ScoreEngine.cs**:
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(typeof(ScoreComponent))]
[Receives(typeof(ScoreMessage))]
[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 you want to make sure this is working, you can add a debug output line directly below SetComponent:
```cs
System.Console.WriteLine("score: {0}", scoreComponent.Score + 1);
```
Now the console should print every time the score increases.
Now we can send a ScoreMessage from **DestroyEngine**.
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(
typeof(SpawnBallAfterDestroyComponent),
typeof(IncreaseScoreAfterDestroyComponent)
)]
[Receives(typeof(DestroyMessage))]
[Sends(
typeof(BallSpawnMessage),
typeof(ScoreMessage)
)]
public class DestroyEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var message in ReadMessages<DestroyMessage>())
{
if (HasComponent<SpawnBallAfterDestroyComponent>(message.Entity))
{
ref readonly var respawnComponent = ref GetComponent<SpawnBallAfterDestroyComponent>(message.Entity);
SendMessage(
new BallSpawnMessage(
new MoonTools.Structs.Position2D(640, 360),
300,
16,
16
),
respawnComponent.Seconds
);
}
if (HasComponent<IncreaseScoreAfterDestroyComponent>(message.Entity))
{
SendMessage(new ScoreMessage(message.DestroyedBy));
}
Destroy(message.Entity);
}
}
}
}
```
Let's make sure to add our response to **BoundaryGoalSpawner.cs**.
```cs
AddComponent(ball, new IncreaseScoreAfterDestroyComponent());
```
And remember to add our new ScoreEngine to the WorldBuilder in **PongFEGame.cs**.
```ts
WorldBuilder.AddEngine(new ScoreEngine());
```
Before we move on, I would like us to make a slight adjustment to how the player index is tracked on our entities. Right now, we pass a PlayerIndex to either PlayerInputComponent or ComputerControlComponent. Since this information is useful across multiple entities and components, I think it would be best for this data to have its own component.
In **PongFE/Components/PlayerComponent.cs**:
```cs
using Encompass;
using PongFE.Enums;
namespace PongFE.Components
{
public struct PlayerComponent : IComponent
{
public PlayerIndex PlayerIndex { get; }
public PlayerComponent(PlayerIndex playerIndex)
{
PlayerIndex = playerIndex;
}
}
}
```
Now our **PaddleSpawner** can look like this:
```cs
using Encompass;
using Microsoft.Xna.Framework.Graphics;
using PongFE.Components;
using PongFE.Enums;
using PongFE.Messages;
namespace PongFE.Spawners
{
public class PaddleSpawner : Spawner<PaddleSpawnMessage>
{
private Texture2D WhitePixel { get; }
public PaddleSpawner(Texture2D whitePixel)
{
WhitePixel = whitePixel;
}
protected override void Spawn(PaddleSpawnMessage message)
{
var paddle = CreateEntity();
if (message.PaddleControl == PaddleControl.Player)
{
AddComponent(paddle, new PlayerInputComponent());
}
else
{
AddComponent(paddle, new ComputerControlComponent());
}
AddComponent(paddle, new PlayerComponent(message.PlayerIndex));
AddComponent(paddle, new PaddleMoveSpeedComponent(400));
AddComponent(paddle, new PositionComponent(message.Position));
AddComponent(paddle, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, message.Width, message.Height)));
AddComponent(paddle, new CanCauseBounceComponent());
AddComponent(paddle, new Texture2DComponent(WhitePixel, 0, new System.Numerics.Vector2(message.Width, message.Height)));
}
}
}
```
Now we make an adjustment to **InputEngine** as well.
```cs
using Encompass;
using Microsoft.Xna.Framework.Input;
using PongFE.Components;
using PongFE.Enums;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(typeof(PlayerInputComponent), typeof(PlayerComponent))]
[Sends(typeof(PaddleMoveMessage))]
public class InputEngine : Engine
{
public override void Update(double dt)
{
var keyboardState = Keyboard.GetState();
foreach (ref readonly var playerInputEntity in ReadEntities<PlayerInputComponent>())
{
ref readonly var playerInputComponent = ref GetComponent<PlayerInputComponent>(playerInputEntity);
if (HasComponent<PlayerComponent>(playerInputEntity))
{
ref readonly var playerComponent = ref GetComponent<PlayerComponent>(playerInputEntity);
if (playerComponent.PlayerIndex == PlayerIndex.One)
{
if (keyboardState.IsKeyDown(Keys.Down))
{
SendMessage(
new PaddleMoveMessage(
playerInputEntity,
PaddleMoveDirection.Down
)
);
}
else if (keyboardState.IsKeyDown(Keys.Up))
{
SendMessage(
new PaddleMoveMessage(
playerInputEntity,
PaddleMoveDirection.Up
)
);
}
}
}
}
}
}
}
```
Alright, now it would be nice to actually see the score being drawn on the screen.