我对 Javascript 和 Three.js(以及在 stackOverflow 上发帖)非常陌生。我试图通过转换此处找到的 PointerLockControls.js 的示例实现来使用 Three.js 实现第一人称相机: PointerLockControls 示例
我的问题是错误“TypeError:无法读取未定义的属性(读取“lock”) 在 HTMLDivElement 处。被抛出 - 由我的代码的第 204 行(完整代码如下)引起,其内容如下:
// initialise locks:
const blocker = document.getElementById( 'blocker' );
const instructions = document.getElementById( 'instructions' );
instructions.addEventListener( 'click', function () {
// LINE 204: ERROR
this.controls_.lock;
} );
示例将此锁定义为:
instructions.addEventListener( 'click', function () {
controls.lock();
} );
我知道这个错误源于这样一个事实:在运行时,
this.controls_
未定义。在添加此事件侦听器之前,我创建并分配 this.controls_
。但是当我测试 this.controls_
在调用“this.controls_.lock”时是否未定义时,this.controls_
未定义。
是否有原因导致
this.controls_
在事件侦听器中未定义?侦听器是否需要独立于定义 FirstPersonCamereaDemo
的 controls_
类来运行?
之前,我类似地定义了
instructions.addEventListener( 'click', function () {
this.controls_.lock();
} );
其中
PointerCamera.lock
定义为
lock() {
this.controls.lock();
}
我尝试使用
const = function () {...}
重新定义锁定函数,这导致 PointerLockControls.js 文件中出现类型错误。
任何帮助将不胜感激!
完整的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>
解决方案是将正确的“this”“绑定”到每个新的“lock”函数,以便每个调用都有适当的范围来适当地调用“this.controls_”和“this.controls”。
此处的讨论 addEventListener 中处理函数的范围对于理解事件处理函数无法访问属于类的正确“this”非常有用,而是“this”将引用指令。
更正的定义是:
在“FirstPersonCameraDemo”中:
instructions.addEventListener( 'click', this.controls_.lock2().bind(this));
在“PointerCamear”中:
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)}