more collision stuff
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
0f715d3451
commit
6d1e3793ef
|
@ -1,142 +0,0 @@
|
||||||
---
|
|
||||||
title: "Collision Checking"
|
|
||||||
date: 2019-05-28T18:51:15-07:00
|
|
||||||
weight: 600
|
|
||||||
---
|
|
||||||
|
|
||||||
In **game/engines/collision_message.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Entity, Message } from "encompass-ecs";
|
|
||||||
import { CollisionType } from "game/components/collision_types";
|
|
||||||
import { Collision } from "lua-lib/bump";
|
|
||||||
|
|
||||||
export class CollisionMessage extends Message {
|
|
||||||
public entity_one: Entity;
|
|
||||||
public entity_two: Entity;
|
|
||||||
public collision_type_one: CollisionType;
|
|
||||||
public collision_type_two: CollisionType;
|
|
||||||
public entity_one_new_x: number;
|
|
||||||
public entity_one_new_y: number;
|
|
||||||
public entity_two_new_x: number;
|
|
||||||
public entity_two_new_y: number;
|
|
||||||
public collision_data: Collision;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's break down what we want collision detection to actually do.
|
|
||||||
|
|
||||||
First, we tell the Collision World about the current positions of the objects. Next we check each object for collisions by using the "check" method, which takes the proposed new position of the object and gives us collision information in return.
|
|
||||||
|
|
||||||
For every collision that we find, we create a CollisionMessage for it.
|
|
||||||
|
|
||||||
In **game/engines/collision_check.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Emits, Engine, Entity, Reads } from "encompass-ecs";
|
|
||||||
import { BoundingBoxComponent } from "game/components/bounding_box";
|
|
||||||
import { CollisionTypesComponent } from "game/components/collision_types";
|
|
||||||
import { PositionComponent } from "game/components/position";
|
|
||||||
import { CollisionMessage } from "game/messages/collision";
|
|
||||||
import { CollisionCheckMessage } from "game/messages/collision_check";
|
|
||||||
import { World } from "lua-lib/bump";
|
|
||||||
|
|
||||||
@Reads(CollisionCheckMessage)
|
|
||||||
@Emits(CollisionMessage)
|
|
||||||
export class CollisionCheckEngine extends Engine {
|
|
||||||
private collision_world: World;
|
|
||||||
|
|
||||||
public initialize(collision_world: World) {
|
|
||||||
this.collision_world = collision_world;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update() {
|
|
||||||
const collision_check_messages = this.read_messages(CollisionCheckMessage);
|
|
||||||
|
|
||||||
// update all positions in collision world
|
|
||||||
for (const message of collision_check_messages.values()) {
|
|
||||||
const entity = message.entity;
|
|
||||||
const position = entity.get_component(PositionComponent);
|
|
||||||
const bounding_box = entity.get_component(BoundingBoxComponent);
|
|
||||||
|
|
||||||
this.collision_world.update(
|
|
||||||
message.entity,
|
|
||||||
position.x - bounding_box.width * 0.5,
|
|
||||||
position.y - bounding_box.height * 0.5
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// perform collision checks with new positions
|
|
||||||
for (const message of collision_check_messages.values()) {
|
|
||||||
const entity = message.entity;
|
|
||||||
const position = entity.get_component(PositionComponent);
|
|
||||||
const bounding_box = entity.get_component(BoundingBoxComponent);
|
|
||||||
const x = position.x + message.x_delta;
|
|
||||||
const y = position.y + message.y_delta;
|
|
||||||
|
|
||||||
const [new_x, new_y, cols, len] = this.collision_world.check(
|
|
||||||
entity,
|
|
||||||
x - bounding_box.width * 0.5,
|
|
||||||
y - bounding_box.height * 0.5,
|
|
||||||
() => "touch"
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const col of cols) {
|
|
||||||
const other = col.other as Entity;
|
|
||||||
const other_position = other.get_component(PositionComponent);
|
|
||||||
|
|
||||||
for (const collision_type_one of entity.get_component(CollisionTypesComponent)!.collision_types) {
|
|
||||||
for (const collision_type_two of other.get_component(CollisionTypesComponent)!.collision_types) {
|
|
||||||
const collision_message = this.emit_message(CollisionMessage);
|
|
||||||
if (collision_type_one < collision_type_two) {
|
|
||||||
collision_message.entity_one = entity;
|
|
||||||
collision_message.entity_two = other;
|
|
||||||
collision_message.collision_type_one = collision_type_one;
|
|
||||||
collision_message.collision_type_two = collision_type_two;
|
|
||||||
collision_message.entity_one_new_x = x;
|
|
||||||
collision_message.entity_one_new_y = y;
|
|
||||||
collision_message.entity_two_new_x = other_position.x;
|
|
||||||
collision_message.entity_two_new_y = other_position.y;
|
|
||||||
collision_message.collision_data = col;
|
|
||||||
} else {
|
|
||||||
collision_message.entity_one = other;
|
|
||||||
collision_message.entity_two = entity;
|
|
||||||
collision_message.collision_type_one = collision_type_two;
|
|
||||||
collision_message.collision_type_two = collision_type_one;
|
|
||||||
collision_message.entity_one_new_x = other_position.x;
|
|
||||||
collision_message.entity_one_new_y = other_position.y;
|
|
||||||
collision_message.entity_two_new_x = x;
|
|
||||||
collision_message.entity_two_new_y = y;
|
|
||||||
collision_message.collision_data = col;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Why are we comparing the collision types? Let's say we want to have a BallWallCollisionMessage. Obviously we will want to know which entity in the collision represents the ball and which one represents the wall. So we just sort them at this step for convenience.
|
|
||||||
|
|
||||||
Let's make sure that our enum is sorted in alphabetical order.
|
|
||||||
|
|
||||||
In **game/components/collision_types.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export enum CollisionType {
|
|
||||||
ball,
|
|
||||||
paddle,
|
|
||||||
wall,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The "initialize" method gives the Engine a reference to the Collision World that needs to be shared by everything that deals with collision. Let's make sure to call the "initialize" method from **game.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const collision_world = CollisionWorld.newWorld(32);
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
world_builder.add_engine(CollisionCheckEngine).initialize(collision_world);
|
|
||||||
```
|
|
|
@ -4,65 +4,65 @@ date: 2019-05-28T19:06:03-07:00
|
||||||
weight: 700
|
weight: 700
|
||||||
---
|
---
|
||||||
|
|
||||||
Let's make the CollisionDispatchEngine. All it needs to do is read the CollisionMessages and create specific collision messages from them.
|
Let's make the CollisionEngine. All it needs to do is read the CollisionMessages, determine what, if any, kind of collision occurred, and create more specific collision messages from them.
|
||||||
|
|
||||||
In **games/engines/collision_dispatch.ts**:
|
Right now the only collision responses we care about are bounces.
|
||||||
|
|
||||||
```ts
|
Let's create our Actor, **PongFE/Components/CanCauseBounceComponent.cs**:
|
||||||
import { Emits, Engine, Reads } from "encompass-ecs";
|
|
||||||
import { CollisionType } from "game/components/collision_types";
|
|
||||||
import { CollisionMessage } from "game/messages/collision";
|
|
||||||
import { BallPaddleCollisionMessage } from "game/messages/collisions/ball_paddle";
|
|
||||||
import { BallWallCollisionMessage } from "game/messages/collisions/ball_wall";
|
|
||||||
import { PaddleWallCollisionMessage } from "game/messages/collisions/paddle_wall";
|
|
||||||
|
|
||||||
@Reads(CollisionMessage)
|
```cs
|
||||||
@Emits(BallPaddleCollisionMessage, BallWallCollisionMessage, PaddleWallCollisionMessage)
|
using Encompass;
|
||||||
export class CollisionDispatchEngine extends Engine {
|
|
||||||
public update() {
|
|
||||||
const collision_messages = this.read_messages(CollisionMessage);
|
|
||||||
|
|
||||||
for (const collision_message of collision_messages.values()) {
|
namespace PongFE.Components
|
||||||
switch (collision_message.collision_type_one) {
|
{
|
||||||
case CollisionType.ball:
|
public struct CanCauseBounceComponent : IComponent { }
|
||||||
switch (collision_message.collision_type_two) {
|
}
|
||||||
case CollisionType.paddle: {
|
```
|
||||||
const message = this.emit_message(BallPaddleCollisionMessage);
|
|
||||||
message.ball_entity = collision_message.entity_one;
|
|
||||||
message.paddle_entity = collision_message.entity_two;
|
|
||||||
message.ball_new_x = collision_message.entity_one_new_x;
|
|
||||||
message.ball_new_y = collision_message.entity_one_new_y;
|
|
||||||
message.normal = collision_message.collision_data.normal;
|
|
||||||
message.touch = collision_message.collision_data.touch;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case CollisionType.wall: {
|
And our Receiver, **PongFE/Components/CanBeBouncedComponent.cs**:
|
||||||
const message = this.emit_message(BallWallCollisionMessage);
|
|
||||||
message.ball_entity = collision_message.entity_one;
|
```cs
|
||||||
message.wall_entity = collision_message.entity_two;
|
using Encompass;
|
||||||
message.ball_new_x = collision_message.entity_one_new_x;
|
|
||||||
message.ball_new_y = collision_message.entity_one_new_y;
|
namespace PongFE.Components
|
||||||
message.normal = collision_message.collision_data.normal;
|
{
|
||||||
message.touch = collision_message.collision_data.touch;
|
public struct CanBeBouncedComponent : IComponent { }
|
||||||
break;
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In **PongFE/Engines/CollisionEngine.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
|
namespace PongFE.Engines
|
||||||
|
{
|
||||||
|
[Reads(
|
||||||
|
typeof(CanCauseBounceComponent),
|
||||||
|
typeof(CanBeBouncedComponent)
|
||||||
|
)]
|
||||||
|
[Receives(typeof(CollisionMessage))]
|
||||||
|
[Sends(typeof(BounceMessage))]
|
||||||
|
public class CollisionEngine : Engine
|
||||||
|
{
|
||||||
|
public override void Update(double dt)
|
||||||
|
{
|
||||||
|
foreach (ref readonly var message in ReadMessages<CollisionMessage>())
|
||||||
|
{
|
||||||
|
CheckBounce(message.EntityA, message.EntityB, message.HitOrientation);
|
||||||
|
CheckBounce(message.EntityB, message.EntityA, message.HitOrientation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case CollisionType.paddle: {
|
private void CheckBounce(Entity a, Entity b, HitOrientation hitOrientation)
|
||||||
switch (collision_message.collision_type_two) {
|
{
|
||||||
case CollisionType.wall: {
|
if (HasComponent<CanCauseBounceComponent>(a))
|
||||||
const message = this.emit_message(PaddleWallCollisionMessage);
|
{
|
||||||
message.paddle_entity = collision_message.entity_one;
|
if (HasComponent<CanBeBouncedComponent>(b))
|
||||||
message.paddle_new_x = collision_message.entity_one_new_x;
|
{
|
||||||
message.paddle_new_y = collision_message.entity_one_new_y;
|
SendMessage(new BounceMessage(b, hitOrientation));
|
||||||
message.wall_entity = collision_message.entity_two;
|
|
||||||
message.normal = collision_message.collision_data.normal;
|
|
||||||
message.touch = collision_message.collision_data.touch;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,16 +72,10 @@ export class CollisionDispatchEngine extends Engine {
|
||||||
|
|
||||||
Now we are emitting proper collision messages every time an entity collides with another.
|
Now we are emitting proper collision messages every time an entity collides with another.
|
||||||
|
|
||||||
Don't forget to add our new engine in **game.ts**
|
Don't forget to add our new engine in **PongFEGame.cs**
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
world_builder.add_engine(CollisionDispatchEngine);
|
WorldBuilder.AddEngine(new CollisionEngine());
|
||||||
```
|
```
|
||||||
|
|
||||||
{{% notice notice %}}
|
Next, we'll make our game actually do something in response to the Bounce message.
|
||||||
Clever readers have probably noticed that this is a bit of an awkward structure. For our game, we only have three types of colliding entities we care about, so some switch statements work fine. What about a game with 20 different kinds of colliding entities? 100? We'd probably want a much more generic structure or this Engine's complexity would get out of hand.
|
|
||||||
|
|
||||||
What you really want to do, fundamentally, is map two collision types, independent of order, to a message emitting function. You'll probably need to implement a custom data structure to do this cleanly. It's very much outside of the scope of this tutorial for me to do this, but I wish you luck!
|
|
||||||
{{% /notice %}}
|
|
||||||
|
|
||||||
Next, we'll make our game actually do things in response to these messages.
|
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
---
|
|
||||||
title: "Collision Resolution"
|
|
||||||
date: 2019-05-28T20:39:54-07:00
|
|
||||||
weight: 800
|
|
||||||
---
|
|
||||||
|
|
||||||
What do we want to actually happen when a ball collides with a wall?
|
|
||||||
|
|
||||||
Obviously the wall doesn't do anything. It just sits there. That's easy!
|
|
||||||
|
|
||||||
The ball needs to bounce off of the wall. We can calculate exactly where it should end up by adding distance along the collision normal equal to twice the difference between the proposed location of the ball and where it touched the wall.
|
|
||||||
|
|
||||||
{{% notice tip %}}
|
|
||||||
**What the heck is a collision normal?**
|
|
||||||
|
|
||||||
You can think of the collision normal as just an arrow pointing away from the wall. If you want more details about this, check out the [bump.lua README](https://github.com/kikito/bump.lua/blob/master/README.md). It has illustrations of collisions and the normal vectors they create.
|
|
||||||
{{% /notice %}}
|
|
||||||
|
|
||||||
In **game/engines/collision/ball_wall.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Emits, Engine, Reads } from "encompass-ecs";
|
|
||||||
import { BoundingBoxComponent } from "game/components/bounding_box";
|
|
||||||
import { PositionComponent } from "game/components/position";
|
|
||||||
import { VelocityComponent } from "game/components/velocity";
|
|
||||||
import { BallWallCollisionMessage } from "game/messages/collisions/ball_wall";
|
|
||||||
import { UpdatePositionMessage } from "game/messages/update_position";
|
|
||||||
import { UpdateVelocityMessage } from "game/messages/update_velocity";
|
|
||||||
|
|
||||||
@Reads(BallWallCollisionMessage)
|
|
||||||
@Emits(UpdatePositionMessage, UpdateVelocityMessage)
|
|
||||||
export class BallWallCollisionEngine extends Engine {
|
|
||||||
public update() {
|
|
||||||
for (const message of this.read_messages(BallWallCollisionMessage).values()) {
|
|
||||||
const ball_position = message.ball_entity.get_component(PositionComponent);
|
|
||||||
const ball_velocity = message.ball_entity.get_component(VelocityComponent);
|
|
||||||
const ball_bounding_box = message.ball_entity.get_component(BoundingBoxComponent);
|
|
||||||
|
|
||||||
const velocity_message = this.emit_component_message(UpdateVelocityMessage, ball_velocity);
|
|
||||||
velocity_message.x_delta = 2 * message.normal.x * Math.abs(ball_velocity.x);
|
|
||||||
velocity_message.y_delta = 2 * message.normal.y * Math.abs(ball_velocity.y);
|
|
||||||
|
|
||||||
// calculate bounce, remembering to re-transform coordinates to origin space
|
|
||||||
const y_distance = Math.abs(message.ball_new_y - (message.touch.y + ball_bounding_box.height * 0.5));
|
|
||||||
const x_distance = Math.abs(message.ball_new_x - (message.touch.x + ball_bounding_box.width * 0.5));
|
|
||||||
|
|
||||||
const position_message = this.emit_component_message(UpdatePositionMessage, ball_position);
|
|
||||||
position_message.x_delta = 2 * message.normal.x * x_distance;
|
|
||||||
position_message.y_delta = 2 * message.normal.y * y_distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notice that we also want to update the velocity when the ball bounces. Let's create that UpdateVelocity behavior.
|
|
||||||
|
|
||||||
In **game/messages/update_velocity.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { ComponentMessage, Message } from "encompass-ecs";
|
|
||||||
import { VelocityComponent } from "game/components/velocity";
|
|
||||||
|
|
||||||
export class UpdateVelocityMessage extends Message implements ComponentMessage {
|
|
||||||
public component: Readonly<VelocityComponent>;
|
|
||||||
public x_delta: number;
|
|
||||||
public y_delta: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In **game/engines/update_velocity.ts**:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { ComponentModifier, Mutates, Reads } from "encompass-ecs";
|
|
||||||
import { VelocityComponent } from "game/components/velocity";
|
|
||||||
import { UpdateVelocityMessage } from "game/messages/update_velocity";
|
|
||||||
import { GCOptimizedSet } from "encompass-gc-optimized-collections";
|
|
||||||
|
|
||||||
@Reads(UpdateVelocityMessage)
|
|
||||||
@Mutates(VelocityComponent)
|
|
||||||
export class UpdateVelocityEngine extends ComponentModifier {
|
|
||||||
public component_message_type = UpdateVelocityMessage;
|
|
||||||
|
|
||||||
public modify(component: VelocityComponent, messages: GCOptimizedSet<UpdateVelocityMessage>) {
|
|
||||||
for (const message of messages.entries()) {
|
|
||||||
component.x += message.x_delta;
|
|
||||||
component.y += message.y_delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Our BallPaddleCollisionEngine will behave the exact same way. Why don't you try to fill it in yourself?
|
|
||||||
|
|
||||||
Finally, we want to make sure our paddles don't go past the game boundary.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Emits, Engine, Reads } from "encompass-ecs";
|
|
||||||
import { BoundingBoxComponent } from "game/components/bounding_box";
|
|
||||||
import { PositionComponent } from "game/components/position";
|
|
||||||
import { PaddleWallCollisionMessage } from "game/messages/collisions/paddle_wall";
|
|
||||||
import { UpdatePositionMessage } from "game/messages/update_position";
|
|
||||||
|
|
||||||
@Reads(PaddleWallCollisionMessage)
|
|
||||||
@Emits(UpdatePositionMessage)
|
|
||||||
export class PaddleWallCollisionEngine extends Engine {
|
|
||||||
public update() {
|
|
||||||
for (const message of this.read_messages(PaddleWallCollisionMessage).values()) {
|
|
||||||
const paddle_position = message.paddle_entity.get_component(PositionComponent);
|
|
||||||
const paddle_bounding_box = message.paddle_entity.get_component(BoundingBoxComponent);
|
|
||||||
|
|
||||||
const x_distance = Math.abs(message.paddle_new_x - (message.touch.x + paddle_bounding_box.width * 0.5));
|
|
||||||
const y_distance = Math.abs(message.paddle_new_y - (message.touch.y + paddle_bounding_box.height * 0.5));
|
|
||||||
|
|
||||||
const position_message = this.emit_component_message(UpdatePositionMessage, paddle_position);
|
|
||||||
position_message.x_delta = message.normal.x * x_distance;
|
|
||||||
position_message.y_delta = message.normal.y * y_distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it for defining our collision behavior!
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
---
|
||||||
|
title: "Collision Response"
|
||||||
|
date: 2019-05-28T20:39:54-07:00
|
||||||
|
weight: 800
|
||||||
|
---
|
||||||
|
|
||||||
|
Now the ball needs to bounce off of the paddle. What does that mean? If there is a horizontal collision, we reverse the horizontal velocity. Otherwise we reverse the vertical velocity.
|
||||||
|
|
||||||
|
In **PongFE/Engines/BounceEngine.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using System.Numerics;
|
||||||
|
using Encompass;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
|
namespace PongFE.Engines
|
||||||
|
{
|
||||||
|
[Reads(
|
||||||
|
typeof(BounceResponseComponent),
|
||||||
|
typeof(VelocityComponent)
|
||||||
|
)]
|
||||||
|
[Receives(typeof(BounceMessage))]
|
||||||
|
[Sends(typeof(UpdateVelocityMessage))]
|
||||||
|
public class BounceEngine : Engine
|
||||||
|
{
|
||||||
|
public override void Update(double dt)
|
||||||
|
{
|
||||||
|
foreach (ref readonly var message in ReadMessages<BounceMessage>())
|
||||||
|
{
|
||||||
|
if (HasComponent<BounceResponseComponent>(message.Entity) && HasComponent<VelocityComponent>(message.Entity))
|
||||||
|
{
|
||||||
|
ref readonly var velocityComponent = ref GetComponent<VelocityComponent>(message.Entity);
|
||||||
|
|
||||||
|
Vector2 newVelocity;
|
||||||
|
if (message.HitOrientation == HitOrientation.Horizontal)
|
||||||
|
{
|
||||||
|
newVelocity =
|
||||||
|
new Vector2(-velocityComponent.Velocity.X, velocityComponent.Velocity.Y);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newVelocity =
|
||||||
|
new Vector2(velocityComponent.Velocity.X, -velocityComponent.Velocity.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
SendMessage(new UpdateVelocityMessage(message.Entity, newVelocity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's create that UpdateVelocity behavior.
|
||||||
|
|
||||||
|
In **PongFE/Messages/UpdateVelocityMessage.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using System.Numerics;
|
||||||
|
using Encompass;
|
||||||
|
|
||||||
|
namespace PongFE.Messages
|
||||||
|
{
|
||||||
|
public struct UpdateVelocityMessage : IMessage, IHasEntity
|
||||||
|
{
|
||||||
|
public Entity Entity { get; }
|
||||||
|
public Vector2 Velocity { get; }
|
||||||
|
|
||||||
|
public UpdateVelocityMessage(Entity entity, Vector2 velocity)
|
||||||
|
{
|
||||||
|
Entity = entity;
|
||||||
|
Velocity = velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In **PongFE/Engines/UpdateVelocityEngine.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
|
namespace PongFE.Engines
|
||||||
|
{
|
||||||
|
[Receives(typeof(UpdateVelocityMessage))]
|
||||||
|
[Writes(typeof(VelocityComponent))]
|
||||||
|
public class UpdateVelocityEngine : Engine
|
||||||
|
{
|
||||||
|
public override void Update(double dt)
|
||||||
|
{
|
||||||
|
foreach (ref readonly var message in ReadMessages<UpdateVelocityMessage>())
|
||||||
|
{
|
||||||
|
SetComponent(message.Entity, new VelocityComponent(message.Velocity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In **PongFEGame.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
WorldBuilder.AddEngine(new BounceEngine());
|
||||||
|
WorldBuilder.AddEngine(new UpdateVelocityEngine());
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it for defining our collision behavior!
|
|
@ -4,104 +4,228 @@ date: 2019-05-28T18:01:49-07:00
|
||||||
weight: 500
|
weight: 500
|
||||||
---
|
---
|
||||||
|
|
||||||
Here's the process we'll follow for our MotionEngine:
|
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.
|
||||||
|
|
||||||
We associate MotionMessages with their PositionComponents. We consolidate them to get a total "x_delta" and a "y_delta". We create an UpdatePositionMessage containing these values. Next, we create CollisionCheckMessages containing the delta values if the PositionComponent's entity has a BoundingBoxComponent.
|
First, we add entities with CollisionComponents to the SpatialHash.
|
||||||
|
|
||||||
Finally, we go over all BoundingBoxComponents that didn't have MotionMessages associated with them and create CollisionCheckMessages for those too. Otherwise things that didn't move wouldn't be collision checked, and that would not be correct.
|
Next, we consolidate MotionMessages by their entities to get a total movement value for each entity.
|
||||||
|
|
||||||
In **game/messages/collision_check.ts**:
|
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.
|
||||||
|
|
||||||
```ts
|
First let's create a CollisionComponent. In **PongFE/Components/CollisionComponent.cs**:
|
||||||
import { Entity, Message } from "encompass-ecs";
|
|
||||||
|
|
||||||
export class CollisionCheckMessage extends Message {
|
```cs
|
||||||
public entity: Entity;
|
using Encompass;
|
||||||
public x_delta: number;
|
using MoonTools.Bonk;
|
||||||
public y_delta: number;
|
|
||||||
|
namespace PongFE.Components
|
||||||
|
{
|
||||||
|
public struct CollisionComponent : IComponent
|
||||||
|
{
|
||||||
|
public Rectangle Rectangle { get; }
|
||||||
|
|
||||||
|
public CollisionComponent(Rectangle rectangle)
|
||||||
|
{
|
||||||
|
Rectangle = rectangle;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In **game/messages/update_position.ts**:
|
This is pretty straightforward. We just pass in a Bonk Rectangle.
|
||||||
|
|
||||||
```ts
|
|
||||||
import { ComponentMessage, Message } from "encompass-ecs";
|
|
||||||
import { PositionComponent } from "game/components/position";
|
|
||||||
|
|
||||||
export class UpdatePositionMessage extends Message implements ComponentMessage {
|
|
||||||
public component: Readonly<PositionComponent>;
|
|
||||||
public x_delta: number;
|
|
||||||
public y_delta: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's rewrite our MotionEngine.
|
Let's rewrite our MotionEngine.
|
||||||
|
|
||||||
```ts
|
First, let's create a Bonk SpatialHash. Every frame we will empty the hash and re-add all relevant entities.
|
||||||
import { Emits, Engine, Reads } from "encompass-ecs";
|
|
||||||
import { BoundingBoxComponent } from "game/components/bounding_box";
|
|
||||||
import { PositionComponent } from "game/components/position";
|
|
||||||
import { CollisionCheckMessage } from "game/messages/collision_check";
|
|
||||||
import { MotionMessage } from "game/messages/component/motion";
|
|
||||||
import { UpdatePositionMessage } from "game/messages/update_position";
|
|
||||||
import { GCOptimizedList, GCOptimizedSet } from "encompass-gc-optimized-collections";
|
|
||||||
|
|
||||||
@Reads(MotionMessage)
|
In **PongFE/Engines/MotionEngine.cs**:
|
||||||
@Emits(UpdatePositionMessage, CollisionCheckMessage)
|
|
||||||
export class MotionEngine extends Engine {
|
|
||||||
private component_to_message = new Map<PositionComponent, GCOptimizedList<MotionMessage>>();
|
|
||||||
private bounding_box_set = new GCOptimizedSet<BoundingBoxComponent>();
|
|
||||||
|
|
||||||
public update(dt: number) {
|
```cs
|
||||||
const motion_messages = this.read_messages(MotionMessage);
|
using System.Collections.Generic;
|
||||||
for (const message of motion_messages.values()) {
|
using System.Numerics;
|
||||||
this.register_message(message);
|
using Encompass;
|
||||||
}
|
using MoonTools.Bonk;
|
||||||
|
using MoonTools.Structs;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
for (const [position_component, messages] of this.component_to_message.entries()) {
|
namespace PongFE.Engines
|
||||||
const entity = this.get_entity(position_component.entity_id)!;
|
{
|
||||||
|
[QueryWith(typeof(PositionComponent), typeof(CollisionComponent))]
|
||||||
|
public class MotionEngine : Engine
|
||||||
|
{
|
||||||
|
private readonly SpatialHash<Entity> _spatialHash = new SpatialHash<Entity>(32);
|
||||||
|
|
||||||
let x_delta = 0;
|
public override void Update(double dt)
|
||||||
let y_delta = 0;
|
{
|
||||||
|
_spatialHash.Clear();
|
||||||
|
|
||||||
for (const message of messages.values()) {
|
foreach (var entity in TrackedEntities)
|
||||||
x_delta += message.x * dt;
|
{
|
||||||
y_delta += message.y * dt;
|
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
|
||||||
}
|
ref readonly var collisionComponent = ref GetComponent<CollisionComponent>(entity);
|
||||||
|
|
||||||
const update_position_message = this.emit_component_message(UpdatePositionMessage, position_component);
|
_spatialHash.Insert(entity, collisionComponent.Rectangle, new Transform2D(positionComponent.Position));
|
||||||
update_position_message.x_delta = x_delta;
|
|
||||||
update_position_message.y_delta = y_delta;
|
|
||||||
|
|
||||||
if (entity.has_component(BoundingBoxComponent)) {
|
|
||||||
const collision_check_message = this.emit_message(CollisionCheckMessage);
|
|
||||||
collision_check_message.entity = entity;
|
|
||||||
collision_check_message.x_delta = x_delta;
|
|
||||||
collision_check_message.y_delta = y_delta;
|
|
||||||
|
|
||||||
this.bounding_box_set.add(entity.get_component(BoundingBoxComponent));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const component of this.read_components(BoundingBoxComponent).values()) {
|
|
||||||
if (!this.bounding_box_set.has(component)) {
|
|
||||||
const collision_check_message = this.emit_message(CollisionCheckMessage);
|
|
||||||
collision_check_message.entity = this.get_entity(component.entity_id)!;
|
|
||||||
collision_check_message.x_delta = 0;
|
|
||||||
collision_check_message.y_delta = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.component_to_message.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private register_message(message: MotionMessage) {
|
|
||||||
if (!this.component_to_message.has(message.component)) {
|
|
||||||
this.component_to_message.set(message.component, new GCOptimizedList<MotionMessage>());
|
|
||||||
}
|
|
||||||
this.component_to_message.get(message.component)!.add(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's detect collisions.
|
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.
|
||||||
|
|
Loading…
Reference in New Issue