6.1 KiB
title | date | weight |
---|---|---|
Motion Engine | 2019-05-23T13:03:39-07:00 | 5 |
To create an Engine, we extend the Engine class.
Create a file: PongFE/Engines/MotionEngine.cs
using Encompass;
public class MotionEngine : Engine
{
public override void Update(double dt)
{
}
}
Every Engine must implement an Update method, which takes a delta-time value as a parameter.
delta-time is simply the time that has elapsed between the last frame and the current one in seconds. We'll talk more about why this is important in a minute.
Let's think for a minute about what we want this Engine to actually do. Motion is just the change of position over time, right? So our MotionEngine is going to modify PositionComponents based on some amount of movement.
We're gonna need a Message.
Create a file: PongFE/Messages/MotionMessage.cs
using System.Numerics;
using Encompass;
namespace PongFE.Messages
{
public struct MotionMessage : IMessage, IHasEntity
{
public Entity Entity { get; }
public Vector2 Movement { get; }
public MotionMessage(Entity entity, Vector2 movement)
{
Entity = entity;
Movement = movement;
}
}
}
Similar to a component, a message is a struct which implements the IMessage interface. Also, motion is something that refers to a specific object, right? So we want our message to have a reference to an entity. We can declare the IHasEntity interface on our message, which allows Encompass to perform certain lookup optimizations. We'll talk about that in a second.
{{% notice warning %}} Don't ever have a Message that refers to another Message. That is very bad. {{% /notice %}}
Now, how is our MotionEngine going to interact with MotionMessages? It's going to Read them.
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
}
}
}
}
If we run the game right now, Encompass will yell at us. Why? Because it can't guarantee that this Engine runs after Engines which send MotionMessages, which is no good. So we need to declare what is called a class attribute on the Engine. We'll talk about sending messages soon. But for now, we need to let Encompass know that this Engine will be receiving MotionMessages with a Receives attribute.
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Receives(typeof(MotionMessage))]
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
}
}
}
}
Now the program will run without error. Let's use the MotionMessages to update PositionComponents.
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Receives(typeof(MotionMessage))]
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
if (HasComponent<PositionComponent>(motionMessage.Entity))
{
ref readonly var positionComponent = ref GetComponent<PositionComponent>(motionMessage.Entity);
var newPosition = positionComponent.Position + motionMessage.Movement;
SetComponent(motionMessage.Entity, new PositionComponent(newPosition));
}
}
}
}
}
We have a couple of new methods showing up here. The first is HasComponent. This method simply returns true if the given Entity has a component of the given type, and false otherwise. It's usually a good idea to call this method before using GetComponent unless we are in a case where we already know for sure that the Entity will have that component type.
Next we have SetComponent. Remember that we can't just change component values wherever we want, because GetComponent returns readonly references. This is for our own good! Requiring explicit component updates prevents you from shooting yourself in the foot by changing values in unexpected places. Every component update is a place for horrible nasty bugs to lurk in our game, so we want to take care and be smart about when we do it.
Each Entity can only have a single component of each type. So calling SetComponent will overwrite the component, or just attach the component if one of that type didn't exist on the entity beforehand.
Finally, if we're going to be reading and changing PositionComponents, the Engine needs to declare that it Reads and Writes them.
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(typeof(PositionComponent))]
[Receives(typeof(MotionMessage))]
[Writes(typeof(PositionComponent))]
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
if (HasComponent<PositionComponent>(motionMessage.Entity))
{
ref readonly var positionComponent = ref GetComponent<PositionComponent>(motionMessage.Entity);
var newPosition = positionComponent.Position + motionMessage.Movement;
SetComponent(motionMessage.Entity, new PositionComponent(newPosition));
}
}
}
}
}
Before we move on any farther, let's make sure to add this Engine to our WorldBuilder.
...
WorldBuilder.AddEngine(new MotionEngine());
WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));
...
Of course, if we run the game now, nothing will happen, because nothing is actually sending out MotionMessages. Let's make that happen.