Having fun with Typescript, ThreeJS and Ammo.js

This week I implemented a simple 3D experiment with Typescript, ThreeJS and Ammo.js. It works like an FPS game where you can navigate with the mouse pointer and W-A-S-D keys, and you can also shoot cannon balls with the mouse button.

webgl_shooter.png

Using Typescript

Typescript is a great language that compiles to Javascript code. If you are a web developer and have never used this language before, you should check it out immediately because it has a lot of interesting things that can make your life (as a web programmer) much easier. You know that Javascript is a powerful language, but alone it can make the maintenance of big projects a nightmare. Typescript is a great step towards high quality maintainable code because it is a typed superset of Javascript.

The challenge of using Typescript with some external libraries is that you need the type definition files of those libraries to compile your code. A type definition file is a piece of typescript code that contains the declaration of types, classes, interfaces and functions of a library. It doesn’t provide implementation details, only types and signatures. This allows Typescript to analyze your code and detect programming errors. For example, check the ThreeJS definition file here:

DefinitelyTyped is a website that aggregates lots of definition files and it is a great asset for Typescript developers. Sometimes it doesn’t contain definition files that you need and this was the case of the Ammo.js library I used in my experiment.

No definition files – What should I do?

When there is no definition file available for the library you want to use, you can build one your own. Typescript can certainly guide you through this process. The compiler errors reported by Typescript will show you exactly what is missing in the definition file. For example, let’s try to declare a variable like this:

let physicsWorld: Ammo.btDiscreteDynamicsWorld;

You can start with an empty definition file (ammo.d.ts). Compiling the code above will show this message:

Cannot find namespace 'Ammo'.

So you can use this information to fix your custom definition file for that library. In this case, you can just add an empty namespace like this:

declare namespace Ammo {
}

After that, if you try to compile again the error message will change to:

Module 'Ammo' has no exported member 'btDiscreteDynamicsWorld'.

So you can change your definition file to:

declare namespace Ammo {
    export class btDiscreteDynamicsWorld {
    }
}

Following this pattern, you can basically add the missing information piece by piece to the definition file until your project is fully compilable.  Of course you have to keep an eye on the real library code and make sure your declarations are consistent with the real implementation. For Ammo.js, you can see below the final definition file I created. Note that there is no implementation code, only types and signatures.

declare namespace Ammo {

   export class btDefaultCollisionConfiguration {}

   export class btCollisionDispatcher {
      constructor(c: btDefaultCollisionConfiguration);
   }

   export class btVector3 {
      x(): number;
      y(): number;
      z(): number;
      constructor(x: number, y: number, z: number);
   }

   export class btAxisSweep3 {
      constructor(min: btVector3, max: btVector3);
   }

   export class btSequentialImpulseConstraintSolver {}

   export class btDiscreteDynamicsWorld {
      constructor(a: btCollisionDispatcher, b: btAxisSweep3, c: btSequentialImpulseConstraintSolver, d: btDefaultCollisionConfiguration);
      setGravity(v: btVector3);
      addRigidBody(b: btRigidBody);
      stepSimulation(n1: number, n2: number);
   }

   export class btConvexShape {
      calculateLocalInertia(n: number, v: btVector3);
      setMargin(n: number);
   }

   export class btBoxShape extends btConvexShape {
      constructor(v: btVector3);
   }

   export class btSphereShape extends btConvexShape {
      constructor(radius: number);
   }

   export class btRigidBody {
      constructor(info: btRigidBodyConstructionInfo);
      setActivationState(s: number);
   }

   export class btQuaternion {
      x(): number;
      y(): number;
      z(): number;
      w(): number;
      constructor(x: number, y: number, z: number, w: number);
   }

   export class btTransform {
      setIdentity();
      setOrigin(v: btVector3);
      getOrigin(): btVector3;
      setRotation(q: btQuaternion);
      getRotation(): btQuaternion;
   }

   export class btRigidBodyConstructionInfo {
      constructor(mass: number, motionState: btDefaultMotionState, shape: btConvexShape, inertia: btVector3);
   }

   export class btDefaultMotionState {
      constructor(t: btTransform);
   }
}

You should realize that my type definitions for Ammo.js are far from complete, considering what the library does. Ammo is a huge library and it contains a lot more things that my code will not use at this point. This is okay and the main point here is that the file I created is enough for me to compile my experiment and keep moving. If I need more methods from Ammo.js, I can adjust that file as needed and move on.

Typescript and browser differences

Sometimes Typescript will NOT understand your Javascript code and you will have to deal with it. Let me give you a clear example:

let m = document.body.requestPointerLock || 
        document.body.mozRequestPointerLock || 
        document.body.webkitRequestPointerLock;

The code above is syntactically correct and it looks for the Pointer Lock API in the current browser. Typescript complains about this code with the following error message:

Property 'mozRequestPointerLock' does not exist on type 'HTMLElement'.
Property 'webkitRequestPointerLock' does not exist on type 'HTMLElement'.

Those methods are browser-specific and typescript doesn’t recognize them. We have to fix this error message and one possible solution is to assign the document.body value to a variable of type “any”. This will prevent typescript from checking if those properties belong to the HTMLElement type:

let _body: any = document.body;
let m = _body.requestPointerLock || 
        _body.mozRequestPointerLock || 
        _body.webkitRequestPointerLock;

There are other ways of handling such cases, but I won’t explain them in this post. One great thing about Typescript is that it doesn’t stop the compilation when it finds errors like the one above. It simply generates the javascript output as if the code were correct and you can clean up that error message later.

Conclusions

Working with Typescript is an amazing experience. I believe that Microsoft is doing a great job with this language and contributing to the future of web development with great ideas. My experiment with ThreeJS and Ammo.js is just in the beginning. I have a good experience with the BulletPhysics library (see my personal projects) and this should allow me to expand the code and build more cool demos, examples and games.

 

2 thoughts on “Having fun with Typescript, ThreeJS and Ammo.js

Leave a comment