Goal Collision

A goal destroys a ball, and then spawns a ball. So let’s set up a new actor-receiver-response set of components to model this behavior.

Our Actor: PongFE/Components/CanDestroyComponent.cs:

using Encompass;

namespace PongFE.Components
{
    public struct CanDestroyComponent { }
}

Our Receiver: PongFE/Components/CanBeDestroyedComponent.cs:

using Encompass;

namespace PongFE.Components
{
    public struct CanBeDestroyedComponent { }
}

Our Response: PongFE/Components/SpawnBallAfterDestroyComponent.cs:

using Encompass;

namespace PongFE.Components
{
    public struct SpawnBallAfterDestroyComponent
    {
        public float Seconds { get; }

        public SpawnBallAfterDestroyComponent(float seconds)
        {
            Seconds = seconds;
        }
    }
}

Now let’s create PongFE/Messages/DestroyMessage.cs:

using Encompass;

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

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

In CollisionEngine.cs:

    ...

    CheckBounce(message.EntityA, message.EntityB, message.HitOrientation);
    CheckBounce(message.EntityB, message.EntityA, message.HitOrientation);

    CheckDestroy(message.EntityA, message.EntityB);
    CheckDestroy(message.EntityB, message.EntityA);

    ...

    private void CheckDestroy(Entity a, Entity b)
    {
        if (HasComponent<CanDestroyComponent>(a))
        {
            if (HasComponent<CanBeDestroyedComponent>(b))
            {
                SendMessage(new DestroyMessage(b));
            }
        }
    }

Don’t forget to add typeof(DestroyMessage) to CollisionEngine’s Sends.

Now let’s create PongFE/Engines/DestroyEngine.cs:

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

namespace PongFE.Engines
{
    [Reads(typeof(SpawnBallAfterDestroyComponent))]
    [Receives(typeof(DestroyMessage))]
    [Sends(typeof(BallSpawnMessage))]
    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),
                            new System.Numerics.Vector2(-200, 100),
                            16,
                            16
                        ),
                        respawnComponent.Seconds
                    );
                }

                Destroy(message.Entity);
            }
        }
    }
}

Notice that SendMessage has an extra argument here. What’s that about? If you pass a numeric value along with SendMessage, the message will be delayed, and send after the given number of seconds has elapsed. This can be very convenient for producing timing-based behaviors. Be warned however, that there is no way to cancel a delayed message send. So if you need to be able to cancel a timed action, it’s probably better to set up a component/engine structure that keeps track of the time.

We’ll need a way to create our goal entities now.

Create a file, PongFE/Messages/GoalBoundarySpawnMessage.cs:

using Encompass;
using MoonTools.Structs;

namespace PongFE.Messages
{
    public struct GoalBoundarySpawnMessage
    {
        public Position2D Position { get; }
        public int Width { get; }
        public int Height { get; }

        public GoalBoundarySpawnMessage(Position2D position, int width, int height)
        {
            Position = position;
            Width = width;
            Height = height;
        }
    }
}

And a file, PongFE/Engines/Spawners/GoalBoundarySpawner.cs:

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

namespace PongFE.Spawners
{
    public class GoalBoundarySpawner : Spawner<GoalBoundarySpawnMessage>
    {
        protected override void Spawn(in 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());
        }
    }
}

Let’s add some components to BallSpawner.cs:

    AddComponent(ball, new CanBeDestroyedComponent());
    AddComponent(ball, new SpawnBallAfterDestroyComponent(0.5f));

Finally let’s add our new engines and spawn messages to PongFEGame.cs:

    ...

    WorldBuilder.AddEngine(new DestroyEngine());

    ...

    WorldBuilder.AddEngine(new GoalBoundarySpawner());

    ...

    // right boundary
    WorldBuilder.SendMessage(
        new GoalBoundarySpawnMessage(
            new MoonTools.Structs.Position2D(1280, 0),
            6,
            720
        )
    );

    // left boundary
    WorldBuilder.SendMessage(
        new GoalBoundarySpawnMessage(
            new MoonTools.Structs.Position2D(-6, 0),
            6,
            720
        )
    );

Now when the ball hits the left or right side of the play area, it will destroy and respawn. Great!