PointerLockControls.js - TypeError: Cannot read properties of undefined (reading 'lock') at HTMLDivElement

48 Views Asked by At

I'm extremely new to Javascript and Three.js (and to posting on stackOverflow). I'm attempting to implement a first person camera using three.js by converting the example implementation of PointerLockControls.js found here: PointerLockControls example

My issue is that the error 'TypeError: Cannot read properties of undefined (reading 'lock') at HTMLDivElement.' is thrown - caused by line 204 of my code (full code below) which reads:

 // initialise locks: 
    const blocker = document.getElementById( 'blocker' );
    const instructions = document.getElementById( 'instructions' );
        
    instructions.addEventListener( 'click', function () {
    // LINE 204: ERROR
    this.controls_.lock;
    } );

The example defines this lock as:

instructions.addEventListener( 'click', function () {

                    controls.lock();

                } );

I understand that this error stems from the fact that, at runtime, this.controls_ is undefined. I create and assign this.controls_ before adding this event listener. But when I test to see whether this.controls_ is undefined at the point 'this.controls_.lock' is called, this.controls_ is undefined.

Is there a reason this.controls_ is undefined within the event listener? Does the listener need to function independently of the FirstPersonCamereaDemo class that controls_ is defined in?

Previously, I analogously defined

instructions.addEventListener( 'click', function () {
            this.controls_.lock();
        } );

where PointerCamera.lock was defined as

lock() {
       this.controls.lock();
}

I've attempted to redefine my lock functions using const = function () {...}, which led to the type error occurring in the PointerLockControls.js file.

Any help would be greatly appreciated!

Full Javascript:

import * as THREE from '../three.js-r134/three.js-r134/build/three.module.js';
import { FirstPersonControls } from '../three.js-r134/three.js-r134/examples/jsm/controls/FirstPersonControls.js';
import { PointerLockControls } from '../three.js-r134/three.js-r134/examples/jsm/controls/PointerLockControls.js';

// class for handling the camera

// camera implemented using the example: https://threejs.org/examples/?q=pointerlock#misc_controls_pointerlock


// listener is domElement used to listen for mouse/touch events
class PointerCamera {
    constructor(camera, dElement, objects) {
        this.camera = camera;
        this.dElement = dElement
        this.objects = objects;
        this.raycaster = null;
        this.moveForward = false;
        this.moveBackward = false;
        this.moveLeft = false;
        this.moveRight = false;
        this.canJump = false;
        this.controls = null;

        //minHeight initialised to initial value of y-coordinate of camera
        this.minHeight = camera.position.y;


        // this.prevTime
        // may not need this if we pass it to the update from update in the main render

        this.velocity = new THREE.Vector3();
        this.direction = new THREE.Vector3();
        this.vertex = new THREE.Vector3();
        this.color = new THREE.Color();

        // initialise movement
        this.dElement.addEventListener( 'keydown', this.onKeyDown);
        this.dElement.addEventListener( 'keyup', this.onKeyUp);

        this.initControls();
    }

    initControls() {
        this.controls = new PointerLockControls(this.camera, this.dElement);

        // locks
        this.lock = function () {
            this.controls.lock();
        }

        this.unlock = function () {
            this.controls.unlock();
        }
    }

    changeMinHeight(newHeight) {
        const oldHeight = this.minHeight;
        this.minHeight = newHeight;

        return oldHeight;
    }

    // keydown function
    onKeyDown( event ) {

        switch ( event.code ) {

            case 'ArrowUp':
            case 'KeyW':
                this.moveForward = true;
                break;

            case 'ArrowLeft':
            case 'KeyA':
                this.moveLeft = true;
                break;

            case 'ArrowDown':
            case 'KeyS':
                this.moveBackward = true;
                break;

            case 'ArrowRight':
            case 'KeyD':
                this.moveRight = true;
                break;

            case 'Space':
                if ( canJump === true ) velocity.y += 350;
                this.canJump = false;
                break;

        }
    }
    
    //keyupfunction
    //if domElement doesn't work, try passing full document to the class.

