Source: physics/index.js

const System = require('engine/system');
const Vector = require('engine/Vector');
const { removeItems } = require('engine/utils/array');
const { clamp } = require('engine/utils/math');

/**
 * Physics system.
 *
 * @class SystemPhysics
 */
class SystemPhysics extends System {
  /**
   * @constructor
   * @param {Object} [settings] Settings to be merged in.
   */
  constructor(settings) {
    super();

    this.name = 'Physics';

    /**
     * Gravity of physics world.
     * @property {Vector} gravity
    */
    this.gravity = Vector.create();
    /**
     * Spatial hash shift factor (larger number = less division)
     * @type {Number}
     */
    this.spatialShift = 5;
    /**
     * Collision solver instance.
     * @type {SATSolver|AABBSolver}
     */
    this.solver = null;
    /**
     * List of colliders in world.
     * @type {array}
     */
    this.colliders = [];
    /**
     * Collision map to trace colliders against.
     * @type {CollisionMap}
     */
    this.collisionMap = null;

    /**
     * Save whether collision of a pair of objects are checked
     * @type {object}
     * @private
     */
    this.checks = {};
    /**
     * How many pair of colliders have been checked in this frame
     * @type {number}
     * @protected
     */
    this.collisionChecks = 0;

    /**
     * Collision map trace result
     * @type {Object}
     * @private
     */
    this.res = {
      x: 0,
      y: 0,
      hitX: false,
      hitY: false,
    };

    this.setup(settings);
  }

  /**
   * Setup this system with setting object.
   * @memberof SystemPhysics#
   * @method setup
   * @param {Object} settings Setting object.
   */
  setup(settings) {
    for (let k in settings) {
      switch (k) {
        // Value
        case 'name':
        case 'spatialShift':
        case 'solver':
        case 'collisionMap':
          this[k] = settings[k];
          break;

        // Vector
        case 'gravity':
          this.gravity.x = settings.gravity.x || 0;
          this.gravity.y = settings.gravity.y || 0;
          break;
      }
    }
  }

  /**
   * Add collider to world.
   * @memberof SystemPhysics#
   * @method addCollider
   * @param {Coll} coll Collider to add
   */
  addCollider(coll) {
    coll.world = this;
    coll.isRemoved = false;
    if (this.colliders.indexOf(coll) === -1) {
      this.colliders.push(coll);
    }
  }

  /**
   * Remove collider from world.
   * @memberof SystemPhysics#
   * @method removeCollider
   * @param {Coll} coll Collider to remove
   */
  removeCollider(coll) {
    if (!coll.world) {return;}
    coll.world = null;
    coll.isRemoved = true;
  }

