diff --git a/examples/swissArmyJetpack.js b/examples/swissArmyJetpack.js index 9bb5bea267..a7fab772dc 100644 --- a/examples/swissArmyJetpack.js +++ b/examples/swissArmyJetpack.js @@ -12,7 +12,65 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var numberOfButtons = 3; +// misc global constants +var NUMBER_OF_BUTTONS = 3; +var DOWN = { x: 0.0, y: -1.0, z: 0.0 }; +var MAX_VOXEL_SCAN_DISTANCE = 20.0; + +// behavior transition thresholds +var MIN_FLYING_SPEED = 1.0; +var MIN_COLLISIONLESS_SPEED = 5.0; +var MAX_WALKING_SPEED = 10.0; +var MAX_COLLIDABLE_SPEED = 25.0; + +// button URL and geometry/UI tuning +var BUTTON_IMAGE_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/testing-swatches.svg"; +var DISABLED_OFFSET_Y = 12; +var ENABLED_OFFSET_Y = 55 + 12; +var UI_BUFFER = 1; +var OFFSET_X = UI_BUFFER; +var OFFSET_Y = 200; +var BUTTON_WIDTH = 30; +var BUTTON_HEIGHT = 30; +var TEXT_OFFSET_X = OFFSET_X + BUTTON_WIDTH + UI_BUFFER; +var TEXT_HEIGHT = BUTTON_HEIGHT; +var TEXT_WIDTH = 210; + +var MSEC_PER_SECOND = 1000; +var EXPIRY_PERIOD = 2 * MSEC_PER_SECOND; + +var dater = new Date(); +var collisionOnExpiry = dater.getTime() + EXPIRY_PERIOD; +var gravityOnExpiry = dater.getTime() + EXPIRY_PERIOD; + +// avatar state +var velocity = { x: 0.0, y: 0.0, z: 0.0 }; +var standing = false; + +// speedometer globals +var speed = 0.0; +var lastPosition = MyAvatar.position; +var speedometer = Overlays.addOverlay("text", { + x: OFFSET_X, + y: OFFSET_Y - BUTTON_HEIGHT, + width: BUTTON_WIDTH + UI_BUFFER + TEXT_WIDTH, + height: TEXT_HEIGHT, + color: { red: 0, green: 0, blue: 0 }, + textColor: { red: 255, green: 0, blue: 0}, + topMargin: 4, + leftMargin: 4, + text: "Speed: 0.0" + }); + +// collision group buttons +var buttons = new Array(); +var labels = new Array(); + +var labelContents = new Array(); +labelContents[0] = "Collide with Avatars"; +labelContents[1] = "Collide with Voxels"; +labelContents[2] = "Collide with Particles"; +var groupBits = 0; var enabledColors = new Array(); enabledColors[0] = { red: 255, green: 0, blue: 0}; @@ -24,47 +82,28 @@ disabledColors[0] = { red: 90, green: 75, blue: 75}; disabledColors[1] = { red: 75, green: 90, blue: 75}; disabledColors[2] = { red: 75, green: 90, blue: 90}; -var buttons = new Array(); -var labels = new Array(); - -var labelContents = new Array(); -labelContents[0] = "Collide with Avatars"; -labelContents[1] = "Collide with Voxels"; -labelContents[2] = "Collide with Particles"; -var groupBits = 0; - var buttonStates = new Array(); -var disabledOffsetT = 0; -var enabledOffsetT = 55; - -var buttonX = 50; -var buttonY = 200; -var buttonWidth = 30; -var buttonHeight = 54; -var textX = buttonX + buttonWidth + 10; - -for (i = 0; i < numberOfButtons; i++) { +for (i = 0; i < NUMBER_OF_BUTTONS; i++) { var offsetS = 12 - var offsetT = disabledOffsetT; + var offsetT = DISABLED_OFFSET_Y; buttons[i] = Overlays.addOverlay("image", { - //x: buttonX + (buttonWidth * i), - x: buttonX, - y: buttonY + (buttonHeight * i), - width: buttonWidth, - height: buttonHeight, - subImage: { x: offsetS, y: offsetT, width: buttonWidth, height: buttonHeight }, - imageURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/testing-swatches.svg", + x: OFFSET_X, + y: OFFSET_Y + (BUTTON_HEIGHT * i), + width: BUTTON_WIDTH, + height: BUTTON_HEIGHT, + subImage: { x: offsetS, y: offsetT, width: BUTTON_WIDTH, height: BUTTON_HEIGHT }, + imageURL: BUTTON_IMAGE_URL, color: disabledColors[i], alpha: 1, }); labels[i] = Overlays.addOverlay("text", { - x: textX, - y: buttonY + (buttonHeight * i) + 12, - width: 150, - height: 50, + x: TEXT_OFFSET_X, + y: OFFSET_Y + (BUTTON_HEIGHT * i), + width: TEXT_WIDTH, + height: TEXT_HEIGHT, color: { red: 0, green: 0, blue: 0}, textColor: { red: 255, green: 0, blue: 0}, topMargin: 4, @@ -75,12 +114,14 @@ for (i = 0; i < numberOfButtons; i++) { buttonStates[i] = false; } + +// functions + function updateButton(i, enabled) { - var offsetY = disabledOffsetT; + var offsetY = DISABLED_OFFSET_Y; var buttonColor = disabledColors[i]; - groupBits if (enabled) { - offsetY = enabledOffsetT; + offsetY = ENABLED_OFFSET_Y; buttonColor = enabledColors[i]; if (i == 0) { groupBits |= COLLISION_GROUP_AVATARS; @@ -98,24 +139,33 @@ function updateButton(i, enabled) { groupBits &= ~COLLISION_GROUP_PARTICLES; } } - MyAvatar.collisionGroups = groupBits; + if (groupBits != MyAvatar.collisionGroups) { + MyAvatar.collisionGroups = groupBits; + } Overlays.editOverlay(buttons[i], { subImage: { y: offsetY } } ); Overlays.editOverlay(buttons[i], { color: buttonColor } ); buttonStates[i] = enabled; } + // When our script shuts down, we should clean up all of our overlays function scriptEnding() { - for (i = 0; i < numberOfButtons; i++) { - print("adebug deleting overlay " + i); + for (i = 0; i < NUMBER_OF_BUTTONS; i++) { Overlays.deleteOverlay(buttons[i]); Overlays.deleteOverlay(labels[i]); } + Overlays.deleteOverlay(speedometer); } Script.scriptEnding.connect(scriptEnding); +function updateSpeedometerDisplay() { + Overlays.editOverlay(speedometer, { text: "Speed: " + speed.toFixed(2) }); +} +Script.setInterval(updateSpeedometerDisplay, 100); + + // Our update() function is called at approximately 60fps, and we will use it to animate our various overlays function update(deltaTime) { if (groupBits != MyAvatar.collisionGroups) { @@ -124,6 +174,61 @@ function update(deltaTime) { updateButton(1, groupBits & COLLISION_GROUP_VOXELS); updateButton(2, groupBits & COLLISION_GROUP_PARTICLES); } + + // measure speed + var distance = Vec3.distance(MyAvatar.position, lastPosition); + speed = 0.8 * speed + 0.2 * distance / deltaTime; + lastPosition = MyAvatar.position; + + dater = new Date(); + var now = dater.getTime(); + + if (speed < MIN_FLYING_SPEED) { + // scan for landing platform + ray = { origin: MyAvatar.position, direction: DOWN }; + var intersection = Voxels.findRayIntersection(ray); + if (intersection.intersects) { + if (!(MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY)) { + var v = intersection.voxel; + var maxCorner = Vec3.sum({ x: v.x, y: v.y, z: v.z }, {x: v.s, y: v.s, z: v.s }); + var distance = lastPosition.y - maxCorner.y; + if ((gravityOnExpiry < now) && (distance < MAX_VOXEL_SCAN_DISTANCE)) { + // NOTE: setting the gravity automatically sets the AVATAR_MOTION_OBEY_LOCAL_GRAVITY behavior bit. + MyAvatar.gravity = DOWN; + } + } + } else { + if (MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) { + MyAvatar.motionBehaviors = MyAvatar.motionBehaviors & ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY; + } + gravityOnExpiry = now + EXPIRY_PERIOD; + } + } else { + gravityOnExpiry = now + EXPIRY_PERIOD; + } + if (speed < MIN_COLLISIONLESS_SPEED) { + if (collisionOnExpiry < now && !(MyAvatar.collisionGroups & COLLISION_GROUP_VOXELS)) { + // TODO: check to make sure not already colliding + // enable collision with voxels + groupBits |= COLLISION_GROUP_VOXELS; + updateButton(1, groupBits & COLLISION_GROUP_VOXELS); + } + } else { + collisionOnExpiry = now + EXPIRY_PERIOD; + } + if (speed > MAX_WALKING_SPEED) { + if (MyAvatar.motionBehaviors & AVATAR_MOTION_OBEY_LOCAL_GRAVITY) { + // turn off gravity + MyAvatar.motionBehaviors = MyAvatar.motionBehaviors & ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY; + } + } + if (speed > MAX_COLLIDABLE_SPEED) { + if (MyAvatar.collisionGroups & COLLISION_GROUP_VOXELS) { + // disable collisions with voxels + groupBits &= ~COLLISION_GROUP_VOXELS; + updateButton(1, groupBits & COLLISION_GROUP_VOXELS); + } + } } Script.update.connect(update); @@ -131,7 +236,7 @@ Script.update.connect(update); // we also handle click detection in our mousePressEvent() function mousePressEvent(event) { var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); - for (i = 0; i < numberOfButtons; i++) { + for (i = 0; i < NUMBER_OF_BUTTONS; i++) { if (clickedOverlay == buttons[i]) { var enabled = !(buttonStates[i]); updateButton(i, enabled); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 97b5c05f25..558ba31d80 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -857,7 +857,7 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_G: if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::ObeyGravity); + Menu::getInstance()->triggerOption(MenuOption::ObeyEnvironmentalGravity); } break; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 44117df55c..ff3c6a1f43 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -188,8 +188,8 @@ Menu::Menu() : addDisabledActionAndSeparator(editMenu, "Physics"); QObject* avatar = appInstance->getAvatar(); - addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ObeyGravity, Qt::SHIFT | Qt::Key_G, true, - avatar, SLOT(updateMotionBehaviorFlags())); + addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ObeyEnvironmentalGravity, Qt::SHIFT | Qt::Key_G, true, + avatar, SLOT(updateMotionBehaviors())); addAvatarCollisionSubMenu(editMenu); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index bc70f8f83f..bbd3aa13ae 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -315,7 +315,7 @@ namespace MenuOption { const QString GoTo = "Go To..."; const QString GoToDomain = "Go To Domain..."; const QString GoToLocation = "Go To Location..."; - const QString ObeyGravity = "Obey Gravity"; + const QString ObeyEnvironmentalGravity = "Obey Environmental Gravity"; const QString HandsCollideWithSelf = "Collide With Self"; const QString HeadMouse = "Head Mouse"; const QString IncreaseAvatarSize = "Increase Avatar Size"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 26dbc09d5b..40e350dcb7 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -125,10 +125,8 @@ void MyAvatar::update(float deltaTime) { head->setAudioLoudness(audio->getLastInputLoudness()); head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness()); - if (_motionBehaviors & AVATAR_MOTION_OBEY_GRAVITY) { + if (_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY) { setGravity(Application::getInstance()->getEnvironment()->getGravity(getPosition())); - } else { - setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); } simulate(deltaTime); @@ -463,6 +461,27 @@ void MyAvatar::renderHeadMouse() const { */ } +void MyAvatar::setLocalGravity(glm::vec3 gravity) { + _motionBehaviors |= AVATAR_MOTION_OBEY_LOCAL_GRAVITY; + // Environmental and Local gravities are incompatible. Since Local is being set here + // the environmental setting must be removed. + _motionBehaviors &= ~AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY; + setGravity(gravity); +} + +void MyAvatar::setGravity(const glm::vec3& gravity) { + _gravity = gravity; + getHead()->setGravity(_gravity); + + // use the gravity to determine the new world up direction, if possible + float gravityLength = glm::length(gravity); + if (gravityLength > EPSILON) { + _worldUpDirection = _gravity / -gravityLength; + } else { + _worldUpDirection = DEFAULT_UP_DIRECTION; + } +} + void MyAvatar::saveData(QSettings* settings) { settings->beginGroup("Avatar"); @@ -1046,19 +1065,6 @@ void MyAvatar::maybeUpdateBillboard() { sendBillboardPacket(); } -void MyAvatar::setGravity(glm::vec3 gravity) { - _gravity = gravity; - getHead()->setGravity(_gravity); - - // use the gravity to determine the new world up direction, if possible - float gravityLength = glm::length(gravity); - if (gravityLength > EPSILON) { - _worldUpDirection = _gravity / -gravityLength; - } else { - _worldUpDirection = DEFAULT_UP_DIRECTION; - } -} - void MyAvatar::goHome() { qDebug("Going Home!"); setPosition(START_LOCATION); @@ -1147,8 +1153,13 @@ void MyAvatar::goToLocationFromResponse(const QJsonObject& jsonObject) { void MyAvatar::updateMotionBehaviors() { _motionBehaviors = 0; - if (Menu::getInstance()->isOptionChecked(MenuOption::ObeyGravity)) { - _motionBehaviors |= AVATAR_MOTION_OBEY_GRAVITY; + if (Menu::getInstance()->isOptionChecked(MenuOption::ObeyEnvironmentalGravity)) { + _motionBehaviors |= AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY; + // Environmental and Local gravities are incompatible. Environmental setting trumps local. + _motionBehaviors &= ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY; + } + if (! (_motionBehaviors & (AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY | AVATAR_MOTION_OBEY_LOCAL_GRAVITY))) { + setGravity(glm::vec3(0.0f)); } } @@ -1164,7 +1175,14 @@ void MyAvatar::setCollisionGroups(quint32 collisionGroups) { void MyAvatar::setMotionBehaviors(quint32 flags) { _motionBehaviors = flags; Menu* menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::ObeyGravity, (bool)(_motionBehaviors & AVATAR_MOTION_OBEY_GRAVITY)); + menu->setIsOptionChecked(MenuOption::ObeyEnvironmentalGravity, (bool)(_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY)); + // Environmental and Local gravities are incompatible. Environmental setting trumps local. + if (_motionBehaviors & AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY) { + _motionBehaviors &= ~AVATAR_MOTION_OBEY_LOCAL_GRAVITY; + setGravity(Application::getInstance()->getEnvironment()->getGravity(getPosition())); + } else if (! (_motionBehaviors & (AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY | AVATAR_MOTION_OBEY_LOCAL_GRAVITY))) { + setGravity(glm::vec3(0.0f)); + } } void MyAvatar::applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 9b6b13568f..a5312b0016 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -25,12 +25,11 @@ enum AvatarHandState NUM_HAND_STATES }; -const quint32 AVATAR_MOTION_OBEY_GRAVITY = 1U << 0; - class MyAvatar : public Avatar { Q_OBJECT Q_PROPERTY(bool shouldRenderLocally READ getShouldRenderLocally WRITE setShouldRenderLocally) Q_PROPERTY(quint32 motionBehaviors READ getMotionBehaviors WRITE setMotionBehaviors) + Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setLocalGravity) public: MyAvatar(); @@ -52,8 +51,7 @@ public: void setMousePressed(bool mousePressed) { _mousePressed = mousePressed; } void setVelocity(const glm::vec3 velocity) { _velocity = velocity; } void setLeanScale(float scale) { _leanScale = scale; } - void setGravity(glm::vec3 gravity); - void setMoveTarget(const glm::vec3 moveTarget); + void setLocalGravity(glm::vec3 gravity); void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; } // getters @@ -123,6 +121,7 @@ private: bool _shouldJump; float _driveKeys[MAX_DRIVE_KEYS]; glm::vec3 _gravity; + glm::vec3 _environmentGravity; float _distanceToNearestAvatar; // How close is the nearest avatar? // motion stuff @@ -151,6 +150,7 @@ private: void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void updateChatCircle(float deltaTime); void maybeUpdateBillboard(); + void setGravity(const glm::vec3& gravity); }; #endif // hifi_MyAvatar_h diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 250ae07b01..be47aed1ba 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -50,6 +50,10 @@ typedef unsigned long long quint64; #include "HeadData.h" #include "HandData.h" +// avatar motion behaviors +const quint32 AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY = 1U << 0; +const quint32 AVATAR_MOTION_OBEY_LOCAL_GRAVITY = 1U << 1; + // First bitset const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index ed05658538..b8b755e099 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -239,6 +239,9 @@ void ScriptEngine::init() { globalObject.setProperty("COLLISION_GROUP_VOXELS", _engine.newVariant(QVariant(COLLISION_GROUP_VOXELS))); globalObject.setProperty("COLLISION_GROUP_PARTICLES", _engine.newVariant(QVariant(COLLISION_GROUP_PARTICLES))); + globalObject.setProperty("AVATAR_MOTION_OBEY_LOCAL_GRAVITY", _engine.newVariant(QVariant(AVATAR_MOTION_OBEY_LOCAL_GRAVITY))); + globalObject.setProperty("AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY", _engine.newVariant(QVariant(AVATAR_MOTION_OBEY_ENVIRONMENTAL_GRAVITY))); + // let the VoxelPacketSender know how frequently we plan to call it _voxelsScriptingInterface.getVoxelPacketSender()->setProcessCallIntervalHint(SCRIPT_DATA_CALLBACK_USECS); _particlesScriptingInterface.getParticlePacketSender()->setProcessCallIntervalHint(SCRIPT_DATA_CALLBACK_USECS); diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index bab529df1f..badc980913 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -36,6 +36,10 @@ float Vec3::length(const glm::vec3& v) { return glm::length(v); } +float Vec3::distance(const glm::vec3& v1, const glm::vec3& v2) { + return glm::distance(v1, v2); +} + glm::vec3 Vec3::normalize(const glm::vec3& v) { return glm::normalize(v); } diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index b30e274d89..e401cd71bd 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -31,6 +31,7 @@ public slots: glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2); float length(const glm::vec3& v); + float distance(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 normalize(const glm::vec3& v); void print(const QString& lable, const glm::vec3& v); };