Home Reference Source

src/components/PhysicsWorld.js

import Script from './Script';
import { vec2 } from '../utils/gl-matrix';
import { b2World, b2Vec2, Dynamics } from '../utils/box2d';

export default class PhysicsWorld extends Script {

  static factory() {
    return new PhysicsWorld();
  }

  static get propsTypes() {
    return {
      ...Script.propsTypes,
      gravity: 'vec2',
      doSleep: 'boolean',
      iterations: 'integer',
      timeScale: 'number'
    };
  }

  get gravity() {
    return this._gravity;
  }

  set gravity(value) {
    if (!(value instanceof Array)) {
      throw new Error('`value` is not type of Array!');
    }
    if (value.length < 2) {
      throw new Error('`value` must have at least 2 elements!');
    }

    vec2.copy(this._gravity, value);

    const { _world } = this;
    if(!!_world) {
      _world.SetGravity(new b2Vec2(value[0], value[1]));
    }
  }

  get doSleep() {
    return this._doSleep;
  }

  set doSleep(value) {
    if (typeof value !== 'boolean') {
      throw new Error('`value` is not type of Boolean!');
    }

    this._doSleep = value;
  }

  get iterations() {
    return this._iterations;
  }

  set iterations(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._iterations = value | 0;
  }

  get timeScale() {
    return this._timeScale;
  }

  set timeScale(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._timeScale = value;
  }

  get world() {
    return this._world;
  }

  constructor() {
    super();

    this._gravity = vec2.create();
    this._doSleep = true;
    this._iterations = 1;
    this._timeScale = 1;
    this._world = null;
    this._contactListener = null;
    this._onBeginContact = this.onBeginContact.bind(this);
    this._onEndContact = this.onEndContact.bind(this);
  }

  dispose() {
    super.dispose();

    this._gravity = null;
    this._world = null;
    this._contactListener = null;
  }

  rayCast(callback, startX, startY, endX, endY) {
    if (!callback) {
      throw new Error('`callback` cannot be null!');
    }
    if (typeof startX !== 'number') {
      throw new Error('`startX` is not type of Number!');
    }
    if (typeof startY !== 'number') {
      throw new Error('`startY` is not type of Number!');
    }
    if (typeof endX !== 'number') {
      throw new Error('`endX` is not type of Number!');
    }
    if (typeof endY !== 'number') {
      throw new Error('`endY` is not type of Number!');
    }

    const { _world } = this;
    if (!!_world) {
      _world.RayCast(
        (fixture, point, normal, fraction) => {
          return callback(
            fixture.GetUserData(),
            point,
            normal,
            fraction
          );
        },
        new b2Vec2(startX, startY),
        new b2Vec2(endX, endY)
      );
    }
  }

  rayCastOne(startX, startY, endX, endY) {
    if (typeof startX !== 'number') {
      throw new Error('`startX` is not type of Number!');
    }
    if (typeof startY !== 'number') {
      throw new Error('`startY` is not type of Number!');
    }
    if (typeof endX !== 'number') {
      throw new Error('`endX` is not type of Number!');
    }
    if (typeof endY !== 'number') {
      throw new Error('`endY` is not type of Number!');
    }

    const { _world } = this;
    if (!!_world) {
      const result = _world.RayCastOne(
        new b2Vec2(startX, startY),
        new b2Vec2(endX, endY)
      );

      return !!result ? result.GetUserData() : null;
    }

    return null;
  }

  rayCastAll(startX, startY, endX, endY) {
    if (typeof startX !== 'number') {
      throw new Error('`startX` is not type of Number!');
    }
    if (typeof startY !== 'number') {
      throw new Error('`startY` is not type of Number!');
    }
    if (typeof endX !== 'number') {
      throw new Error('`endX` is not type of Number!');
    }
    if (typeof endY !== 'number') {
      throw new Error('`endY` is not type of Number!');
    }

    const { _world } = this;
    if (!!_world) {
      const result = _world.RayCastAll(
        new b2Vec2(startX, startY),
        new b2Vec2(endX, endY)
      );

      return !!result ? result.map(fixture => fixture.GetUserData()) : null;
    }

    return null;
  }

  onAttach() {
    const { _gravity, _doSleep } = this;

    const world = this._world = new b2World(
      new b2Vec2(_gravity[0], _gravity[1]),
      _doSleep
    );
    const contactListener = new Dynamics.b2ContactListener();
    contactListener.BeginContact = this._onBeginContact;
    contactListener.EndContact = this._onEndContact;
    world.SetContactListener(contactListener);
  }

  onDetach() {
    this._world = null;
  }

  onUpdate(deltaTime) {
    const { _world, _iterations, _timeScale } = this;

    if (!!_world) {
      _world.Step(deltaTime * 0.001 * _timeScale, _iterations, _iterations);
    }
  }

  onBeginContact(contact) {
    const bodyA = contact.GetFixtureA().GetUserData();
    const bodyB = contact.GetFixtureB().GetUserData();

    if (!!bodyA && bodyA.listenForContacts) {
      const { entity } = bodyA;
      if (!!entity) {
        entity.performAction('begin-contact', bodyB, contact);
      }
    }
    if (!!bodyB && bodyB.listenForContacts) {
      const { entity } = bodyB;
      if (!!entity) {
        entity.performAction('begin-contact', bodyA, contact);
      }
    }
  }

  onEndContact(contact) {
    const bodyA = contact.GetFixtureA().GetUserData();
    const bodyB = contact.GetFixtureB().GetUserData();

    if (!!bodyA && bodyA.listenForContacts) {
      const { entity } = bodyA;
      if (!!entity) {
        entity.performAction('end-contact', bodyB, contact);
      }
    }
    if (!!bodyB && bodyB.listenForContacts) {
      const { entity } = bodyB;
      if (!!entity) {
        entity.performAction('end-contact', bodyA, contact);
      }
    }
  }

}