encompass-cs-docs/content/pong/ball/moving.md

4.5 KiB

title date weight
Moving 2019-05-23T18:10:17-07:00 20

We already have MotionMessages and a MotionEngine. So it seems logical to re-use these structures for our ball.

What is actually going to be sending out the MotionMessages?

What is the main characteristic of the ball in Pong? That's right - it is continuously moving. In other words, it has velocity.

Let's make a VelocityComponent. In PongFE/Components/VelocityComponent.cs:

using System.Numerics;
using Encompass;

namespace PongFE.Components
{
    public struct VelocityComponent
    {
        public Vector2 Velocity { get; }

        public VelocityComponent(Vector2 velocity)
        {
            Velocity = velocity;
        }
    }
}

Now, the XNA API actually provides us with a Vector2 struct. Why am I using System.Numerics here? The answer is that it is more up-to-date and optimized than the XNA specification. As long as we can convert the values when we need to hand the Vector2 off to something, it won't be an issue.

Let's also create a VelocityEngine.

What does our VelocityEngine actually do? Basically, if something has both a PositionComponent and VelocityComponent, we want the PositionComponent to update based on the VelocityComponent every frame.

It turns out Encompass provides a structure for this pattern. Let's use it now.

Create PongFE/Engines/VelocityEngine.cs:

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

namespace PongFE.Engines
{
    [Sends(typeof(MotionMessage))]
    [QueryWith(typeof(PositionComponent), typeof(VelocityComponent))]
    public class VelocityEngine : Engine
    {
        public override void Update(double dt)
        {
            foreach (var entity in TrackedEntities)
            {
                ref readonly var velocityComponent = ref GetComponent<VelocityComponent>(entity);
                SendMessage(new MotionMessage(entity, velocityComponent.Velocity * (float)dt));
            }
        }
    }
}

QueryWith is a class attribute that allows us to specify a set of components that will cause the Engine to track an entity. In this case, our QueryWith attribute will cause entities that have both a PositionComponent and a VelocityComponent to be tracked. QueryWith also implicitly creates Reads for the relevant Components.

{{% notice note %}} There is also a QueryWithout that will exclude entities from tracking if they have the specified component(s). This can come in handy if you, say, want to temporarily pause motion or something. {{% /notice %}}

Let's add our new Engine to the WorldBuilder:

WorldBuilder.AddEngine(new VelocityEngine());

And add our new VelocityComponent in the BallSpawner.

AddComponent(ball, new VelocityComponent(new System.Numerics.Vector2(50, -50)));

Actually... let's get rid of that magic value by adding velocity to the BallSpawnMessage.

PongFE/Messages/BallSpawnMessage.cs

using System.Numerics;
using Encompass;
using MoonTools.Structs;

namespace PongFE.Messages
{
    public struct BallSpawnMessage
    {
        public Position2D Position { get; }
        public Vector2 Velocity { get; }

        public BallSpawnMessage(Position2D position, Vector2 velocity)
        {
            Position = position;
            Velocity = velocity;
        }
    }
}

PongFE/Engines/Spawners/BallSpawner.cs

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

namespace PongFE.Spawners
{
    public class BallSpawner : Spawner<BallSpawnMessage>
    {
        private Texture2D BallTexture { get; }

        public BallSpawner(Texture2D ballTexture)
        {
            BallTexture = ballTexture;
        }

        protected override void Spawn(in BallSpawnMessage message)
        {
            var ball = CreateEntity();
            AddComponent(ball, new PositionComponent(message.Position));
            AddComponent(ball, new VelocityComponent(message.Velocity));
            AddComponent(ball, new Texture2DComponent(BallTexture, 0));
        }
    }
}

PongFEGame.cs

    WorldBuilder.SendMessage(
        new BallSpawnMessage(
            new MoonTools.Structs.Position2D(640, 360),
            new System.Numerics.Vector2(50, -50)
        )
    );

Let's run the game again.

Still pretty boring but we're getting somewhere.