[SOLVED] Restarting app with physics (ammo)

I am using PlayCanvas engine-only, and ran into an issue with having ammo loaded on the page, and then restarting the app.
I took the code from the falling shapes example (https://playcanvas.github.io/#physics/falling-shapes.html), then added this to destroy and start the app again. I get this error after starting the app the second time: Uncaught TypeError: can't access property "getGravity", this.dynamicsWorld is null.

const loadAmmo = () => new Promise((resolve) => {
  if (wasmSupported()) {
    loadWasmModuleAsync('Ammo', './lib/ammo.wasm.js', './lib/ammo.wasm.wasm', () => { resolve(); });
  } else {
    loadWasmModuleAsync('Ammo', './lib/ammo.js', '', () => { resolve(); });
  }
});

loadAmmo().then(() => {
  console.log('Start app 1');
  demo();

  setTimeout(() => {
    console.log('Destroy app');
    pc.app.destroy();

    setTimeout(() => {
      console.log('Start app 2');
      demo();
    }, 1000);
  }, 5000);
});

Project download: https://we.tl/t-x7iXFiyAix (run npm install first, then serve it and run it on http://localhost:8080) (Warning: it really spams the console so it might freeze your browser)

How would I get around this error? :slight_smile:

Edit: also posted on the engine-github https://github.com/playcanvas/engine/issues/2442

Hi @Ivolutio,

Indeed I can reproduce your error, it seems that the second time you start the app the onLibraryLoaded() method here isn’t called again:

Most likely there is something with how the Ammo.js module is being loaded that doesn’t trigger that. Could you post an issue about it in the engine tracker?

1 Like

Having trouble trying to build the dist folder. I’'ve deleted the folder but npm install doesn’t recreate it :sweat:

The dist folder doesnt get created on build. It only adds the main.js on build, and uses the index.html that’s already in the dist folder :slight_smile:. So you’ll have to get it back from the zip.

1 Like

Odd, I delete the main.js file and it doesn’t get recreated.

Does it give you any errors? it looks like this for me: image

No errors

stevenyau@XXXXXXXXXXXXX test-env_ammo-restart % npm install
audited 695 packages in 2.471s

38 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Am I meant to run a different command?

Ok that is fine. Now just run npm run serve and go to localhost:8080.

stevenyau@XXXXXXX test-env_ammo-restart % npm run serve

> test-env@1.0.0 serve /Users/stevenyau/Downloads/test-env_ammo-restart
> webpack-dev-server --config ./webpack.config.js --mode development --host 0.0.0.0

sh: webpack-dev-server: command not found
npm ERR! code ELIFECYCLE
npm ERR! syscall spawn
npm ERR! file sh
npm ERR! errno ENOENT
npm ERR! test-env@1.0.0 serve: `webpack-dev-server --config ./webpack.config.js --mode development --host 0.0.0.0`
npm ERR! spawn ENOENT
npm ERR!
npm ERR! Failed at the test-env@1.0.0 serve script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/stevenyau/.npm/_logs/2020-09-25T10_05_05_947Z-debug.log

Please bear in mind I’m not a web developer so the NPM ecosystem is very new to me :sweat_smile:

Well I guess I’m working too much with it as I forget how to set it up properly.
You need to have the webpack dev server installed globally: npm install webpack-dev-server -g.

I can run npm run build and do the hosting myself it seems, I go with that :slight_smile:

Did the same when I gave it a try, to avoid adding a global dependency. Just run npm run build after each change.

Found the issue. There’s global data that wasn’t reset. I have a fix coming into a PR but in the meantime, it’s easy to monkey patch (see end of file)

FYI, destroying an app is not a common user path so there may be other issues like this. The bug reports and repro projects are very helpful, thank you!

// Playcanvas
import * as pc from 'playcanvas';

function demo() {
  const canvas = document.getElementById('application-canvas');

  // Create the application and start the update loop
  const app = new pc.Application(canvas);
  app.start();

  // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
  app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
  app.setCanvasResolution(pc.RESOLUTION_AUTO);

  window.addEventListener('resize', () => {
    app.resizeCanvas(canvas.width, canvas.height);
  });

  app.scene.ambientLight = new pc.Color(0.2, 0.2, 0.2);

  // Set the gravity for our rigid bodies
  app.systems.rigidbody.gravity.set(0, -9.81, 0);

  function createMaterial(color) {
    const material = new pc.StandardMaterial();
    material.diffuse = color;
    // we need to call material.update when we change its properties
    material.update();
    return material;
  }

  // create a few materials for our objects
  const red = createMaterial(new pc.Color(1, 0.3, 0.3));
  const gray = createMaterial(new pc.Color(0.7, 0.7, 0.7));

  // ***********    Create our floor   *******************

  const floor = new pc.Entity();
  floor.addComponent('model', {
    type: 'box',
    material: gray,
  });

  // scale it
  floor.setLocalScale(10, 1, 10);

  // add a rigidbody component so that other objects collide with it
  floor.addComponent('rigidbody', {
    type: 'static',
    restitution: 0.5,
  });

  // add a collision component
  floor.addComponent('collision', {
    type: 'box',
    halfExtents: new pc.Vec3(5, 0.5, 5),
  });

  // add the floor to the hierarchy
  app.root.addChild(floor);

  // ***********    Create lights   *******************

  // make our scene prettier by adding a directional light
  const light = new pc.Entity();
  light.addComponent('light', {
    type: 'directional',
    color: new pc.Color(1, 1, 1),
    castShadows: true,
    shadowBias: 0.2,
    shadowDistance: 16,
    normalOffsetBias: 0.05,
    shadowResolution: 2048,
  });

  // set the direction for our light
  light.setLocalEulerAngles(45, 30, 0);

  // Add the light to the hierarchy
  app.root.addChild(light);

  // ***********    Create camera    *******************

  // Create an Entity with a camera component
  const camera = new pc.Entity();
  camera.addComponent('camera', {
    clearColor: new pc.Color(0.5, 0.5, 0.8),
    farClip: 50,
  });

  // add the camera to the hierarchy
  app.root.addChild(camera);

  // Move the camera a little further away
  camera.translate(0, 10, 15);
  camera.lookAt(0, 0, 0);

  // ***********    Create templates    *******************

  // Create a template for a falling box
  // It will have a model component of type 'box'...
  const boxTemplate = new pc.Entity();
  boxTemplate.addComponent('model', {
    type: 'box',
    castShadows: true,
    material: gray,
  });

  // ...a rigidbody component of type 'dynamic' so that it is simulated
  // by the physics engine...
  boxTemplate.addComponent('rigidbody', {
    type: 'dynamic',
    mass: 50,
    restitution: 0.5,
  });

  // ... and a collision component of type 'box'
  boxTemplate.addComponent('collision', {
    type: 'box',
    halfExtents: new pc.Vec3(0.5, 0.5, 0.5),
  });

  // Create other shapes too for variety...

  // A sphere...
  const sphereTemplate = new pc.Entity();
  sphereTemplate.addComponent('model', {
    type: 'sphere',
    castShadows: true,
    material: gray,
  });

  sphereTemplate.addComponent('rigidbody', {
    type: 'dynamic',
    mass: 50,
    restitution: 0.5,
  });

  sphereTemplate.addComponent('collision', {
    type: 'sphere',
    radius: 0.5,
  });

  // A capsule...
  const capsuleTemplate = new pc.Entity();
  capsuleTemplate.addComponent('model', {
    type: 'capsule',
    castShadows: true,
    material: gray,
  });

  capsuleTemplate.addComponent('rigidbody', {
    type: 'dynamic',
    mass: 50,
    restitution: 0.5,
  });

  capsuleTemplate.addComponent('collision', {
    type: 'capsule',
    radius: 0.5,
    height: 2,
  });

  // A cylinder...
  const cylinderTemplate = new pc.Entity();
  cylinderTemplate.addComponent('model', {
    type: 'cylinder',
    castShadows: true,
    material: gray,
  });

  cylinderTemplate.addComponent('rigidbody', {
    type: 'dynamic',
    mass: 50,
    restitution: 0.5,
  });

  cylinderTemplate.addComponent('collision', {
    type: 'cylinder',
    radius: 0.5,
    height: 1,
  });

  // add all the templates to an array so that
  // we can randomly spawn them
  const templates = [boxTemplate, sphereTemplate, capsuleTemplate, cylinderTemplate];

  // disable the templates because we don't want them to be visible
  // we'll just use them to clone other Entities
  templates.forEach((template) => {
    template.enabled = false;
  });

  // ***********    Update Function   *******************

  // initialize variables for our update function
  let timer = 0;
  let count = 40;

  // Set an update function on the application's update event
  app.on('update', (dt) => {
    // create a falling box every 0.2 seconds
    if (count > 0) {
      timer -= dt;
      if (timer <= 0) {
        count--;
        timer = 0.2;

        // Clone a random template and position it above the floor
        const template = templates[Math.floor(pc.math.random(0, templates.length))];
        const clone = template.clone();
        // enable the clone because the template is disabled
        clone.enabled = true;

        app.root.addChild(clone);

        clone.rigidbody.teleport(pc.math.random(-1, 1), 10, pc.math.random(-1, 1));
      }
    }

    // Show active bodies in red and frozen bodies in gray
    app.root.findComponents('rigidbody').forEach((body) => {
      body.entity.model.material = body.isActive() ? red : gray;
    });
  });
};

// check for wasm module support
function wasmSupported() {
  try {
    if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {
      const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00));
      if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
    }
  } catch (e) { }
  return false;
}

