encompass-cs-docs/content/pong/move_paddle/decoupling.md

5.9 KiB

title date weight
Decoupling 2019-05-23T16:37:26-07:00 30

There's more to decoupling than just objects not directly referencing each other.

Really, when we talk about decoupling, we are saying that we don't want the program having unnecessary connections between structures in the code.

This is a pretty abstract principle, but there is a nice illustration of it in our program as it exists right now.

Right now our InputEngine is sending Messages to our MotionEngine. Is that actually what we want?

What I am saying is... can you think of an example where something other than direct input might want to control the movement of a paddle?

What about AI? What about an "attract mode", where the game plays itself?

There's also a lot of structure about the paddle entity being embedded into the logic of the InputEngine. That doesn't feel good to me either.

Let's put something in between them.

We have to ask ourselves: how is it that we want the game to react when we press the buttons? We want the correct paddle to move up or down. That seems like a good abstraction for a Message.

Create a file: PongFE/Messages/PaddleMoveMessage.cs

using Encompass;

namespace PongFE.Messages
{
    public enum PaddleMoveDirection
    {
        Up,
        Down
    }

    public struct PaddleMoveMessage : IMessage, IHasEntity
    {
        public Entity Entity { get; }
        public PaddleMoveDirection PaddleMoveDirection { get; }

        public PaddleMoveMessage(Entity entity, PaddleMoveDirection paddleMoveDirection)
        {
            Entity = entity;
            PaddleMoveDirection = paddleMoveDirection;
        }
    }
}

Now we're ready to rumble!!

Create a file: PongFE/Engines/PaddleMovementEngine.cs

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

namespace PongFE.Engines
{
    [Reads(typeof(PaddleMoveSpeedComponent))]
    [Receives(typeof(PaddleMoveMessage))]
    [Sends(typeof(MotionMessage))]
    public class PaddleMovementEngine : Engine
    {
        public override void Update(double dt)
        {
            foreach (ref readonly var message in ReadMessages<PaddleMoveMessage>())
            {
                if (HasComponent<PaddleMoveSpeedComponent>(message.Entity))
                {
                    var directionMultiplier = message.PaddleMoveDirection == PaddleMoveDirection.Down ? 1 : -1;

                    ref readonly var paddleMoveSpeedComponent =
                        ref GetComponent<PaddleMoveSpeedComponent>(message.Entity);

                    SendMessage(
                        new MotionMessage(
                            message.Entity,
                            new System.Numerics.Vector2(0, paddleMoveSpeedComponent.Speed * directionMultiplier * (float)dt)
                        )
                    );
                }
            }
        }
    }
}

Finally, we revise our InputEngine to send PaddleMoveMessages.

using Encompass;
using Microsoft.Xna.Framework.Input;
using PongFE.Components;
using PongFE.Messages;

namespace PongFE.Engines
{
    [Reads(typeof(PlayerInputComponent))]
    [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 (playerInputComponent.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
                            )
                        );
                    }
                }
            }
        }
    }
}

And don't forgot to add our new Engine to the WorldBuilder.

    ...

    WorldBuilder.AddEngine(new InputEngine());
    WorldBuilder.AddEngine(new PaddleMovementEngine());
    WorldBuilder.AddEngine(new MotionEngine());

    ...

Look at how concise and easy to understand everything is now! If each of your engines has a very precise and simple to explain job, you are in a good place. InputEngine's job is to detect input and send messages in response to the input. PaddleMovementEngine's job is to check for paddle movement messages and move the appropriate paddle. And MotionEngine's job is to move entities. And when we get around to adding Player 2 we'll barely have to do any work at all, because everything that drives Player 1 can be used to drive Player 2.

Decoupling isn't just a law we follow just because someone told us it was a good idea or whatever. It exists as a principle to remind us to try to express our ideas at a higher level, so we can write more powerful, flexible, and concise code and create less room for error.

When you get more experienced you'll be able to sniff out tightly coupled code very easily. Don't stress about it too much, but always be thinking about how you can make your code structure more expressive and easy to understand and reuse.

Before we move on, let's make the paddle a little zippier.

    WorldBuilder.SetComponent(paddle, new PaddleMoveSpeedComponent(400));

That's more like it.