Search code examples
javascripttypescriptgame-engineentity-component-system

How to handle collisions in ECS pattern?


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:

  • LayerComponent - component, which keeps name of layer for rendering;
  • PositionComponent - component, which keeps position;
  • AppearanceComponent - component, which keeps rendering options;
  • BoxColliderComponent - component, which keeps size of AABB for collision handle.

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
    );
  }
}


Solution

  • 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