encompass-cs-docs/content/pong/opponent/_index.md

6.2 KiB

title date weight
The Opponent 2019-05-30T13:11:35-07:00 500

Now that we have the ball moving around and bouncing, let's get the computer-controlled paddle working. We already have a lot of tools available to us from our implementation of the player paddle, so let's re-use as much as we can.

The computer-controlled paddle is essentially the same as the player-controlled paddle. The only difference is what causes it to move. So here's what we need to do.

  • Designate in the PaddleSpawnMessage what is controlling the paddle, a player or the computer
  • Implement an engine that sends PaddleMoveMessages to the paddle for the computer-controlled paddle

Let's revise our PaddleSpawnMessage. An enum type for our paddle control seems appropriate here.

In Enums.cs:

    public enum PaddleControl
    {
        Player,
        Computer
    }

In PaddleSpawnMessage.cs:

using Encompass;
using MoonTools.Structs;
using PongFE.Enums;

namespace PongFE.Messages
{
    public struct PaddleSpawnMessage : IMessage
    {
        public Position2D Position { get; }
        public PlayerIndex PlayerIndex { get; }
        public PaddleControl PaddleControl { get; }
        public int Width { get; }
        public int Height { get; }

        public PaddleSpawnMessage(
            Position2D position,
            PlayerIndex playerIndex,
            PaddleControl paddleControl,
            int width,
            int height
        )
        {
            Position = position;
            PlayerIndex = playerIndex;
            PaddleControl = paddleControl;
            Width = width;
            Height = height;
        }
    }
}

Let's create a new component to designate computer control of the paddle.

PongFE/Components/ComputerControlComponent.cs:

using Encompass;
using PongFE.Enums;

namespace PongFE.Components
{
    public struct ComputerControlComponent : IComponent
    {
        public PlayerIndex PlayerIndex { get; }

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

Then in the PaddleSpawner, where we add the PlayerInputComponent, let's put this instead:

    if (message.PaddleControl == PaddleControl.Player)
    {
        AddComponent(paddle, new PlayerInputComponent(message.PlayerIndex));
    }
    else
    {
        AddComponent(paddle, new ComputerControlComponent(message.PlayerIndex));
    }

Now we can move on to the actual computer control behavior. The behavior of the Pong computer is pretty simple - it just moves the paddle towards the y position of the ball.

Let's create a new component.

In PongFE/Components/CanBeTrackedComponent.cs:

using Encompass;

namespace PongFE.Components
{
    public struct CanBeTrackedComponent : IComponent { }
}

and in our BallSpawner:

AddComponent(ball, new CanBeTrackedComponent());

Now let's make our ComputerControlEngine. It will read the state of the game and tell the paddle to move in the direction of the ball.

In PongFE/Engines/ComputerControlEngine.ts:

using Encompass;
using PongFE.Components;
using PongFE.Enums;
using PongFE.Messages;

namespace PongFE.Engines
{
    [Reads(
        typeof(ComputerControlComponent),
        typeof(PositionComponent),
        typeof(CanBeTrackedComponent)
    )]
    [Sends(typeof(PaddleMoveMessage))]
    public class ComputerControlEngine : Engine
    {
        public override void Update(double dt)
        {
            foreach (ref readonly var entity in ReadEntities<ComputerControlComponent>())
            {
                if (HasComponent<PositionComponent>(entity))
                {
                    ref readonly var trackerPositionComponent = ref GetComponent<PositionComponent>(entity);

                    if (SomeComponent<CanBeTrackedComponent>())
                    {
                        ref readonly var trackedEntity = ref ReadEntity<CanBeTrackedComponent>();

                        if (HasComponent<PositionComponent>(trackedEntity))
                        {
                            ref readonly var trackedPositionComponent = ref GetComponent<PositionComponent>(trackedEntity);

                            if (trackerPositionComponent.Position.Y - trackedPositionComponent.Position.Y > 40)
                            {
                                SendMessage(new PaddleMoveMessage(entity, PaddleMoveDirection.Up));
                            }
                            else if (trackerPositionComponent.Position.Y - trackedPositionComponent.Position.Y < -40)
                            {
                                SendMessage(new PaddleMoveMessage(entity, PaddleMoveDirection.Down));
                            }
                        }
                    }
                }
            }
        }
    }
}

We have two new method showing up here, SomeComponent. SomeComponent is a handy method that lets us know if a component of a certain type exists in the World. ReadEntity just gives us an arbitrary entity that contains a component of a certain type.

Notice how we are being careful not to assume the tracked entity actually exists in this code. Remember - it never hurts to check if a component actually exists! You might save yourself from a nasty crash.

Don't forget to add the new Engine in PongFEgame.cs!

WorldBuilder.AddEngine(new ComputerControlEngine());

Finally, let's send a message to spawn the computer-controlled paddle.

    WorldBuilder.SendMessage(
        new PaddleSpawnMessage(
            new MoonTools.Structs.Position2D(1255, 5),
            Enums.PlayerIndex.Two,
            PaddleControl.Computer,
            20,
            80
        )
    );

If we were doing this in an object-oriented way, we would have had to inherit from the paddle or introduce another state to the paddle, thus forcing us to refactor or increase the complexity of the paddle object itself.

Notice how in our case we didn't really have to change any of our existing logic - all we had to do was create new components and write a new engine for producing behavior from those components, while getting to retain all the behavior we got from the other paddle components. See how clean and de-coupled this is? This is the power of composition over inheritance.