src/utils/WASDPlusControls.js
import AFRAME from "aframe";
const THREE = AFRAME.THREE;
const bind = AFRAME.utils.bind;
const shouldCaptureKeyEvent = AFRAME.utils.shouldCaptureKeyEvent;
const KEYS = [
"KeyW", "KeyA", "KeyS", "KeyD",
"ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown",
"Space", "ShiftLeft", "ShiftRight"
];
const MAX_DELTA = 0.25;
const CLAMP_VELOCITY = 0.01;
const MOD_Y = 1.5;
/**
* Check if the object is empty or not
*
* @param {object} keys
*/
const isEmptyObject = (keys) => {
let key;
for (key in keys) { return false; }
return true;
};
/**
* @summary Add space and shift controls to the movement
*/
AFRAME.registerComponent("wasd-plus-controls", {
schema: {
acceleration: {type: "number", default : 150},
enabled : {type: "boolean", default : true},
xInverted : {type: "boolean", default : false},
yInverted : {type: "boolean", default : false},
zInverted : {type: "boolean", default : false},
},
init: function() {
//tracks the state of keypresses
this.keys = {};
this.easing = 1.1;
this.velocity = new THREE.Vector3();
this.onBlur = bind(this.onBlur, this);
this.onFocus = bind(this.onFocus, this);
this.onKeyDown = bind(this.onKeyDown, this);
this.onKeyUp = bind(this.onKeyUp, this);
this.onVisibilityChange = bind(this.onVisibilityChange, this);
this.attachVisibilityEventListeners();
},
tick: function (time, delta) {
let el = this.el;
let velocity = this.velocity;
if (!velocity["x"] && !velocity["y"] && !velocity["z"] && isEmptyObject(this.keys)) { return; }
// Update velocity.
delta = delta / 1000;
this.updateVelocity(delta);
if (!velocity["x"] && !velocity["y"] && !velocity["z"]) { return; }
// Get movement vector and translate position.
el.object3D.position.add(this.getMovementVector(delta));
},
remove: function () {
this.removeKeyEventListeners();
this.removeVisibilityEventListeners();
},
play: function () {
this.attachKeyEventListeners();
},
pause: function () {
this.keys = {};
this.removeKeyEventListeners();
},
updateVelocity: function (delta) {
let data = this.data;
let keys = this.keys;
let velocity = this.velocity;
let acceleration;
let xSign;
let ySign;
let zSign;
// If FPS too low, reset velocity.
if (delta > MAX_DELTA) {
velocity["x"] = 0;
velocity["y"] = 0;
velocity["z"] = 0;
return;
}
// https://gamedev.stackexchange.com/questions/151383/frame-rate-independant-movement-with-acceleration
let scaledEasing = Math.pow(1 / this.easing, delta * 60);
// Velocity Easing based on framerate
if (velocity["x"] !== 0) {
velocity["x"] -= velocity["x"] * scaledEasing;
}
if (velocity["y"] !== 0) {
velocity["y"] -= velocity["y"] * scaledEasing;
}
if (velocity["z"] !== 0) {
velocity["z"] -= velocity["z"] * scaledEasing;
}
// Clamp velocity easing.
if (Math.abs(velocity["x"]) < CLAMP_VELOCITY) { velocity["x"] = 0; }
if (Math.abs(velocity["y"]) < CLAMP_VELOCITY) { velocity["y"] = 0; }
if (Math.abs(velocity["z"]) < CLAMP_VELOCITY) { velocity["z"] = 0; }
if (!data.enabled) { return; }
// Update velocity using keys pressed.
acceleration = data.acceleration;
xSign = data.xInverted ? -1 : 1;
if (keys.KeyA || keys.ArrowLeft) { velocity["x"] -= xSign * acceleration * delta; }
if (keys.KeyD || keys.ArrowRight) { velocity["x"] += xSign * acceleration * delta; }
ySign = data.yInverted ? -1 : 1;
if (keys.ShiftLeft || keys.ShiftRight) { velocity["y"] -= ySign * acceleration * delta * MOD_Y; }
if (keys.Space) { velocity["y"] += ySign * acceleration * delta * MOD_Y; }
zSign = data.zInverted ? -1 : 1;
if (keys.KeyW || keys.ArrowUp) { velocity["z"] -= zSign * acceleration * delta; }
if (keys.KeyS || keys.ArrowDown) { velocity["z"] += zSign * acceleration * delta; }
},
getMovementVector: function (delta) {
let directionVector = new THREE.Vector3(0, 0, 0);
let rotationEuler = new THREE.Euler(0, 0, 0);
let rotation = this.el.object3D.rotation;
let velocity = this.velocity;
directionVector.copy(velocity);
directionVector.multiplyScalar(delta);
// Absolute.
if (!rotation) { return directionVector; }
// Transform direction relative to heading.
rotationEuler.set(0, rotation.y, 0);
directionVector.applyEuler(rotationEuler);
return directionVector;
},
attachVisibilityEventListeners: function () {
window.addEventListener("blur", this.onBlur);
window.addEventListener("focus", this.onFocus);
document.addEventListener("visibilitychange", this.onVisibilityChange);
},
removeVisibilityEventListeners: function () {
window.removeEventListener("blur", this.onBlur);
window.removeEventListener("focus", this.onFocus);
document.removeEventListener("visibilitychange", this.onVisibilityChange);
},
attachKeyEventListeners: function () {
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
},
removeKeyEventListeners: function () {
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
},
onBlur: function () {
this.pause();
},
onFocus: function () {
this.play();
},
onVisibilityChange: function () {
if (document.hidden) {
this.onBlur();
} else {
this.onFocus();
}
},
onKeyDown: function (event) {
if (!shouldCaptureKeyEvent(event)) { return; }
let code = event.code;
if (KEYS.includes(code)) { this.keys[code] = true; }
},
onKeyUp: function (event) {
if (!shouldCaptureKeyEvent(event)) { return; }
let code = event.code;
delete this.keys[code];
}
});