diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js index 5705bd4498..f4a6d1790d 100644 --- a/examples/controllers/handControllerGrab.js +++ b/examples/controllers/handControllerGrab.js @@ -87,15 +87,18 @@ function getTag() { } function entityIsGrabbedByOther(entityID) { + // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. var actionIDs = Entities.getActionIDs(entityID); for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { var actionID = actionIDs[actionIndex]; var actionArguments = Entities.getActionArguments(entityID, actionID); var tag = actionArguments["tag"]; if (tag == getTag()) { + // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay. continue; } if (tag.slice(0, 5) == "grab-") { + // we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it. return true; } } @@ -164,6 +167,12 @@ function MyController(hand, triggerAction) { } }; + this.setState = function(newState) { + // print("STATE: " + this.state + " --> " + newState); + this.state = newState; + } + + this.lineOn = function(closePoint, farPoint, color) { // draw a line if (this.pointer === null) { @@ -217,14 +226,14 @@ function MyController(hand, triggerAction) { this.off = function() { if (this.triggerSmoothedSqueezed()) { - this.state = STATE_SEARCHING; + this.setState(STATE_SEARCHING); return; } } this.search = function() { if (this.triggerSmoothedReleased()) { - this.state = STATE_RELEASE; + this.setState(STATE_RELEASE); return; } @@ -235,6 +244,8 @@ function MyController(hand, triggerAction) { direction: Quat.getUp(this.getHandRotation()) }; + this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + var defaultGrabbableData = { grabbable: true }; @@ -250,20 +261,20 @@ function MyController(hand, triggerAction) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, intersection.entityID, defaultGrabbableData); if (grabbableData.grabbable === false) { + this.grabbedEntity = null; return; } if (intersectionDistance < NEAR_PICK_MAX_DISTANCE) { // the hand is very close to the intersected object. go into close-grabbing mode. - this.state = STATE_NEAR_GRABBING; - + this.setState(STATE_NEAR_GRABBING); } else { + // don't allow two people to distance grab the same object if (entityIsGrabbedByOther(intersection.entityID)) { - // don't allow two people to distance grab the same object - return; + this.grabbedEntity = null; + } else { + // the hand is far from the intersected object. go into distance-holding mode + this.setState(STATE_DISTANCE_HOLDING); } - // the hand is far from the intersected object. go into distance-holding mode - this.state = STATE_DISTANCE_HOLDING; - this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } } else { // forward ray test failed, try sphere test. @@ -287,15 +298,14 @@ function MyController(hand, triggerAction) { } } if (this.grabbedEntity === null) { - this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + // this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } else if (props.locked === 0 && props.collisionsWillMove === 1) { - this.state = STATE_NEAR_GRABBING; + this.setState(STATE_NEAR_GRABBING); } else if (props.collisionsWillMove === 0) { // We have grabbed a non-physical object, so we want to trigger a non-colliding event as opposed to a grab event - this.state = STATE_NEAR_GRABBING_NON_COLLIDING; + this.setState(STATE_NEAR_GRABBING_NON_COLLIDING); } } - }; this.distanceHolding = function() { @@ -325,7 +335,7 @@ function MyController(hand, triggerAction) { } if (this.actionID !== null) { - this.state = STATE_CONTINUE_DISTANCE_HOLDING; + this.setState(STATE_CONTINUE_DISTANCE_HOLDING); this.activateEntity(this.grabbedEntity); if (this.hand === RIGHT_HAND) { Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); @@ -342,7 +352,7 @@ function MyController(hand, triggerAction) { this.continueDistanceHolding = function() { if (this.triggerSmoothedReleased()) { - this.state = STATE_RELEASE; + this.setState(STATE_RELEASE); return; } @@ -400,7 +410,7 @@ function MyController(hand, triggerAction) { var now = Date.now(); var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds this.computeReleaseVelocity(deltaPosition, deltaTime, false); - + this.currentObjectPosition = newObjectPosition; this.currentObjectTime = now; @@ -423,7 +433,7 @@ function MyController(hand, triggerAction) { this.nearGrabbing = function() { if (this.triggerSmoothedReleased()) { - this.state = STATE_RELEASE; + this.setState(STATE_RELEASE); return; } @@ -454,7 +464,7 @@ function MyController(hand, triggerAction) { if (this.actionID === NULL_ACTION_ID) { this.actionID = null; } else { - this.state = STATE_CONTINUE_NEAR_GRABBING; + this.setState(STATE_CONTINUE_NEAR_GRABBING); if (this.hand === RIGHT_HAND) { Entities.callEntityMethod(this.grabbedEntity, "setRightHand"); } else { @@ -471,7 +481,7 @@ function MyController(hand, triggerAction) { this.continueNearGrabbing = function() { if (this.triggerSmoothedReleased()) { - this.state = STATE_RELEASE; + this.setState(STATE_RELEASE); return; } @@ -498,7 +508,7 @@ function MyController(hand, triggerAction) { this.nearGrabbingNonColliding = function() { if (this.triggerSmoothedReleased()) { - this.state = STATE_RELEASE; + this.setState(STATE_RELEASE); return; } if (this.hand === RIGHT_HAND) { @@ -507,12 +517,12 @@ function MyController(hand, triggerAction) { Entities.callEntityMethod(this.grabbedEntity, "setLeftHand"); } Entities.callEntityMethod(this.grabbedEntity, "startNearGrabNonColliding"); - this.state = STATE_CONTINUE_NEAR_GRABBING_NON_COLLIDING; + this.setState(STATE_CONTINUE_NEAR_GRABBING_NON_COLLIDING); }; this.continueNearGrabbingNonColliding = function() { if (this.triggerSmoothedReleased()) { - this.state = STATE_RELEASE; + this.setState(STATE_RELEASE); return; } Entities.callEntityMethod(this.grabbedEntity, "continueNearGrabbingNonColliding"); @@ -623,7 +633,7 @@ function MyController(hand, triggerAction) { this.grabbedVelocity = ZERO_VEC; this.grabbedEntity = null; this.actionID = null; - this.state = STATE_OFF; + this.setState(STATE_OFF); }; this.cleanup = function() { diff --git a/examples/grab.js b/examples/grab.js index 03a227931d..1e0adf9f1b 100644 --- a/examples/grab.js +++ b/examples/grab.js @@ -45,26 +45,25 @@ var IDENTITY_QUAT = { z: 0, w: 0 }; -var ACTION_LIFETIME = 120; // 2 minutes +var ACTION_LIFETIME = 10; // seconds function getTag() { return "grab-" + MyAvatar.sessionUUID; } function entityIsGrabbedByOther(entityID) { + // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. var actionIDs = Entities.getActionIDs(entityID); - var actionIndex; - var actionID; - var actionArguments; - var tag; - for (actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { - actionID = actionIDs[actionIndex]; - actionArguments = Entities.getActionArguments(entityID, actionID); - tag = actionArguments.tag; - if (tag === getTag()) { + for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { + var actionID = actionIDs[actionIndex]; + var actionArguments = Entities.getActionArguments(entityID, actionID); + var tag = actionArguments["tag"]; + if (tag == getTag()) { + // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay. continue; } - if (tag.slice(0, 5) === "grab-") { + if (tag.slice(0, 5) == "grab-") { + // we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it. return true; } } @@ -530,4 +529,4 @@ Controller.mousePressEvent.connect(pressEvent); Controller.mouseMoveEvent.connect(moveEvent); Controller.mouseReleaseEvent.connect(releaseEvent); Controller.keyPressEvent.connect(keyPressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); \ No newline at end of file +Controller.keyReleaseEvent.connect(keyReleaseEvent); diff --git a/interface/src/InterfaceActionFactory.cpp b/interface/src/InterfaceActionFactory.cpp index 2879c19eaa..cf5dad8fa5 100644 --- a/interface/src/InterfaceActionFactory.cpp +++ b/interface/src/InterfaceActionFactory.cpp @@ -65,9 +65,9 @@ EntityActionPointer InterfaceActionFactory::factoryBA(EntityItemPointer ownerEnt if (action) { action->deserialize(data); - } - if (action->lifetimeIsOver()) { - return nullptr; + if (action->lifetimeIsOver()) { + return nullptr; + } } return action; diff --git a/libraries/entities/src/EntityActionInterface.h b/libraries/entities/src/EntityActionInterface.h index e61019c98c..4432b26a6a 100644 --- a/libraries/entities/src/EntityActionInterface.h +++ b/libraries/entities/src/EntityActionInterface.h @@ -48,6 +48,8 @@ public: virtual bool lifetimeIsOver() { return false; } + bool locallyAddedButNotYetReceived = false; + protected: virtual glm::vec3 getPosition() = 0; virtual void setPosition(glm::vec3 position) = 0; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index ce719ee976..c4f5ad0061 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -34,6 +34,7 @@ bool EntityItem::_sendPhysicsUpdates = true; int EntityItem::_maxActionsDataSize = 800; +quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; EntityItem::EntityItem(const EntityItemID& entityItemID) : _type(EntityTypes::Unknown), @@ -1505,6 +1506,8 @@ bool EntityItem::addAction(EntitySimulation* simulation, EntityActionPointer act result = addActionInternal(simulation, action); if (!result) { removeActionInternal(action->getID()); + } else { + action->locallyAddedButNotYetReceived = true; } }); @@ -1525,7 +1528,8 @@ bool EntityItem::addActionInternal(EntitySimulation* simulation, EntityActionPoi simulation->addAction(action); bool success; - QByteArray newDataCache = serializeActions(success); + QByteArray newDataCache; + serializeActions(success, newDataCache); if (success) { _allActionsDataCache = newDataCache; _dirtyFlags |= EntityItem::DIRTY_PHYSICS_ACTIVATION; @@ -1546,7 +1550,7 @@ bool EntityItem::updateAction(EntitySimulation* simulation, const QUuid& actionI success = action->updateArguments(arguments); if (success) { - _allActionsDataCache = serializeActions(success); + serializeActions(success, _allActionsDataCache); _dirtyFlags |= EntityItem::DIRTY_PHYSICS_ACTIVATION; } else { qDebug() << "EntityItem::updateAction failed"; @@ -1566,6 +1570,7 @@ bool EntityItem::removeAction(EntitySimulation* simulation, const QUuid& actionI bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulation* simulation) { assertWriteLocked(); + _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { if (!simulation) { EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; @@ -1581,7 +1586,7 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulation* s } bool success = true; - _allActionsDataCache = serializeActions(success); + serializeActions(success, _allActionsDataCache); _dirtyFlags |= EntityItem::DIRTY_PHYSICS_ACTIVATION; return success; } @@ -1618,6 +1623,8 @@ void EntityItem::deserializeActions() { void EntityItem::deserializeActionsInternal() { assertWriteLocked(); + quint64 now = usecTimestampNow(); + if (!_element) { return; } @@ -1643,6 +1650,10 @@ void EntityItem::deserializeActionsInternal() { QUuid actionID; serializedActionStream >> actionType; serializedActionStream >> actionID; + if (_previouslyDeletedActions.contains(actionID)) { + continue; + } + updated << actionID; if (_objectActions.contains(actionID)) { @@ -1650,14 +1661,14 @@ void EntityItem::deserializeActionsInternal() { // TODO: make sure types match? there isn't currently a way to // change the type of an existing action. action->deserialize(serializedAction); + action->locallyAddedButNotYetReceived = false; } else { auto actionFactory = DependencyManager::get(); - - // EntityItemPointer entity = entityTree->findEntityByEntityItemID(_id, false); EntityItemPointer entity = shared_from_this(); EntityActionPointer action = actionFactory->factoryBA(entity, serializedAction); if (action) { entity->addActionInternal(simulation, action); + action->locallyAddedButNotYetReceived = false; } } } @@ -1667,11 +1678,27 @@ void EntityItem::deserializeActionsInternal() { while (i != _objectActions.end()) { QUuid id = i.key(); if (!updated.contains(id)) { - _actionsToRemove << id; + EntityActionPointer action = i.value(); + // if we've just added this action, don't remove it due to lack of mention in an incoming packet. + if (! action->locallyAddedButNotYetReceived) { + _actionsToRemove << id; + _previouslyDeletedActions.insert(id, now); + } } i++; } + // trim down _previouslyDeletedActions + QMutableHashIterator _previouslyDeletedIter(_previouslyDeletedActions); + while (_previouslyDeletedIter.hasNext()) { + _previouslyDeletedIter.next(); + if (now - _previouslyDeletedIter.value() > _rememberDeletedActionTime) { + _previouslyDeletedActions.remove(_previouslyDeletedIter.key()); + } + } + + _actionDataDirty = true; + return; } @@ -1697,13 +1724,13 @@ void EntityItem::setActionDataInternal(QByteArray actionData) { deserializeActionsInternal(); } -QByteArray EntityItem::serializeActions(bool& success) const { +void EntityItem::serializeActions(bool& success, QByteArray& result) const { assertLocked(); - QByteArray result; if (_objectActions.size() == 0) { success = true; - return QByteArray(); + result.clear(); + return; } QVector serializedActions; @@ -1721,21 +1748,20 @@ QByteArray EntityItem::serializeActions(bool& success) const { if (result.size() >= _maxActionsDataSize) { success = false; - return result; + return; } success = true; - return result; + return; } const QByteArray EntityItem::getActionDataInternal() const { if (_actionDataDirty) { bool success; - QByteArray newDataCache = serializeActions(success); + serializeActions(success, _allActionsDataCache); if (success) { - _allActionsDataCache = newDataCache; + _actionDataDirty = false; } - _actionDataDirty = false; } return _allActionsDataCache; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index f070e2c1e4..d11ffadc75 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -508,17 +508,21 @@ protected: bool addActionInternal(EntitySimulation* simulation, EntityActionPointer action); bool removeActionInternal(const QUuid& actionID, EntitySimulation* simulation = nullptr); void deserializeActionsInternal(); - QByteArray serializeActions(bool& success) const; + void serializeActions(bool& success, QByteArray& result) const; QHash _objectActions; static int _maxActionsDataSize; mutable QByteArray _allActionsDataCache; + // when an entity-server starts up, EntityItem::setActionData is called before the entity-tree is // ready. This means we can't find our EntityItemPointer or add the action to the simulation. These // are used to keep track of and work around this situation. void checkWaitingToRemove(EntitySimulation* simulation = nullptr); mutable QSet _actionsToRemove; mutable bool _actionDataDirty = false; + // _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag + static quint64 _rememberDeletedActionTime; + mutable QHash _previouslyDeletedActions; }; #endif // hifi_EntityItem_h