    onKeyUp = function ( event ) {

        switch ( event.code ) {

            case 'ArrowUp':
            case 'KeyW':
                this.moveForward = false;
                break;

            case 'ArrowLeft':
            case 'KeyA':
                this.moveLeft = false;
                break;

            case 'ArrowDown':
            case 'KeyS':
                this.moveBackward = false;
                break;

            case 'ArrowRight':
            case 'KeyD':
                this.moveRight = false;
                break;

        }

    }

    update(timeElapsedS) {

        if (this.controls.isLocked === true) {

            // to adjust vertical acceleration, need to calculate whether on the ground or not.
            const delta = timeElapsedS; // time elapsed in seconds

            // may need to double check this hits 0.
            this.velocity.x -= this.velocity.x * 10 * delta;
            this.velocity.z -= this.velocity.z * 10 * delta;

            // simulate gravity: 
            this.velocity.y -= 9.8 * 100 * delta; // 100.0 = mass of player

            this.direction.z = Number(this.moveForward) - Number(this.moveBackward);
            this.direction.x = Number(this.moveRight) - Number(this.moveLeft);
            this.direction.normalize(); // ensures consistent movement in all directions


            if (this.moveForward || this.moveBackward) this.velocity.z -= this.direction.z * 400 * delta;
            if (this.moveLeft || this.moveRight) this.velocity.x -= this.direction.x * 400 * delta;


            // add object interaction with ray casting here
            
            // update camera position: 
            this.controls.moveRight( - this.velocity.x * delta);
            this.controls.moveForward( - this.velocity.z * delta);

            this.controls.getObject().position.y += ( this.velocity.y * delta ); // new behavior

            // detect if camera falls below minimum: 

            if ( controls.getObject().position.y < 10 ) {
                this.velocity.y = 0;
                this.controls.getObject().position.y = 10;
                this.canJump = true;
            }
        }
    }
}

class FirstPersonCameraDemo {
    constructor() {
        this.initialize_();
    }

    initialize_() {
        this.initializeRenderer_();
        this.initializeLights_();
        this.initializeScene_();
        this.initializeDemo_();

        this.previousRAF_ = null;
        this.raf_();
        this.onWindowResize_();
    }

    initializeDemo_() {
        this.controls_ = new PointerCamera(this.camera_, document.body, []);
        this.controls_.controls.lookSpeed = 0.8;
        this.controls_.controls.movementSpeed = 5;
        this.controls_.controls.heightCoef = 0;
        // const controlLock = function () {this.controls_.lock};

        // initialise locks: 
        const blocker = document.getElementById( 'blocker' );
        const instructions = document.getElementById( 'instructions' );
        
        instructions.addEventListener( 'click', function () {
            this.controls_.lock;
        } );

        this.controls_.controls.addEventListener( 'lock', function () {
            instructions.style.display = 'none';
            blocker.style.display = 'none';
        } );

        this.controls_.controls.addEventListener( 'unlock', function () {
            blocker.style.display = 'block';
            instructions.style.display = '';
        } );
        
        this.scene_.add(this.controls_.controls.getObject());
    }

    initializeRenderer_() {
        this.threejs_ = new THREE.WebGLRenderer({
            antialias: false,
        });
        this.threejs_.shadowMap.enabled = true;
        this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
        this.threejs_.setPixelRatio(window.devicePixelRatio);
        this.threejs_.setSize(window.innerWidth, window.innerHeight);
        this.threejs_.physicallyCorrectLights = true;
        this.threejs_.outputEncoding = THREE.sRGBEncoding;
    
        document.body.appendChild(this.threejs_.domElement);

        window.addEventListener('resize', () => {
            this.onWindowResize_();
        }, false);


        // initialise CAMERA
        this.minHeight = 10;
        const fov = 60;
        const aspect = window.innerWidth / window.innerHeight;
        const near = 1.0;
        const far = 1000.0;
        this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
        // minimum and default height (10 - change to variable later)
        this.camera_.position.set(0, this.minHeight, 0);

        this.scene_ = new THREE.Scene();
    }

