// // move.js // // Created by AndrewMeadows, 2014.09.17 // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // // The avatar can be controlled by setting two motor parameters: motorVelocity and motorTimescale. // Once the motorVelocity is set the avatar will try to move in that direction and speed. The // motorTimescale is the approximate amount of time it takes for the avatar to reach within 1/e of its // motorVelocity, so a short timescale makes it ramp up fast, and a long timescale makes it slow. // These parameters control the motor's speed and strength. var MAX_MOTOR_TIMESCALE = 0.5; var PUSHING_MOTOR_TIMESCALE = 0.25; var BRAKING_MOTOR_TIMESCALE = 0.125; var VERY_LONG_TIME = 1000000.0; var AVATAR_SPEED = 4.0; var MIN_BRAKING_SPEED = 0.2; var motorAccumulator = {x:0.0, y:0.0, z:0.0}; var isBraking = false; // There is a bug in Qt-5.3.0 (and below) that prevents QKeyEvent.isAutoRepeat from being correctly set. // This means we can't tell when a held button is actually released -- all we get are the repeated // keyPress- (and keyRelease-) events. So what we have to do is keep a list of last press timestamps // for buttons of interest, and then periodically scan that list for old timestamps and drop any that // have expired (at which point we actually remove that buttons effect on the motor). As long as the // check period is longer than the time between key repeats then things will be smooth, and as long // as the expiry time is short enough then the stop won't feel too laggy. var MAX_AUTO_REPEAT_DELAY = 3; var KEY_RELEASE_EXPIRY_MSEC = 100; // KeyInfo class contructor: function KeyInfo(contribution) { this.motorContribution = contribution; // Vec3 contribution of this key toward motorVelocity this.releaseTime = new Date(); // time when this button was released this.pressTime = new Date(); // time when this button was pressed this.isPressed = false; } // NOTE: the avatar's default orientation is such that "forward" is along the -zAxis, and "left" is along -xAxis. var controlKeys = { "UP" : new KeyInfo({x: 0.0, y: 0.0, z:-1.0}), "DOWN" : new KeyInfo({x: 0.0, y: 0.0, z: 1.0}), "SHIFT+LEFT" : new KeyInfo({x:-1.0, y: 0.0, z: 0.0}), "SHIFT+RIGHT": new KeyInfo({x: 1.0, y: 0.0, z: 0.0}), "w" : new KeyInfo({x: 0.0, y: 0.0, z:-1.0}), "s" : new KeyInfo({x: 0.0, y: 0.0, z: 1.0}), "e" : new KeyInfo({x: 0.0, y: 1.0, z: 0.0}), "c" : new KeyInfo({x: 0.0, y:-1.0, z: 0.0}) }; // list of last timestamps when various buttons were last pressed var pressTimestamps = {}; function keyPressEvent(event) { // NOTE: we're harvesting some of the same keyboard controls that are used by the default (scriptless) // avatar control. The scriptless control can be disabled via the Menu, thereby allowing this script // to be the ONLY controller of the avatar position. var keyName = event.text; if (event.isShifted) { keyName = "SHIFT+" + keyName; } var key = controlKeys[keyName]; if (key != undefined) { key.pressTime = new Date(); // set the last pressTimestap element to undefined (MUCH faster than removing from the list) pressTimestamps[keyName] = undefined; var msec = key.pressTime.valueOf() - key.releaseTime.valueOf(); if (!key.isPressed) { // add this key's effect to the motorAccumulator motorAccumulator = Vec3.sum(motorAccumulator, key.motorContribution); key.isPressed = true; } } } function keyReleaseEvent(event) { var keyName = event.text; if (event.isShifted) { keyName = "SHIFT+" + keyName; } var key = controlKeys[keyName]; if (key != undefined) { // add key to pressTimestamps pressTimestamps[keyName] = new Date(); key.releaseTime = new Date(); var msec = key.releaseTime.valueOf() - key.pressTime.valueOf(); } } function updateMotor(deltaTime) { // remove expired pressTimestamps var now = new Date(); for (var keyName in pressTimestamps) { var t = pressTimestamps[keyName]; if (t != undefined) { var msec = now.valueOf() - t.valueOf(); if (msec > KEY_RELEASE_EXPIRY_MSEC) { // the release of this key is now official, and we remove it from the motorAccumulator motorAccumulator = Vec3.subtract(motorAccumulator, controlKeys[keyName].motorContribution); controlKeys[keyName].isPressed = false; // set the last pressTimestap element to undefined (MUCH faster than removing from the list) pressTimestamps[keyName] = undefined; } } } var motorVelocity = {x:0.0, y:0.0, z:0.0}; // figure out if we're pushing or braking var accumulatorLength = Vec3.length(motorAccumulator); var isPushing = false; if (accumulatorLength == 0.0) { if (!isBraking) { isBraking = true; } isPushing = false; } else { isPushing = true; motorVelocity = Vec3.multiply(AVATAR_SPEED / accumulatorLength, motorAccumulator); } // compute the timescale var motorTimescale = MAX_MOTOR_TIMESCALE; if (isBraking) { var speed = Vec3.length(MyAvatar.getVelocity()); if (speed < MIN_BRAKING_SPEED) { // we're going slow enough to turn off braking // --> we'll drift to a halt, but not so stiffly that we can't be bumped isBraking = false; motorTimescale = MAX_MOTOR_TIMESCALE; } else { // we're still braking motorTimescale = BRAKING_MOTOR_TIMESCALE; } } else if (isPushing) { motorTimescale = PUSHING_MOTOR_TIMESCALE; } // apply the motor parameters MyAvatar.motorVelocity = motorVelocity; MyAvatar.motorTimescale = motorTimescale; } function scriptEnding() { // disable the motor MyAvatar.motorVelocity = {x:0.0, y:0.0, z:0.0} MyAvatar.motorTimescale = VERY_LONG_TIME; } // init stuff MyAvatar.motorReferenceFrame = "camera"; // "camera" is default, other options are "avatar" and "world" MyAvatar.motorTimescale = VERY_LONG_TIME; // connect callbacks Controller.keyPressEvent.connect(keyPressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); Script.update.connect(updateMotor); Script.scriptEnding.connect(scriptEnding)