Newer
Older
Kevin Lee
committed
const EPS = 10e-6;
joni
committed
/**
* Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly.
* The controller accounts for playspace offset and orientation and depends on the nav mesh system for translation.
* @namespace avatar
* @component character-controller
*/
joni
committed
AFRAME.registerComponent("character-controller", {
schema: {
joni
committed
pivot: { type: "selector" },
snapRotationDegrees: { default: THREE.Math.DEG2RAD * 45 },
joni
committed
},
init: function() {
this.navZone = "character";
this.navGroup = null;
this.navNode = null;
joni
committed
this.velocity = new THREE.Vector3(0, 0, 0);
this.accelerationInput = new THREE.Vector3(0, 0, 0);
this.pendingSnapRotationMatrix = new THREE.Matrix4();
this.angularVelocity = 0; // Scalar value because we only allow rotation around Y
this._withinWarningLimit = true;
this._warningCount = 0;
this.snapRotateLeft = this.snapRotateLeft.bind(this);
this.snapRotateRight = this.snapRotateRight.bind(this);
this.setAngularVelocity = this.setAngularVelocity.bind(this);
this.handleTeleport = this.handleTeleport.bind(this);
joni
committed
},
update: function() {
this.leftRotationMatrix = new THREE.Matrix4().makeRotationY(this.data.snapRotationDegrees);
this.rightRotationMatrix = new THREE.Matrix4().makeRotationY(-this.data.snapRotationDegrees);
joni
committed
},
play: function() {
eventSrc.addEventListener("rotateY", this.setAngularVelocity);
eventSrc.addEventListener("teleported", this.handleTeleport);
joni
committed
},
pause: function() {
eventSrc.removeEventListener("rotateY", this.setAngularVelocity);
eventSrc.removeEventListener("teleported", this.handleTeleport);
this.reset();
},
reset() {
this.accelerationInput.set(0, 0, 0);
this.velocity.set(0, 0, 0);
this.angularVelocity = 0;
this.pendingSnapRotationMatrix.identity();
joni
committed
},
const axes = event.detail.axis;
this.accelerationInput.set(axes[0], 0, axes[1]);
},
setAngularVelocity: function(event) {
this.angularVelocity = event.detail.value;
joni
committed
},
joni
committed
this.pendingSnapRotationMatrix.copy(this.leftRotationMatrix);
joni
committed
},
joni
committed
this.pendingSnapRotationMatrix.copy(this.rightRotationMatrix);
joni
committed
},
Kevin Lee
committed
const position = event.detail.newPosition;
const navPosition = event.detail.hitPoint;
this.resetPositionOnNavMesh(position, navPosition, this.el.object3D);
joni
committed
tick: (function() {
const move = new THREE.Matrix4();
const trans = new THREE.Matrix4();
const transInv = new THREE.Matrix4();
const pivotPos = new THREE.Vector3();
const rotationAxis = new THREE.Vector3(0, 1, 0);
const yawMatrix = new THREE.Matrix4();
const rotationMatrix = new THREE.Matrix4();
const rotationInvMatrix = new THREE.Matrix4();
const pivotRotationMatrix = new THREE.Matrix4();
const pivotRotationInvMatrix = new THREE.Matrix4();
const startPos = new THREE.Vector3();
const startScale = new THREE.Vector3();
joni
committed
return function(t, dt) {
if (!this.el.sceneEl.is("entered")) return;
joni
committed
const deltaSeconds = dt / 1000;
const root = this.el.object3D;
const pivot = this.data.pivot.object3D;
const distance = this.data.groundAcc * deltaSeconds;
const rotationDelta = this.data.rotationSpeed * this.angularVelocity * deltaSeconds;
joni
committed
startScale.copy(root.scale);
startPos.copy(root.position);
// Other aframe components like teleport-controls set position/rotation/scale, not the matrix, so we need to make sure to compose them back into the matrix
root.updateMatrix();
if (userinput.get(paths.actions.snapRotateLeft)) {
if (userinput.get(paths.actions.snapRotateRight)) {
const acc = userinput.get(paths.actions.characterAcceleration);
this.accelerationInput.set(
this.accelerationInput.x + acc[0],
this.accelerationInput.y + 0,
this.accelerationInput.z + acc[1]
);
joni
committed
pivotPos.copy(pivot.position);
pivotPos.applyMatrix4(root.matrix);
trans.setPosition(pivotPos);
transInv.makeTranslation(-pivotPos.x, -pivotPos.y, -pivotPos.z);
rotationMatrix.makeRotationAxis(rotationAxis, root.rotation.y);
rotationInvMatrix.makeRotationAxis(rotationAxis, -root.rotation.y);
pivotRotationMatrix.makeRotationAxis(rotationAxis, pivot.rotation.y);
pivotRotationInvMatrix.makeRotationAxis(rotationAxis, -pivot.rotation.y);
this.updateVelocity(deltaSeconds, pivot);
this.accelerationInput.set(0, 0, 0);
const boost = userinput.get(paths.actions.boost) ? 2 : 1;
this.velocity.x * distance * boost,
this.velocity.y * distance * boost,
this.velocity.z * distance * boost
joni
committed
// Translate to middle of playspace (player rig)
root.matrix.premultiply(transInv);
joni
committed
// Zero playspace (player rig) rotation
root.matrix.premultiply(rotationInvMatrix);
joni
committed
// Zero pivot (camera/head) rotation
root.matrix.premultiply(pivotRotationInvMatrix);
joni
committed
// Apply joystick translation
root.matrix.premultiply(move);
joni
committed
// Apply joystick yaw rotation
root.matrix.premultiply(yawMatrix);
joni
committed
// Apply snap rotation if necessary
root.matrix.premultiply(this.pendingSnapRotationMatrix);
joni
committed
// Reapply pivot (camera/head) rotation
root.matrix.premultiply(pivotRotationMatrix);
joni
committed
// Reapply playspace (player rig) rotation
root.matrix.premultiply(rotationMatrix);
// Reapply playspace (player rig) translation
root.matrix.premultiply(trans);
// update pos/rot/scale
root.matrix.decompose(root.position, root.quaternion, root.scale);
joni
committed
// TODO: the above matrix trnsfomraitons introduce some floating point errors in scale, this reverts them to
// avoid spamming network with fake scale updates
root.scale.copy(startScale);
joni
committed
this.pendingSnapRotationMatrix.identity(); // Revert to identity
Kevin Lee
committed
if (this.velocity.lengthSq() > EPS && !this.data.fly) {
this.setPositionOnNavMesh(startPos, root.position, root);
Kevin Lee
committed
}
joni
committed
};
})(),
_warnWithWarningLimit: function(msg) {
if (!this._withinWarningLimit) return;
this._warningCount++;
if (this._warningCount > MAX_WARNINGS) {
this._withinWarningLimit = false;
msg = "Warning count exceeded. Will not log further warnings";
}
console.warn("character-controller", msg);
},
_setNavNode: function(pos) {
if (this.navNode !== null) return;
this.navNode =
pathfinder.getClosestNode(pos, this.navZone, this.navGroup, true) ||
pathfinder.getClosestNode(pos, this.navZone, this.navGroup);
setPositionOnNavMesh: function(start, end, object3D) {
const { pathfinder } = this.el.sceneEl.systems.nav;
if (!(this.navZone in pathfinder.zones)) return;
if (this.navGroup === null) {
this.navGroup = pathfinder.getGroup(this.navZone, end, true, true);
}
this._setNavNode(end);
this.navNode = pathfinder.clampStep(start, end, this.navNode, this.navZone, this.navGroup, object3D.position);
},
Kevin Lee
committed
resetPositionOnNavMesh: function(position, navPosition, object3D) {
const { pathfinder } = this.el.sceneEl.systems.nav;
if (!(this.navZone in pathfinder.zones)) return;
this.navGroup = pathfinder.getGroup(this.navZone, navPosition, true, true);
this.navNode = null;
this._setNavNode(navPosition);
pathfinder.clampStep(position, navPosition, this.navNode, this.navZone, this.navGroup, object3D.position);
Kevin Lee
committed
},
const data = this.data;
const velocity = this.velocity;
joni
committed
// If FPS too low, reset velocity.
if (dt > MAX_DELTA) {
velocity.x = 0;
joni
committed
velocity.z = 0;
return;
}
// Decay velocity.
if (velocity.x !== 0) {
velocity.x -= velocity.x * data.easing * dt;
}
if (velocity.y !== 0) {
velocity.y -= velocity.y * data.easing * dt;
}
if (velocity.z !== 0) {
velocity.z -= velocity.z * data.easing * dt;
}
joni
committed
const dvx = data.groundAcc * dt * this.accelerationInput.x;
const dvz = data.groundAcc * dt * -this.accelerationInput.z;
velocity.x += dvx;
if (this.data.fly) {
const pitch = pivot.rotation.x / PI_2;
velocity.y += dvz * -pitch;
velocity.z += dvz * (1.0 - pitch);
} else {
velocity.z += dvz;
}
const decay = 0.7;
this.accelerationInput.x = this.accelerationInput.x * decay;
this.accelerationInput.z = this.accelerationInput.z * decay;
joni
committed
if (Math.abs(velocity.x) < CLAMP_VELOCITY) {
velocity.x = 0;
}
if (this.data.fly && Math.abs(velocity.y) < CLAMP_VELOCITY) {