Confusion about how to implement ICameraInput in TypeScript

I’m following the guides on Customize Camera Inputs - Babylon.js Documentation
I like the camera movement in this playground example:

And I am trying to port the example from javascript to typescript.

The docs say Using TypeScript, you could implement the interface ICameraInput so presumably I implement that with a class like so:

export class FreeCameraKeyboardWalkInput implements BABYLON.ICameraInput<BABYLON.UniversalCamera>

and then just port the required functions from the playground into the class.

Where I get confused is that the example functions in the playground reference properties like this._onKeyDown and this._onKeyUp which are not defined in the interface. I’m not sure where my class is supposed to get those.

Here is an example:

In the playground, one of the functions to implement is ‘detachControl’:

//Add detachment controls
        FreeCameraKeyboardWalkInput.prototype.detachControl = function (element) {
            if (this._onKeyDown) {
                element.removeEventListener("keydown", this._onKeyDown);
                element.removeEventListener("keyup", this._onKeyUp);
                BABYLON.Tools.UnregisterTopRootEvents([
                    { name: "blur", handler: this._onLostFocus }
                ]);
                this._keys = [];
                this._onKeyDown = null;
                this._onKeyUp = null;
            }
        };

In my class that would be a instance method (see further below):

export class FreeCameraKeyboardWalkInput implements BABYLON.ICameraInput<BABYLON.UniversalCamera>{
  
  camera: UniversalCamera
  _keys: number[]
  keysUp: number[]
  keysDown: number[]
  keysLeft: number[]
  keysRight: number[]

  constructor() {
    this._keys = [];
    this.keysUp = [38];
    this.keysDown = [40];
    this.keysLeft = [37];
    this.keysRight = [39];
  }
 .....
 
   detachControl(element: HTMLElement) {     
      if (this._onKeyDown) {
        element.removeEventListener("keydown", this._onKeyDown);
        element.removeEventListener("keyup", this._onKeyUp);
        BABYLON.Tools.UnregisterTopRootEvents(window, [
          { name: "blur", handler: this._onLostFocus }
        ]);
        this._keys = [];
        this._onKeyDown = null;
        this._onKeyUp = null;
      }
    }

Error: Property ‘_onKeyDown’ does not exist on type ‘FreeCameraKeyboardWalkInput’

I did a search of the code base for _onKeyDown and it can be found in ’ src/Inputs/scene.inputManager.ts’

So then I thought maybe I need to extend my class from an inputManager to access those properties?

I tried inheriting from a inputs manager like this, as a guess:
export class FreeCameraKeyboardWalkInput extends BABYLON.CameraInputsManager<BABYLON.UniversalCamera> implements BABYLON.ICameraInput<BABYLON.UniversalCamera> but intellisense still doesn’t know how this._onKeyDown is defined.

No sure at this point… anyway I think the documentation could benefit from at least one TypeScript example. :slight_smile:

Thanks for any advice on how to proceed!

Nevermind. I searched the codebase for ICameraInput and found some typescript examples that implemented it. It’s done in a very different way, but using those examples I was able to port the playground example into a class. Here it is if anyone else is interested:

import * as BABYLON from 'babylonjs'

export class FreeCameraKeyboardWalkInput implements BABYLON.ICameraInput<BABYLON.UniversalCamera>{
  public camera: BABYLON.UniversalCamera
  public keysUp = [38]
  public keysDown = [40]
  public keysLeft = [37]
  public keysRight = [39]

  private _keys = new Array<number>();
  private _onCanvasBlurObserver: BABYLON.Nullable<BABYLON.Observer<BABYLON.Engine>>;
  private _onKeyboardObserver: BABYLON.Nullable<BABYLON.Observer<BABYLON.KeyboardInfo>>;
  private _engine: BABYLON.Engine;
  private _scene: BABYLON.Scene;

