2024-03-01 13:35:36 +00:00
|
|
|
/**
|
|
|
|
|
* Movement Controls
|
|
|
|
|
*
|
|
|
|
|
* @author Don McCurdy <dm@donmccurdy.com>
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const COMPONENT_SUFFIX = '-controls';
|
|
|
|
|
const MAX_DELTA = 0.2; // ms
|
|
|
|
|
const EPS = 10e-6;
|
|
|
|
|
const MOVED = 'moved';
|
|
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
if( !AFRAME.components['movement-controls'] ){
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
AFRAME.registerComponent('movement-controls', {
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
/*******************************************************************
|
|
|
|
|
* Schema
|
|
|
|
|
*/
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
dependencies: ['rotation'],
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
schema: {
|
|
|
|
|
enabled: { default: true },
|
|
|
|
|
controls: { default: ['gamepad', 'trackpad', 'keyboard', 'touch'] },
|
|
|
|
|
speed: { default: 0.3, min: 0 },
|
|
|
|
|
fly: { default: false },
|
|
|
|
|
constrainToNavMesh: { default: false },
|
|
|
|
|
camera: { default: '[movement-controls] [camera]', type: 'selector' }
|
|
|
|
|
},
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
/*******************************************************************
|
|
|
|
|
* Lifecycle
|
|
|
|
|
*/
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
init: function () {
|
2024-03-01 13:35:36 +00:00
|
|
|
const el = this.el;
|
2024-12-09 15:42:34 +00:00
|
|
|
if (!this.data.camera) {
|
|
|
|
|
this.data.camera = el.querySelector('[camera]');
|
|
|
|
|
}
|
|
|
|
|
this.velocityCtrl = null;
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
this.velocity = new THREE.Vector3();
|
|
|
|
|
this.heading = new THREE.Quaternion();
|
|
|
|
|
this.eventDetail = {};
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
// Navigation
|
|
|
|
|
this.navGroup = null;
|
|
|
|
|
this.navNode = null;
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
if (el.sceneEl.hasLoaded) {
|
|
|
|
|
this.injectControls();
|
2024-03-01 13:35:36 +00:00
|
|
|
} else {
|
2024-12-09 15:42:34 +00:00
|
|
|
el.sceneEl.addEventListener('loaded', this.injectControls.bind(this));
|
2024-03-01 13:35:36 +00:00
|
|
|
}
|
2024-12-09 15:42:34 +00:00
|
|
|
},
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
update: function (prevData) {
|
|
|
|
|
const el = this.el;
|
|
|
|
|
const data = this.data;
|
|
|
|
|
const nav = el.sceneEl.systems.nav;
|
|
|
|
|
if (el.sceneEl.hasLoaded) {
|
|
|
|
|
this.injectControls();
|
|
|
|
|
}
|
|
|
|
|
if (nav && data.constrainToNavMesh !== prevData.constrainToNavMesh) {
|
|
|
|
|
data.constrainToNavMesh
|
|
|
|
|
? nav.addAgent(this)
|
|
|
|
|
: nav.removeAgent(this);
|
|
|
|
|
}
|
|
|
|
|
if (data.enabled !== prevData.enabled) {
|
|
|
|
|
// Propagate the enabled change to all controls
|
|
|
|
|
for (let i = 0; i < data.controls.length; i++) {
|
|
|
|
|
const name = data.controls[i] + COMPONENT_SUFFIX;
|
|
|
|
|
this.el.setAttribute(name, { enabled: this.data.enabled });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
injectControls: function () {
|
|
|
|
|
const data = this.data;
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
for (let i = 0; i < data.controls.length; i++) {
|
|
|
|
|
const name = data.controls[i] + COMPONENT_SUFFIX;
|
|
|
|
|
this.el.setAttribute(name, { enabled: this.data.enabled });
|
2024-03-01 13:35:36 +00:00
|
|
|
}
|
2024-12-09 15:42:34 +00:00
|
|
|
},
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
updateNavLocation: function () {
|
|
|
|
|
this.navGroup = null;
|
|
|
|
|
this.navNode = null;
|
|
|
|
|
},
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
/*******************************************************************
|
|
|
|
|
* Tick
|
|
|
|
|
*/
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
tick: (function () {
|
|
|
|
|
const start = new THREE.Vector3();
|
|
|
|
|
const end = new THREE.Vector3();
|
|
|
|
|
const clampedEnd = new THREE.Vector3();
|
|
|
|
|
|
|
|
|
|
return function (t, dt) {
|
|
|
|
|
if (!dt) return;
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
const el = this.el;
|
|
|
|
|
const data = this.data;
|
|
|
|
|
|
|
|
|
|
if (!data.enabled) return;
|
|
|
|
|
|
|
|
|
|
this.updateVelocityCtrl();
|
|
|
|
|
const velocityCtrl = this.velocityCtrl;
|
|
|
|
|
const velocity = this.velocity;
|
|
|
|
|
|
|
|
|
|
if (!velocityCtrl) return;
|
|
|
|
|
|
|
|
|
|
// Update velocity. If FPS is too low, reset.
|
|
|
|
|
if (dt / 1000 > MAX_DELTA) {
|
|
|
|
|
velocity.set(0, 0, 0);
|
2024-03-01 13:35:36 +00:00
|
|
|
} else {
|
2024-12-09 15:42:34 +00:00
|
|
|
this.updateVelocity(dt);
|
2024-03-01 13:35:36 +00:00
|
|
|
}
|
|
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
if (data.constrainToNavMesh
|
|
|
|
|
&& velocityCtrl.isNavMeshConstrained !== false) {
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
if (velocity.lengthSq() < EPS) return;
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
start.copy(el.object3D.position);
|
|
|
|
|
end
|
|
|
|
|
.copy(velocity)
|
|
|
|
|
.multiplyScalar(dt / 1000)
|
|
|
|
|
.add(start);
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
const nav = el.sceneEl.systems.nav;
|
|
|
|
|
this.navGroup = this.navGroup === null ? nav.getGroup(start) : this.navGroup;
|
|
|
|
|
this.navNode = this.navNode || nav.getNode(start, this.navGroup);
|
|
|
|
|
this.navNode = nav.clampStep(start, end, this.navGroup, this.navNode, clampedEnd);
|
|
|
|
|
el.object3D.position.copy(clampedEnd);
|
|
|
|
|
} else if (el.hasAttribute('velocity')) {
|
|
|
|
|
el.setAttribute('velocity', velocity);
|
2024-03-01 13:35:36 +00:00
|
|
|
} else {
|
2024-12-09 15:42:34 +00:00
|
|
|
el.object3D.position.x += velocity.x * dt / 1000;
|
|
|
|
|
el.object3D.position.y += velocity.y * dt / 1000;
|
|
|
|
|
el.object3D.position.z += velocity.z * dt / 1000;
|
2024-03-01 13:35:36 +00:00
|
|
|
}
|
2024-12-09 15:42:34 +00:00
|
|
|
|
|
|
|
|
};
|
|
|
|
|
}()),
|
|
|
|
|
|
|
|
|
|
/*******************************************************************
|
|
|
|
|
* Movement
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
updateVelocityCtrl: function () {
|
|
|
|
|
const data = this.data;
|
|
|
|
|
if (data.enabled) {
|
|
|
|
|
for (let i = 0, l = data.controls.length; i < l; i++) {
|
|
|
|
|
const control = this.el.components[data.controls[i] + COMPONENT_SUFFIX];
|
|
|
|
|
if (control && control.isVelocityActive()) {
|
|
|
|
|
this.velocityCtrl = control;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-03-01 13:35:36 +00:00
|
|
|
}
|
2024-12-09 15:42:34 +00:00
|
|
|
this.velocityCtrl = null;
|
2024-03-01 13:35:36 +00:00
|
|
|
}
|
2024-12-09 15:42:34 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateVelocity: (function () {
|
|
|
|
|
const vector2 = new THREE.Vector2();
|
|
|
|
|
const quaternion = new THREE.Quaternion();
|
|
|
|
|
|
|
|
|
|
return function (dt) {
|
|
|
|
|
let dVelocity;
|
|
|
|
|
const el = this.el;
|
|
|
|
|
const control = this.velocityCtrl;
|
|
|
|
|
const velocity = this.velocity;
|
|
|
|
|
const data = this.data;
|
|
|
|
|
|
|
|
|
|
if (control) {
|
|
|
|
|
if (control.getVelocityDelta) {
|
|
|
|
|
dVelocity = control.getVelocityDelta(dt);
|
|
|
|
|
} else if (control.getVelocity) {
|
|
|
|
|
velocity.copy(control.getVelocity());
|
|
|
|
|
return;
|
|
|
|
|
} else if (control.getPositionDelta) {
|
|
|
|
|
velocity.copy(control.getPositionDelta(dt).multiplyScalar(1000 / dt));
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('Incompatible movement controls: ', control);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (el.hasAttribute('velocity') && !data.constrainToNavMesh) {
|
|
|
|
|
velocity.copy(this.el.getAttribute('velocity'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dVelocity && data.enabled) {
|
|
|
|
|
const cameraEl = data.camera;
|
|
|
|
|
|
|
|
|
|
// Rotate to heading
|
|
|
|
|
quaternion.copy(cameraEl.object3D.quaternion);
|
|
|
|
|
quaternion.premultiply(el.object3D.quaternion);
|
|
|
|
|
dVelocity.applyQuaternion(quaternion);
|
|
|
|
|
|
|
|
|
|
const factor = dVelocity.length();
|
|
|
|
|
if (data.fly) {
|
|
|
|
|
velocity.copy(dVelocity);
|
|
|
|
|
velocity.multiplyScalar(this.data.speed * 16.66667);
|
|
|
|
|
} else {
|
|
|
|
|
vector2.set(dVelocity.x, dVelocity.z);
|
|
|
|
|
vector2.setLength(factor * this.data.speed * 16.66667);
|
|
|
|
|
velocity.x = vector2.x;
|
|
|
|
|
velocity.y = 0;
|
|
|
|
|
velocity.z = vector2.y;
|
|
|
|
|
}
|
|
|
|
|
if (velocity.x !== 0 || velocity.y !== 0 || velocity.z !== 0) {
|
|
|
|
|
this.eventDetail.velocity = velocity;
|
|
|
|
|
this.el.emit(MOVED, this.eventDetail);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-03-01 13:35:36 +00:00
|
|
|
|
2024-12-09 15:42:34 +00:00
|
|
|
}())
|
|
|
|
|
});
|
|
|
|
|
}
|