2019-05-29 04:05:36 +00:00
---
title: "Motion Engine: The Revenge"
date: 2019-05-28T18:01:49-07:00
weight: 500
---
2020-07-21 22:21:40 +00:00
Before we begin you might want to skim the [docs for Bonk ](http://moonside.games/docs/bonk/ ). But the short version of what you need to know is that SpatialHash is a structure that lets us do fast, inaccurate checks to quickly eliminate potential collision checks.
2019-05-31 17:32:34 +00:00
2020-07-14 23:02:50 +00:00
First, we add entities with CollisionComponents to the SpatialHash.
2019-05-31 17:32:34 +00:00
2020-07-14 23:02:50 +00:00
Next, we consolidate MotionMessages by their entities to get a total movement value for each entity.
2019-05-29 04:24:48 +00:00
2020-07-21 22:27:21 +00:00
Finally, we go over all entities with a PositionComponent and CollisionComponent, and sweep over the distance it has moved to check for collisions. If any entity would overlap with another, we adjust its movement to prevent it from overlapping and dispatch a CollisionMessage.
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
First let's create a CollisionComponent. In **PongFE/Components/CollisionComponent.cs** :
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
```cs
using Encompass;
using MoonTools.Bonk;
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
namespace PongFE.Components
{
public struct CollisionComponent : IComponent
{
public Rectangle Rectangle { get; }
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
public CollisionComponent(Rectangle rectangle)
{
Rectangle = rectangle;
}
}
2019-05-29 04:05:36 +00:00
}
```
2020-07-14 23:02:50 +00:00
This is pretty straightforward. We just pass in a Bonk Rectangle.
2019-05-31 17:32:34 +00:00
Let's rewrite our MotionEngine.
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
First, let's create a Bonk SpatialHash. Every frame we will empty the hash and re-add all relevant entities.
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
In **PongFE/Engines/MotionEngine.cs** :
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
```cs
using System.Collections.Generic;
using System.Numerics;
using Encompass;
using MoonTools.Bonk;
using MoonTools.Structs;
using PongFE.Components;
using PongFE.Messages;
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
namespace PongFE.Engines
{
[QueryWith(typeof(PositionComponent), typeof(CollisionComponent))]
public class MotionEngine : Engine
{
private readonly SpatialHash< Entity > _spatialHash = new SpatialHash< Entity > (32);
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
public override void Update(double dt)
{
_spatialHash.Clear();
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
foreach (var entity in TrackedEntities)
{
ref readonly var positionComponent = ref GetComponent< PositionComponent > (entity);
ref readonly var collisionComponent = ref GetComponent< CollisionComponent > (entity);
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
_spatialHash.Insert(entity, collisionComponent.Rectangle, new Transform2D(positionComponent.Position));
2019-05-29 04:05:36 +00:00
}
}
2020-07-14 23:02:50 +00:00
}
}
```
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
Next, let's consolidate our MotionMessages per Entity.
```cs
...
private readonly Dictionary< Entity , Vector2 > _moveAmounts = new Dictionary< Entity , Vector2 > ();
...
foreach (ref readonly var entity in ReadEntities< PositionComponent > ())
{
ref readonly var positionComponent = ref GetComponent< PositionComponent > (entity);
_moveAmounts[entity] = Vector2.Zero;
foreach (var motionMessage in ReadMessagesWithEntity< MotionMessage > (entity))
{
_moveAmounts[entity] += motionMessage.Movement;
2019-05-29 04:05:36 +00:00
}
2020-07-14 23:02:50 +00:00
}
...
```
2020-07-21 22:27:21 +00:00
This is where our *IHasEntity* optimization comes in. It allows us to use the **ReadMessagesWithEntity** method.
2020-07-14 23:02:50 +00:00
Finally, let's implement our sweep test.
```cs
private (bool, bool, Position2D, Entity) SolidCollisionPosition(Rectangle rectangle, Position2D startPosition, Position2D endPosition)
{
var startX = startPosition.X;
var endX = endPosition.X;
var startY = startPosition.Y;
var endY = endPosition.Y;
bool xHit, yHit;
int xPosition, yPosition;
Entity xCollisionEntity, yCollisionEntity;
(xHit, xPosition, xCollisionEntity) = SweepX(_spatialHash, rectangle, Position2D.Zero, new Position2D(startX, startY), endX - startX);
if (!xHit) { xPosition = endX; }
(yHit, yPosition, yCollisionEntity) = SweepY(_spatialHash, rectangle, Position2D.Zero, new Position2D(xPosition, startY), endY - startY);
return (xHit, yHit, new Position2D(xPosition, yPosition), xHit ? xCollisionEntity : yCollisionEntity);
}
private (bool, int, Entity) SweepX(SpatialHash< Entity > solidSpatialHash, Rectangle rectangle, Position2D offset, Position2D startPosition, int horizontalMovement)
{
var sweepResult = SweepTest.Test(solidSpatialHash, rectangle, new Transform2D(offset + startPosition), new Vector2(horizontalMovement, 0));
return (sweepResult.Hit, startPosition.X + (int)sweepResult.Motion.X, sweepResult.ID);
}
public static (bool, int, Entity) SweepY(SpatialHash< Entity > solidSpatialHash, Rectangle rectangle, Position2D offset, Position2D startPosition, int verticalMovement)
{
var sweepResult = SweepTest.Test(solidSpatialHash, rectangle, new Transform2D(offset + startPosition), new Vector2(0, verticalMovement));
return (sweepResult.Hit, startPosition.Y + (int)sweepResult.Motion.Y, sweepResult.ID);
}
```
2020-07-21 22:27:21 +00:00
Here we use Bonk's **SweepTest** functionality. A sweep test moves a rectangle along a vector, checking for collisions along the movement of the sweep. First we sweep in a horizontal direction, and then in a vertical direction, returning the positions where collisions occurred. This means that objects won't awkwardly stop in place when they touch something.
2020-07-14 23:02:50 +00:00
2020-07-21 22:27:21 +00:00
{{% notice note %}}
In the future you might want to change how the sweep tests resolve a position, and this would certainly be possible using this system. You could have different components like **SlideOnCollisionComponent** or **StickOnCollisionComponent** , for example, that would affect the returned sweep position.
{{% /notice %}}
Now that we have a mechanism for detecting sweep hits, we can send out a CollisionMessage and UpdatePositionMessage.
2020-07-14 23:02:50 +00:00
In **PongFE/Messages/CollisionMessage.cs**
```cs
using Encompass;
namespace PongFE.Messages
{
public enum HitOrientation
{
Horizontal,
Vertical
}
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
public struct CollisionMessage : IMessage
{
public Entity EntityA { get; }
public Entity EntityB { get; }
public HitOrientation HitOrientation;
public CollisionMessage(Entity a, Entity b, HitOrientation hitOrientation)
{
EntityA = a;
EntityB = b;
HitOrientation = hitOrientation;
}
2019-05-29 04:05:36 +00:00
}
2020-07-14 23:02:50 +00:00
}
```
It is useful to differentiate between a horizontal hit and and a vertical hit, so we set up an enum to track that.
2019-05-29 04:05:36 +00:00
2020-07-14 23:02:50 +00:00
And in **PongFE/Messages/UpdatePositionMessage.cs**
```cs
using Encompass;
using MoonTools.Structs;
namespace PongFE.Messages
{
public struct UpdatePositionMessage : IMessage, IHasEntity
{
public Entity Entity { get; }
public Position2D Position { get; }
public UpdatePositionMessage(Entity entity, Position2D position)
{
Entity = entity;
Position = position;
2019-05-29 04:05:36 +00:00
}
}
}
```
2020-07-14 23:02:50 +00:00
This is pretty straightforward. We'll use this message to set an entity's position.
2020-07-21 22:27:21 +00:00
Now we can send these messages in **MotionEngine** .
2020-07-14 23:02:50 +00:00
```cs
...
foreach (var entity in TrackedEntities)
{
Vector2 moveAmount = _moveAmounts[entity];
ref readonly var positionComponent = ref GetComponent< PositionComponent > (entity);
var projectedPosition = positionComponent.Position + moveAmount;
ref readonly var collisionComponent = ref GetComponent< CollisionComponent > (entity);
var rectangle = collisionComponent.Rectangle;
var (xHit, yHit, newPosition, collisionEntity) = SolidCollisionPosition(rectangle, positionComponent.Position, projectedPosition);
if (xHit || yHit)
{
projectedPosition = newPosition;
if (xHit)
{
SendMessage(new CollisionMessage(entity, collisionEntity, HitOrientation.Horizontal));
}
else
{
SendMessage(new CollisionMessage(entity, collisionEntity, HitOrientation.Vertical));
}
}
SendMessage(new UpdatePositionMessage(entity, projectedPosition));
}
...
```
2020-07-21 22:27:21 +00:00
Here, we go over everything with a Position and Collision component, sweep test for collisions, and send appropriate Collision and UpdatePosition messages accordingly.
2020-07-14 23:02:50 +00:00
Now let's handle those collision messages.