232 lines
7.4 KiB
Markdown
232 lines
7.4 KiB
Markdown
---
|
|
title: "Motion Engine: The Revenge"
|
|
date: 2019-05-28T18:01:49-07:00
|
|
weight: 500
|
|
---
|
|
|
|
Before we begin you might want to skim the docs for 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.
|
|
|
|
First, we add entities with CollisionComponents to the SpatialHash.
|
|
|
|
Next, we consolidate MotionMessages by their entities to get a total movement value for each entity.
|
|
|
|
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 overlaps, we dispatch a CollisionMessage.
|
|
|
|
First let's create a CollisionComponent. In **PongFE/Components/CollisionComponent.cs**:
|
|
|
|
```cs
|
|
using Encompass;
|
|
using MoonTools.Bonk;
|
|
|
|
namespace PongFE.Components
|
|
{
|
|
public struct CollisionComponent : IComponent
|
|
{
|
|
public Rectangle Rectangle { get; }
|
|
|
|
public CollisionComponent(Rectangle rectangle)
|
|
{
|
|
Rectangle = rectangle;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
This is pretty straightforward. We just pass in a Bonk Rectangle.
|
|
|
|
Let's rewrite our MotionEngine.
|
|
|
|
First, let's create a Bonk SpatialHash. Every frame we will empty the hash and re-add all relevant entities.
|
|
|
|
In **PongFE/Engines/MotionEngine.cs**:
|
|
|
|
```cs
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using Encompass;
|
|
using MoonTools.Bonk;
|
|
using MoonTools.Structs;
|
|
using PongFE.Components;
|
|
using PongFE.Messages;
|
|
|
|
namespace PongFE.Engines
|
|
{
|
|
[QueryWith(typeof(PositionComponent), typeof(CollisionComponent))]
|
|
public class MotionEngine : Engine
|
|
{
|
|
private readonly SpatialHash<Entity> _spatialHash = new SpatialHash<Entity>(32);
|
|
|
|
public override void Update(double dt)
|
|
{
|
|
_spatialHash.Clear();
|
|
|
|
foreach (var entity in TrackedEntities)
|
|
{
|
|
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
|
|
ref readonly var collisionComponent = ref GetComponent<CollisionComponent>(entity);
|
|
|
|
_spatialHash.Insert(entity, collisionComponent.Rectangle, new Transform2D(positionComponent.Position));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
...
|
|
```
|
|
|
|
This is where our *IHasEntity* optimization comes in - it allows us to use the **ReadMessagesWithEntity** method.
|
|
|
|
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);
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
Now that we have a mechanism for detecting sweep hits, we can send out our CollisionMessage and UpdatePositionMessage.
|
|
|
|
In **PongFE/Messages/CollisionMessage.cs**
|
|
|
|
```cs
|
|
using Encompass;
|
|
|
|
namespace PongFE.Messages
|
|
{
|
|
public enum HitOrientation
|
|
{
|
|
Horizontal,
|
|
Vertical
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
It is useful to differentiate between a horizontal hit and and a vertical hit, so we set up an enum to track that.
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
This is pretty straightforward. We'll use this message to set an entity's position.
|
|
|
|
```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));
|
|
}
|
|
|
|
...
|
|
```
|
|
|
|
Putting it all together. We go over everything with a Position and Collision component, sweep test for collisions, and send appropriate Collision and UpdatePosition messages accordingly.
|
|
|
|
Now let's handle those collision messages.
|