I'm making a game with typescript and ECS pattern. But i can't understand how to handle collisions between entities.
I have entity Player
with set of components:
Also i have entity Enemy
with the same set of components. These entities have differ in values in LayerComponent
.
The Player
entity in the LayerComponent
keeps the Player
value, and the Enemy
entity keeps the Enemy
value.
I don't know how to handle collisions between these entities. These entities should not move through each other.
At the moment i've created system PlayerPosition
, which handles collision and blocks moving through entities
with BoxColliderComponent
. But i think this it is wrong, because collisions have to be handled in their own system.
Code of PlayerPosition
import { System } from 'ecs';
import { ecs, EntityType } from 'game';
import Vector2, { IVector2 } from 'services/vector2.service';
import MouseService from 'services/mouse.service';
import ELayers from 'constants/layers';
import Enemy from 'entities/enemy';
interface IIntersect {
position: IVector2;
height: number;
width: number;
}
export default class PlayerPositionSystem extends System<EntityType> {
readonly ctx: CanvasRenderingContext2D;
readonly entities: EntityType[] = [];
private readonly mouse: MouseService = new MouseService();
constructor(ctx: CanvasRenderingContext2D) {
super();
this.ctx = ctx;
}
addEntity(entity: EntityType): void {
if (this.test(entity)) {
this.entities.push(entity);
} else {
console.warn(`The entity '${entity.id}' have no necessary component`);
}
}
test(entity: EntityType): boolean {
const position = entity.components.position;
return !!position;
}
update(entity: EntityType): void {
const component = entity.components.position;
const colliderComponent = entity.components.boxCollider;
const layerComponent = entity.components.layer;
if (!component || !colliderComponent || !layerComponent) {
return;
}
if (layerComponent.props.layer !== ELayers.player) {
return;
}
const mouseCoordinates = this.mouse.getMouseCoordinate();
const { position, velocity } = component.props;
const distance = mouseCoordinates.distance(position);
const deltaVector = mouseCoordinates.subtraction(position);
const inversionDistance = 1 / distance;
const direction = new Vector2(
deltaVector.x * inversionDistance,
deltaVector.y * inversionDistance
);
const newPosition = position.addition(
new Vector2(
distance > 5 ? direction.x * velocity : 0,
distance > 5 ? direction.y * velocity : 0
)
);
const currentObject: IIntersect = {
position: new Vector2(newPosition.x, newPosition.y),
height: colliderComponent.props.size.y,
width: colliderComponent.props.size.x,
};
for (const object of this.entities) {
if (object === entity) {
continue;
}
const itemComponents = object.components;
const itemPosition =
itemComponents.position && itemComponents.position.props;
const itemBoxCollider =
itemComponents.boxCollider && itemComponents.boxCollider.props;
if (!itemPosition || !itemBoxCollider) {
continue;
}
const item: IIntersect = {
...itemPosition,
height: itemBoxCollider.size.y,
width: itemBoxCollider.size.x,
};
if (this.intersect(currentObject, item)) {
const itemLayer = object.components.layer;
if (itemLayer && itemLayer.props.layer === ELayers.enemy) {
object.remove();
const canvas = this.ctx.canvas;
let x = Math.random() * canvas.width - 100;
x = x < 0 ? 0 : x;
let y = Math.random() * canvas.height - 100;
y = y < 0 ? 0 : y;
ecs.addEntity(Enemy({ velocity: 3, position: new Vector2(x, y) }));
}
let x = newPosition.x;
let y = newPosition.y;
if (
this.intersect(
{
...currentObject,
position: new Vector2(x, position.y),
},
item
)
) {
x = position.x;
}
if (
this.intersect(
{
...currentObject,
position: new Vector2(position.x, y),
},
item
)
) {
y = position.y;
}
newPosition.set(new Vector2(x, y));
}
}
component.setProperties({ position: newPosition });
}
intersect(object: IIntersect, object2: IIntersect): boolean {
const { position: pos1, height: h1, width: w1 } = object;
const { position: pos2, height: h2, width: w2 } = object2;
return (
pos1.x + w1 > pos2.x &&
pos2.x + w2 > pos1.x &&
pos1.y + h1 > pos2.y &&
pos2.y + h2 > pos1.y
);
}
}
I don't know if there's supposed to be a bug or if this code is working so i'll assume your question is strictly about where to put the code for your collision detection system :
In this kind of case, you have to consider the interactions between your collision detection system and your movement system. Most of the time, the approach will be something like
1 - Apply movement without taking collisions into account
2 - Detect collisions
3 - Adjust the movement you made depending on the collisions you just detected
So since your collision detection is tightly coupled with your movement system, it would make sense for me to keep it in there. However, it might be a good idea to still isolate your collision detection system, so what you can do is simply call your collision detection system from your movement system, making your collision system a 'sub-system' of your movement system so to speak.
The other option would be to indeed separate them but then your collisions detection system will need to itself readjust the positions of your entities. This is probably ok, but it would probably add some complexity to your code (I'm guessing you will need to store more data in your components) and it would break the assumption that only your movement system mutates your entities position (which might be a good thing to keep, but it's not necessary by any means).
Hope this helps