  attachControl(element: HTMLElement, noPreventDefault?: boolean): void {
    if (this._onCanvasBlurObserver) {
      return;
    }

    this._scene = this.camera.getScene();
    this._engine = this._scene.getEngine();

    this._onCanvasBlurObserver = this._engine.onCanvasBlurObservable.add(() => {
      this._keys = [];
    });

    this._onKeyboardObserver = this._scene.onKeyboardObservable.add((info) => {
      let evt = info.event;
      if (!evt.metaKey) {
        if (info.type === BABYLON.KeyboardEventTypes.KEYDOWN) {
          if (this.keysUp.indexOf(evt.keyCode) !== -1 ||
            this.keysDown.indexOf(evt.keyCode) !== -1 ||
            this.keysLeft.indexOf(evt.keyCode) !== -1 ||
            this.keysRight.indexOf(evt.keyCode) !== -1) {
            var index = this._keys.indexOf(evt.keyCode);
            if (index === -1) {
              this._keys.push(evt.keyCode);
            }
            if (!noPreventDefault) {
              evt.preventDefault();
            }
          }


        } else {
          if (this.keysUp.indexOf(evt.keyCode) !== -1 ||
            this.keysDown.indexOf(evt.keyCode) !== -1 ||
            this.keysLeft.indexOf(evt.keyCode) !== -1 ||
            this.keysRight.indexOf(evt.keyCode) !== -1) {
            var index = this._keys.indexOf(evt.keyCode);

            if (index >= 0) {
              this._keys.splice(index, 1);
            }
            if (!noPreventDefault) {
              evt.preventDefault();
            }
          }
        }
      }
    });

  }

  detachControl(element: BABYLON.Nullable<HTMLElement>): void {
    if (this._scene) {
      if (this._onKeyboardObserver) {
        this._scene.onKeyboardObservable.remove(this._onKeyboardObserver);
      }

      if (this._onCanvasBlurObserver) {
        this._engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);
      }
      this._onKeyboardObserver = null;
      this._onCanvasBlurObserver = null;
    }
    this._keys = [];
  }

  public checkInputs(): void {
    if (this._onKeyboardObserver) {
      var camera = this.camera;
      // Keyboard
      for (var index = 0; index < this._keys.length; index++) {
        var keyCode = this._keys[index];
        var speed = camera._computeLocalCameraSpeed() / 2;
        if (this.keysLeft.indexOf(keyCode) !== -1) {
          camera.rotation.y -= speed;
          camera._localDirection.copyFromFloats(0, 0, 0);
        } else if (this.keysUp.indexOf(keyCode) !== -1) {
          camera._localDirection.copyFromFloats(0, 0, speed);
        } else if (this.keysRight.indexOf(keyCode) !== -1) {
          camera.rotation.y += speed;
          camera._localDirection.copyFromFloats(0, 0, 0);
        } else if (this.keysDown.indexOf(keyCode) !== -1) {
          camera._localDirection.copyFromFloats(0, 0, -speed);
        }

        if (camera.getScene().useRightHandedSystem) {
          camera._localDirection.z *= -1;
        }

        camera.getViewMatrix().invertToRef(camera._cameraTransformMatrix);
        BABYLON.Vector3.TransformNormalToRef(camera._localDirection, camera._cameraTransformMatrix, camera._transformedDirection);
        camera.cameraDirection.addInPlace(camera._transformedDirection);
      }
    }
  }

  public getClassName(): string {
    return 'FreeCameraKeyboardWalkInput'
  }

  public _onLostFocus(): void {
    this._keys = [];
  }

  public getSimpleName(): string {
    return 'keyboard'
  }
}
1 Like

They are defined in the example:

FreeCameraKeyboardRotateInput.prototype.attachControl = function(
  element,
  noPreventDefault
) {
  var _this = this;
  if (!this._onKeyDown) {
    element.tabIndex = 1;
    this._onKeyDown = function(evt) {
        ...
    };
    this._onKeyUp = function(evt) {
        ...
    };

Yes… , but this is rather incongruous for the given advice for typescript users, ‘implement the interface ICameraInput’, of which this._onKeyDown and such are not members of that interface.

1 Like

Those are simply private properties used to implement the interface.

The only specific thing to javascript is that you don’t need to declare this property beforehand and you can simply write this._onKeyDown=... to create and assign something to the property, whereas in Typescript you will have to declare the property (private _onKeyDown: (evt: any) => void;) first before being able to assign something to it.

Thank you Owen. Lost almost 2 days for the right implementation of types for this functionality. I also tried implementing BABYLON.Camera, but in vain. It was a pool of errors.
The ‘class’ you created gave a better idea on how to implement it in ts. I think the doc does not says much. Only a rough idea. But your answer helped so much.

Glad it helped!