  /**
   * Update colliders and check collisions.
   * @memberof SystemPhysics#
   * @method fixedUpdate
   * @param {Number} dt     Delta time in millisecond
   * @param {Number} delta  Delta time in second
   */
  fixedUpdate(dt, delta) {
    this.collisionChecks = 0;
    this.checks = {};

    let i, j, coll, coll2, group, hash = {};
    let halfWidth, halfHeight, sx, sy, ex, ey, x, y, a2b, b2a, key;

    // Process colliders
    for (i = 0; i < this.colliders.length; i++) {
      coll = this.colliders[i];

      // Save position of last frame
      coll.last.copy(coll.position);

      // Collider is already removed, just remove it
      if (coll.isRemoved) {
        removeItems(this.colliders, i--, 1);
        continue;
      }

      if (!coll.isStatic) {
        // Update velocity
        if (coll.mass !== 0) {
          coll.velocity.add(
            this.gravity.x * coll.mass * delta,
            this.gravity.y * coll.mass * delta
          );
        }

        coll.velocity.add(coll.force.x * delta, coll.force.y * delta);
        if (coll.damping > 0 && coll.damping < 1) {
          coll.velocity.multiply(Math.pow(1 - coll.damping, delta));
        }

        // Clamp the velocity
        if (coll.velocityLimit.x > 0) {
          coll.velocity.x = clamp(coll.velocity.x, -coll.velocityLimit.x, coll.velocityLimit.x);
        }
        if (coll.velocityLimit.y > 0) {
          coll.velocity.y = clamp(coll.velocity.y, -coll.velocityLimit.y, coll.velocityLimit.y);
        }

        // Calculate delta movement during this frame
        this.res.x = coll.velocity.x * delta;
        this.res.y = coll.velocity.y * delta;

        // Trace against the map there is one
        // TODO: add a flag to pass this step
        if (this.collisionMap) {
          this.collisionMap.trace(coll, this.res.x, this.res.y, this.res);
          // Manually handle trace result
          coll.handleMovementTrace(this.res);
        }

        // Apply trace result
        coll.position.x += this.res.x;
        coll.position.y += this.res.y;
      }

      // Update bounds
      if (coll.shape) {
        halfWidth = coll.shape.width * 0.5;
        halfHeight = coll.shape.height * 0.5;

        coll.lastLeft = Math.floor(coll.last.x - halfWidth);
        coll.lastRight = Math.floor(coll.last.x + halfWidth);
        coll.lastTop = Math.floor(coll.last.y - halfHeight);
        coll.lastBottom = Math.floor(coll.last.y + halfHeight);

        coll.left = Math.floor(coll.position.x - halfWidth);
        coll.right = Math.floor(coll.position.x + halfWidth);
        coll.top = Math.floor(coll.position.y - halfHeight);
        coll.bottom = Math.floor(coll.position.y + halfHeight);
      }

      // Insert the hash and test collisions
      sx = coll.left >> this.spatialShift;
      sy = coll.top >> this.spatialShift;
      ex = coll.right >> this.spatialShift;
      ey = coll.bottom >> this.spatialShift;

      // Non-static colliders will be notified before collision
      if (!coll.isStatic) {
        coll.beforeCollide();
      }

      for (y = sy; y <= ey; y++) {
        for (x = sx; x <= ex; x++) {
          // Find or create the list
          if (!hash[x]) {
            hash[x] = {};
          }
          if (!hash[x][y]) {
            hash[x][y] = [];
          }
          group = hash[x][y];

          // Insert collider into the group
          group.push(coll);

          // Pass: only one collider
          if (group.length === 1) {
            continue;
          }

          // Test colliders in the same group
          for (j = 0; j < group.length; j++) {
            coll2 = group[j];

            // Pass: same collider or someone is already removed
            if (coll2 === coll || coll.isRemoved || coll2.isRemoved) {
              continue;
            }

            a2b = !!(coll.collideAgainst & coll2.collisionGroup) && !(coll.isStatic);
            b2a = !!(coll2.collideAgainst & coll.collisionGroup) && !(coll2.isStatic);

            // Pass: never collide with each other
            if (!a2b && !b2a) {
              continue;
            }

            key = `${coll.id < coll2.id ? coll.id : coll2.id}:${coll.id > coll2.id ? coll.id : coll2.id}`;

            // Pass: already checked
            if (this.checks[key]) {
              continue;
            }

            // Mark this pair is already checked
            this.checks[key] = true;
            this.collisionChecks++;

            // Test overlap
            if (this.solver.hitTest(coll, coll2)) {
              // Apply response
              this.solver.hitResponse(coll, coll2, a2b, b2a);
            }
          }
        }
      }
    }
  }

  /**
   * Remove all colliders and collision groups.
   * @memberof SystemPhysics#
   * @method cleanup
   */
  cleanup() {
    this.colliders.length = 0;
  }

  /**
   * Callback that will be invoked on each entity spawn.
   * @memberof SystemPhysics#
   * @method onEntitySpawn
   * @param  {Entity} ent Entity instance
   */
  onEntitySpawn(ent) {
    if (ent.coll) {
      ent.coll.entity = ent;
      this.addCollider(ent.coll);
      // Override coll's position with the entity's
      ent.coll.position = ent.position;
    }
  }
  /**
   * Callback that will be invoked on each entity remove.
   * @memberof SystemPhysics#
   * @method onEntityRemove
   * @param  {Entity} ent Entity instance
   */
  onEntityRemove(ent) {
    if (ent.coll) {
      ent.coll.remove();
      ent.coll.entity = null;
    }
  }
}

module.exports = SystemPhysics;

/**
 * Get a collision group by index
 * @memberof module:engine/physics
 * @param  {Number} idx Index of the group.
 * @return {Number}     Group mask
 */
module.exports.getGroupMask = function(idx) {
  if (idx < 31) {
    return 1 << idx;
  }
  else {
    console.log('Warning: only 0-30 indexed collision group is supported!');
    return 0;
  }
};