encompass-cs-docs/content/pong/scoring/tracking_score.md

8.9 KiB

title date weight
Tracking the Score 2019-06-04T16:49:50-07:00 30

Finally, we need to track the score and update it appropriately.

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.

We will obviously need a ScoreComponent.

In PongFE/Component/ScoreComponent.cs:

using Encompass;

namespace PongFE.Components
{
    public struct ScoreComponent
    {
        public int Score { get; }

        public ScoreComponent(int score)
        {
            Score = score;
        }
    }
}

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?

In GoalBoundarySpawner.cs:

    AddComponent(entity, new ScoreComponent(0));

Now let's create a collision response component.

In PongFE/Components/IncreaseScoreAfterDestroyComponent.cs:

using Encompass;

namespace PongFE.Components
{
    public struct IncreaseScoreAfterDestroyComponent { }
}

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:

using Encompass;

namespace PongFE.Messages
{
    public struct DestroyMessage
    {
        public Entity Entity { get; }
        public Entity DestroyedBy { get; }

        public DestroyMessage(Entity entity, Entity destroyedBy)
        {
            Entity = entity;
            DestroyedBy = destroyedBy;
        }
    }
}

And in CollisionEngine.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:

using Encompass;

namespace PongFE.Messages
{
    public struct ScoreMessage
    {
        public Entity Entity { get; }

        public ScoreMessage(Entity entity)
        {
            Entity = entity;
        }
    }
}

And in PongFE/Engines/ScoreEngine.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:

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.

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.

    AddComponent(ball, new IncreaseScoreAfterDestroyComponent());

And remember to add our new ScoreEngine to the WorldBuilder in PongFEGame.cs.

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:

using Encompass;
using PongFE.Enums;

namespace PongFE.Components
{
    public struct PlayerComponent
    {
        public PlayerIndex PlayerIndex { get; }

        public PlayerComponent(PlayerIndex playerIndex)
        {
            PlayerIndex = playerIndex;
        }
    }
}

Now our PaddleSpawner can look like this:

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(in 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.

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.