    initializeScene_() {
        const loader = new THREE.CubeTextureLoader();
        const texture = loader.load([
            './bkg/bkg/red/bkg1_right1.png',
            './bkg/bkg/red/bkg1_left2.png',
            './bkg/bkg/red/bkg1_top3.png',
            './bkg/bkg/red/bkg1_bottom4.png',
            './bkg/bkg/red/bkg1_front5.png',
            './bkg/bkg/red/bkg1_back6.png'
        ]);

        this.scene_.background = texture;

        const planegeo = new THREE.PlaneGeometry(100, 100, 10, 10);
        // plane.castShadow = false;
        // plane.receiveShadow = true;
        planegeo.rotateX(-Math.PI / 2);
        const material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
        const plane = new THREE.Mesh( planegeo, material );
        this.scene_.add(plane);
    }

    initializeLights_() { 

        // his light: 
    const distance = 50.0;
    const angle = Math.PI / 4.0;
    const penumbra = 0.5;
    const decay = 1.0;

    let light = new THREE.SpotLight(
        0xFFFFFF, 100.0, distance, angle, penumbra, decay);
    light.castShadow = true;
    light.shadow.bias = -0.00001;
    light.shadow.mapSize.width = 4096;
    light.shadow.mapSize.height = 4096;
    light.shadow.camera.near = 1;
    light.shadow.camera.far = 100;

    light.position.set(25, 25, 0);
    light.lookAt(0, 0, 0);
    this.scene_.add(light);

    const upColour = 0xFFFF80;
    const downColour = 0x808080;
    light = new THREE.HemisphereLight(upColour, downColour, 0.5);
    light.color.setHSL( 0.6, 1, 0.6 );
    light.groundColor.setHSL( 0.095, 1, 0.75 );
    light.position.set(0, 4, 0);
    this.scene_.add(light);
    }

    onWindowResize_() {
        this.camera_.aspect = window.innerWidth / window.innerHeight;
        this.camera_.updateProjectionMatrix();

        this.threejs_.setSize(window.innerWidth, window.innerHeight);
    }

    raf_() {
        requestAnimationFrame((t) => {
            if (this.previousRAF_ === null) {
              this.previousRAF_ = t;
            }
      
            this.step_(t - this.previousRAF_);
            this.threejs_.autoClear = true;
            this.threejs_.render(this.scene_, this.camera_);
            console.log("just tried to render");
            this.threejs_.autoClear = false;
            this.previousRAF_ = t;
            this.raf_();
          }); 
    }

    step_(timeElapsed) {
        // console.log("in demo step");
        const timeElapsedS = timeElapsed * 0.01;
        // may need to change above to 0.001

        this.controls_.update(timeElapsedS);
    }
}

// run
let _APP = null;

window.addEventListener('DOMContentLoaded', () => {
    console.log("successful print");
    _APP = new FirstPersonCameraDemo();
});

HTML:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Trying to get basic FPS control</title>
        <style>
            body { margin: 0; }

            #blocker {
                position: absolute;
                width: 100%;
                height: 100%;
                background-color: rgba(0, 0, 0, 0.5);
            }

            #instructions {
                width: 100%;
                height: 100%;

                display: flex;
                flex-direction: column;
                justify-content: center;
                align-items: center;

                text-align: center;
                font-size: 14px;
                cursor: pointer;
            }
        </style>
    </head>
    <body>
        <div id="blocker">
            <div id="instructions">
                <p style="font-size:36px">
                    Click to play
                </p>
                <p>
                    Move: WASD<br/>
                    Jump: SPACE<br/>
                    Look: MOUSE
                </p>
            </div>
        </div>
        <script type="module" src="./fpsbasic.js"></script>
    </body>
</html>
1

There are 1 best solutions below

0
Mr_Dalliard On

The solution was to bind the correct this to each of the new lock functions so that each call has the appropriate scope to call FirstPersonCameraDemo.controls_ and PointerCamera.controls appropriately.

The discussion scope of handler functions in addEventListener was very useful in understanding that the event handler functions would not have access to the correct 'this' belonging to the classes, but instead 'this' would refer to instructions.

The corrected definitions are:

within FirstPersonCameraDemo:

instructions.addEventListener( 'click', this.controls_.lock2().bind(this));

Within 'PointerCamera':

    initControls() {
        this.controls = new PointerLockControls(this.camera, this.dElement);

        // locks
        this.lock = function (e) {
            return this.controls.lock(e);
        }

        this.unlock = function () {
            this.controls.unlock();
        }
    }

    lock2() {return this.lock.bind(this)}
    unlock2() {return this.unlock.bind(this)}