// load a script
function loadScriptAsync(url, doneCallback) {
  const tag = document.createElement('script');
  tag.onload = function () {
    doneCallback();
  };
  tag.onerror = function () {
    throw new Error(`failed to load ${url}`);
  };
  tag.async = true;
  tag.src = url;
  document.head.appendChild(tag);
}

// load and initialize a wasm module
function loadWasmModuleAsync(moduleName, jsUrl, binaryUrl, doneCallback) {
  loadScriptAsync(jsUrl, () => {
    const lib = window[moduleName];
    window[`${moduleName}Lib`] = lib;
    lib({
      locateFile() {
        return binaryUrl;
      },
    }).then((instance) => {
      window[moduleName] = instance;
      doneCallback();
    });
  });
}

const loadAmmo = () => new Promise((resolve) => {
  if (wasmSupported()) {
    loadWasmModuleAsync('Ammo', './lib/ammo.wasm.js', './lib/ammo.wasm.wasm', () => { resolve(); });
  } else {
    loadWasmModuleAsync('Ammo', './lib/ammo.js', '', () => { resolve(); });
  }
});

loadAmmo().then(() => {
  console.log('Start app 1');
  demo();

  setTimeout(() => {
    console.log('Destroy app');
    pc.app.destroy();

    setTimeout(() => {
      console.log('Start app 2');
      demo();
    }, 1000);
  }, 5000);

  pc.ComponentSystem.destroy = function () {
    pc.ComponentSystem.off('initialize');
    pc.ComponentSystem.off('postInitialize');
    pc.ComponentSystem.off('toolsUpdate');
    pc.ComponentSystem.off('update');
    pc.ComponentSystem.off('animationUpdate');
    pc.ComponentSystem.off('fixedUpdate');
    pc.ComponentSystem.off('postUpdate');

    pc.ComponentSystem._init = [];
    pc.ComponentSystem._postInit = [];
    pc.ComponentSystem._toolsUpdate = [];
    pc.ComponentSystem._update = [];
    pc.ComponentSystem._animationUpdate = [];
    pc.ComponentSystem._fixedUpdate = [];
    pc.ComponentSystem._postUpdate = [];
};
});
2 Likes

that makes sense. I am currently using VueJS where you can click a card to load a different playcanvas build. When you leave the viewer page, it will destroy the pc.app and later recreate it again. Vue doesnt actually reload the page.

But thanks for looking into this, and the monkey patch :slight_smile: