Merge pull request #7031 from AndrewMeadows/simulation-ownership

server can clear simulation ownership
This commit is contained in:
Brad Hefta-Gaub 2016-02-06 19:19:39 -08:00
commit 3f0ebf7732
12 changed files with 186 additions and 94 deletions

View file

@ -652,6 +652,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
}
if (_simulationOwner.set(newSimOwner)) {
_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
somethingChanged = true;
}
}
{ // When we own the simulation we don't accept updates to the entity's transform/velocities
@ -987,7 +988,7 @@ EntityTreePointer EntityItem::getTree() const {
return tree;
}
bool EntityItem::wantTerseEditLogging() {
bool EntityItem::wantTerseEditLogging() const {
EntityTreePointer tree = getTree();
return tree ? tree->wantTerseEditLogging() : false;
}

View file

@ -104,7 +104,7 @@ public:
quint64 getLastBroadcast() const { return _lastBroadcast; }
void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; }
void markAsChangedOnServer() { _changedOnServer = usecTimestampNow(); }
void markAsChangedOnServer() { _changedOnServer = usecTimestampNow(); }
quint64 getLastChangedOnServer() const { return _changedOnServer; }
// TODO: eventually only include properties changed since the params.lastViewFrustumSent time
@ -351,14 +351,14 @@ public:
void setPhysicsInfo(void* data) { _physicsInfo = data; }
EntityTreeElementPointer getElement() const { return _element; }
EntityTreePointer getTree() const;
bool wantTerseEditLogging();
bool wantTerseEditLogging() const;
glm::mat4 getEntityToWorldMatrix() const;
glm::mat4 getWorldToEntityMatrix() const;
glm::vec3 worldToEntity(const glm::vec3& point) const;
glm::vec3 entityToWorld(const glm::vec3& point) const;
quint64 getLastEditedFromRemote() { return _lastEditedFromRemote; }
quint64 getLastEditedFromRemote() const { return _lastEditedFromRemote; }
void getAllTerseUpdateProperties(EntityItemProperties& properties) const;

View file

@ -910,7 +910,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem
success = packetData->startSubTree(octcode);
delete[] octcode;
// assuming we have rome to fit our octalCode, proceed...
// assuming we have room to fit our octalCode, proceed...
if (success) {
// Now add our edit content details...

View file

@ -11,38 +11,58 @@
//#include <PerfStat.h>
#include "EntityItem.h"
#include "SimpleEntitySimulation.h"
#include <DirtyOctreeElementOperator.h>
#include "EntityItem.h"
#include "EntitiesLogging.h"
const quint64 AUTO_REMOVE_SIMULATION_OWNER_USEC = 2 * USECS_PER_SECOND;
const quint64 MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD = 2 * USECS_PER_SECOND;
void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) {
// If an Entity has a simulation owner and we don't get an update for some amount of time,
// clear the owner. This guards against an interface failing to release the Entity when it
// has finished simulating it.
auto nodeList = DependencyManager::get<LimitedNodeList>();
if (_entitiesWithSimulator.size() == 0) {
return;
}
if (now < _nextSimulationExpiry) {
// nothing has expired yet
return;
}
// If an Entity has a simulation owner but there has been no update for a while: clear the owner.
// If an Entity goes ownerless for too long: zero velocity and remove from _entitiesWithSimulator.
_nextSimulationExpiry = now + MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD;
QMutexLocker lock(&_mutex);
SetOfEntities::iterator itemItr = _entitiesWithSimulator.begin();
while (itemItr != _entitiesWithSimulator.end()) {
EntityItemPointer entity = *itemItr;
if (entity->getSimulatorID().isNull()) {
itemItr = _entitiesWithSimulator.erase(itemItr);
} else if (now - entity->getLastChangedOnServer() >= AUTO_REMOVE_SIMULATION_OWNER_USEC) {
SharedNodePointer ownerNode = nodeList->nodeWithUUID(entity->getSimulatorID());
if (ownerNode.isNull() || !ownerNode->isAlive()) {
qCDebug(entities) << "auto-removing simulation owner" << entity->getSimulatorID();
entity->clearSimulationOwnership();
itemItr = _entitiesWithSimulator.erase(itemItr);
quint64 expiry = entity->getLastChangedOnServer() + MIN_SIMULATION_OWNERSHIP_UPDATE_PERIOD;
if (expiry < now) {
if (entity->getSimulatorID().isNull()) {
// no simulators are volunteering
// zero the velocity on this entity so that it doesn't drift far away
entity->setVelocity(glm::vec3(0.0f));
entity->setVelocity(Vectors::ZERO);
entity->setAngularVelocity(Vectors::ZERO);
entity->setAcceleration(Vectors::ZERO);
// remove from list
itemItr = _entitiesWithSimulator.erase(itemItr);
continue;
} else {
++itemItr;
// the simulator has stopped updating this object
// clear ownership and restart timer, giving nearby simulators time to volunteer
qCDebug(entities) << "auto-removing simulation owner " << entity->getSimulatorID();
entity->clearSimulationOwnership();
}
} else {
++itemItr;
entity->markAsChangedOnServer();
// dirty all the tree elements that contain the entity
DirtyOctreeElementOperator op(entity->getElement());
getEntityTree()->recurseTreeWithOperator(&op);
} else if (expiry < _nextSimulationExpiry) {
_nextSimulationExpiry = expiry;
}
++itemItr;
}
}

View file

@ -29,6 +29,7 @@ protected:
virtual void clearEntitiesInternal() override;
SetOfEntities _entitiesWithSimulator;
quint64 _nextSimulationExpiry { 0 };
};
#endif // hifi_SimpleEntitySimulation_h

View file

@ -16,11 +16,11 @@
#include <NumericalConstants.h>
// static
// static
const int SimulationOwner::NUM_BYTES_ENCODED = NUM_BYTES_RFC4122_UUID + 1;
SimulationOwner::SimulationOwner(const SimulationOwner& other)
SimulationOwner::SimulationOwner(const SimulationOwner& other)
: _id(other._id), _priority(other._priority), _expiry(other._expiry) {
}
@ -48,11 +48,6 @@ void SimulationOwner::clear() {
void SimulationOwner::setPriority(quint8 priority) {
_priority = priority;
if (_priority == 0) {
// when priority is zero we clear everything
_expiry = 0;
_id = QUuid();
}
}
void SimulationOwner::promotePriority(quint8 priority) {

View file

@ -18,10 +18,10 @@
#include <SharedUtil.h>
#include <UUID.h>
const quint8 NO_PRORITY = 0x00;
const quint8 ZERO_SIMULATION_PRIORITY = 0x00;
// Simulation observers will bid to simulate unowned active objects at the lowest possible priority
// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it
// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it
// to RECRUIT priority so that other volunteers don't accidentally take over.
const quint8 VOLUNTEER_SIMULATION_PRIORITY = 0x01;
const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1;

View file

@ -0,0 +1,30 @@
//
// DirtyOctreeElementOperator.cpp
// libraries/entities/src
//
// Created by Andrew Meawdows 2016.02.04
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "DirtyOctreeElementOperator.h"
DirtyOctreeElementOperator::DirtyOctreeElementOperator(OctreeElementPointer element)
: _element(element) {
assert(_element.get());
_point = _element->getAACube().calcCenter();
}
bool DirtyOctreeElementOperator::preRecursion(OctreeElementPointer element) {
if (element == _element) {
return false;
}
return element->getAACube().contains(_point);
}
bool DirtyOctreeElementOperator::postRecursion(OctreeElementPointer element) {
element->markWithChangedTime();
return true;
}

View file

@ -0,0 +1,30 @@
//
// DirtyOctreeElementOperator.h
// libraries/entities/src
//
// Created by Andrew Meawdows 2016.02.04
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_DirtyOctreeElementOperator_h
#define hifi_DirtyOctreeElementOperator_h
#include "Octree.h"
class DirtyOctreeElementOperator : public RecurseOctreeOperator {
public:
DirtyOctreeElementOperator(OctreeElementPointer element);
~DirtyOctreeElementOperator() {}
virtual bool preRecursion(OctreeElementPointer element);
virtual bool postRecursion(OctreeElementPointer element);
private:
glm::vec3 _point;
OctreeElementPointer _element;
};
#endif // hifi_DirtyOctreeElementOperator_h

View file

@ -26,10 +26,7 @@
#include "EntityTree.h"
#endif
static const float ACCELERATION_EQUIVALENT_EPSILON_RATIO = 0.1f;
static const quint8 STEPS_TO_DECIDE_BALLISTIC = 4;
const uint32_t LOOPS_FOR_SIMULATION_ORPHAN = 50;
const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50;
const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5;
#ifdef WANT_DEBUG_ENTITY_TREE_LOCKS
@ -52,8 +49,6 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer
ObjectMotionState(shape),
_entityPtr(entity),
_entity(entity.get()),
_sentInactive(true),
_lastStep(0),
_serverPosition(0.0f),
_serverRotation(),
_serverVelocity(0.0f),
@ -61,13 +56,16 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer
_serverGravity(0.0f),
_serverAcceleration(0.0f),
_serverActionData(QByteArray()),
_lastMeasureStep(0),
_lastVelocity(glm::vec3(0.0f)),
_measuredAcceleration(glm::vec3(0.0f)),
_measuredDeltaTime(0.0f),
_accelerationNearlyGravityCount(0),
_nextOwnershipBid(0),
_loopsWithoutOwner(0)
_measuredDeltaTime(0.0f),
_lastMeasureStep(0),
_lastStep(0),
_loopsWithoutOwner(0),
_accelerationNearlyGravityCount(0),
_numInactiveUpdates(1),
_outgoingPriority(ZERO_SIMULATION_PRIORITY)
{
_type = MOTIONSTATE_TYPE_ENTITY;
assert(_entity);
@ -102,27 +100,35 @@ bool EntityMotionState::handleEasyChanges(uint32_t& flags) {
ObjectMotionState::handleEasyChanges(flags);
if (flags & Simulation::DIRTY_SIMULATOR_ID) {
_loopsWithoutOwner = 0;
if (_entity->getSimulatorID().isNull()) {
// simulation ownership is being removed
// remove the ACTIVATION flag because this object is coming to rest
// according to a remote simulation and we don't want to wake it up again
flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION;
// hint to Bullet that the object is deactivating
_body->setActivationState(WANTS_DEACTIVATION);
_outgoingPriority = NO_PRORITY;
} else {
// simulation ownership has been removed by an external simulator
if (glm::length2(_entity->getVelocity()) == 0.0f) {
// this object is coming to rest --> clear the ACTIVATION flag and outgoing priority
flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION;
_body->setActivationState(WANTS_DEACTIVATION);
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
_loopsWithoutOwner = 0;
} else {
// unowned object is still moving --> we should volunteer to own it
// TODO? put a delay in here proportional to distance from object?
setOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY);
_loopsWithoutOwner = LOOPS_FOR_SIMULATION_ORPHAN;
_nextOwnershipBid = 0;
}
} else {
// this entity's simulation is owned by someone, so we push its ownership expiry into the future
_nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS;
if (Physics::getSessionUUID() == _entity->getSimulatorID() || _entity->getSimulationPriority() >= _outgoingPriority) {
// we own the simulation or our priority looses to (or ties with) remote
_outgoingPriority = NO_PRORITY;
// either we already own the simulation or our old outgoing priority momentarily looses to current owner
// so we clear it
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
}
}
}
if (flags & Simulation::DIRTY_SIMULATOR_OWNERSHIP) {
// (DIRTY_SIMULATOR_OWNERSHIP really means "we should bid for ownership with SCRIPT priority")
// we're manipulating this object directly via script, so we artificially
// manipulate the logic to trigger an immediate bid for ownership
// The DIRTY_SIMULATOR_OWNERSHIP bit really means "we should bid for ownership at SCRIPT priority".
// Since that bit is set there must be a local script that is updating the physics properties of the objects
// therefore we upgrade _outgoingPriority to trigger a bid for ownership.
setOutgoingPriority(SCRIPT_EDIT_SIMULATION_PRIORITY);
}
if ((flags & Simulation::DIRTY_PHYSICS_ACTIVATION) && !_body->isActive()) {
@ -203,7 +209,6 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) {
_loopsWithoutOwner++;
if (_loopsWithoutOwner > LOOPS_FOR_SIMULATION_ORPHAN && usecTimestampNow() > _nextOwnershipBid) {
//qDebug() << "Warning -- claiming something I saw moving." << getName();
setOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY);
}
}
@ -235,14 +240,14 @@ btCollisionShape* EntityMotionState::computeNewShape() {
}
bool EntityMotionState::isCandidateForOwnership(const QUuid& sessionID) const {
if (!_body || !_entity) {
return false;
}
assert(_body);
assert(_entity);
assert(entityTreeIsLocked());
return _outgoingPriority != NO_PRORITY || sessionID == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit();
return _outgoingPriority != ZERO_SIMULATION_PRIORITY || sessionID == _entity->getSimulatorID() || _entity->actionDataNeedsTransmit();
}
bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
// NOTE: we only get here if we think we own the simulation
assert(_body);
// if we've never checked before, our _lastStep will be 0, and we need to initialize our state
if (_lastStep == 0) {
@ -253,7 +258,7 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
_serverAngularVelocity = bulletToGLM(_body->getAngularVelocity());
_lastStep = simulationStep;
_serverActionData = _entity->getActionData();
_sentInactive = true;
_numInactiveUpdates = 1;
return false;
}
@ -266,16 +271,21 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
int numSteps = simulationStep - _lastStep;
float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP;
const float INACTIVE_UPDATE_PERIOD = 0.5f;
if (_sentInactive) {
if (_numInactiveUpdates > 0) {
const uint8_t MAX_NUM_INACTIVE_UPDATES = 3;
if (_numInactiveUpdates > MAX_NUM_INACTIVE_UPDATES) {
// clear local ownership (stop sending updates) and let the server clear itself
_entity->clearSimulationOwnership();
return false;
}
// we resend the inactive update every INACTIVE_UPDATE_PERIOD
// until it is removed from the outgoing updates
// (which happens when we don't own the simulation and it isn't touching our simulation)
const float INACTIVE_UPDATE_PERIOD = 0.5f;
return (dt > INACTIVE_UPDATE_PERIOD);
}
bool isActive = _body->isActive();
if (!isActive) {
if (!_body->isActive()) {
// object has gone inactive but our last send was moving --> send non-moving update immediately
return true;
}
@ -374,11 +384,12 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep, const QUuid& s
}
if (_entity->getSimulatorID() != sessionID) {
// we don't own the simulation, but maybe we should...
if (_outgoingPriority != NO_PRORITY) {
// we don't own the simulation
if (_outgoingPriority != ZERO_SIMULATION_PRIORITY) {
// but we would like to own it
if (_outgoingPriority < _entity->getSimulationPriority()) {
// our priority loses to remote, so we don't bother to bid
_outgoingPriority = NO_PRORITY;
// but our priority loses to remote, so we don't bother trying
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
return false;
}
return usecTimestampNow() > _nextOwnershipBid;
@ -400,10 +411,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q
_entity->setVelocity(zero);
_entity->setAngularVelocity(zero);
_entity->setAcceleration(zero);
_sentInactive = true;
_numInactiveUpdates++;
} else {
const uint8_t STEPS_TO_DECIDE_BALLISTIC = 4;
float gravityLength = glm::length(_entity->getGravity());
float accVsGravity = glm::abs(glm::length(_measuredAcceleration) - gravityLength);
const float ACCELERATION_EQUIVALENT_EPSILON_RATIO = 0.1f;
if (accVsGravity < ACCELERATION_EQUIVALENT_EPSILON_RATIO * gravityLength) {
// acceleration measured during the most recent simulation step was close to gravity.
if (getAccelerationNearlyGravityCount() < STEPS_TO_DECIDE_BALLISTIC) {
@ -440,7 +453,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q
_entity->setVelocity(zero);
_entity->setAngularVelocity(zero);
}
_sentInactive = false;
_numInactiveUpdates = 0;
}
// remember properties for local server prediction
@ -488,12 +501,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, const Q
// we own the simulation but the entity has stopped, so we tell the server that we're clearing simulatorID
// but we remember that we do still own it... and rely on the server to tell us that we don't
properties.clearSimulationOwner();
_outgoingPriority = NO_PRORITY;
_outgoingPriority = ZERO_SIMULATION_PRIORITY;
}
// else the ownership is not changing so we don't bother to pack it
} else {
// we don't own the simulation for this entity yet, but we're sending a bid for it
properties.setSimulationOwner(sessionID, glm::max<quint8>(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY));
properties.setSimulationOwner(sessionID, glm::max<uint8_t>(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY));
_nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS;
}
@ -558,7 +571,7 @@ void EntityMotionState::clearIncomingDirtyFlags() {
}
// virtual
quint8 EntityMotionState::getSimulationPriority() const {
uint8_t EntityMotionState::getSimulationPriority() const {
return _entity->getSimulationPriority();
}
@ -568,7 +581,7 @@ QUuid EntityMotionState::getSimulatorID() const {
return _entity->getSimulatorID();
}
void EntityMotionState::bump(quint8 priority) {
void EntityMotionState::bump(uint8_t priority) {
setOutgoingPriority(glm::max(VOLUNTEER_SIMULATION_PRIORITY, --priority));
}
@ -601,7 +614,7 @@ void EntityMotionState::measureBodyAcceleration() {
if (numSubsteps > PHYSICS_ENGINE_MAX_NUM_SUBSTEPS) {
_loopsWithoutOwner = 0;
_lastStep = ObjectMotionState::getWorldSimulationStep();
_sentInactive = false;
_numInactiveUpdates = 0;
}
}
}
@ -631,6 +644,6 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma
_entity->computeCollisionGroupAndFinalMask(group, mask);
}
void EntityMotionState::setOutgoingPriority(quint8 priority) {
_outgoingPriority = glm::max<quint8>(_outgoingPriority, priority);
void EntityMotionState::setOutgoingPriority(uint8_t priority) {
_outgoingPriority = glm::max<uint8_t>(_outgoingPriority, priority);
}

View file

@ -53,7 +53,7 @@ public:
void incrementAccelerationNearlyGravityCount() { _accelerationNearlyGravityCount++; }
void resetAccelerationNearlyGravityCount() { _accelerationNearlyGravityCount = 0; }
quint8 getAccelerationNearlyGravityCount() { return _accelerationNearlyGravityCount; }
uint8_t getAccelerationNearlyGravityCount() { return _accelerationNearlyGravityCount; }
virtual float getObjectRestitution() const override { return _entity->getRestitution(); }
virtual float getObjectFriction() const override { return _entity->getFriction(); }
@ -69,9 +69,9 @@ public:
virtual const QUuid getObjectID() const override { return _entity->getID(); }
virtual quint8 getSimulationPriority() const override;
virtual uint8_t getSimulationPriority() const override;
virtual QUuid getSimulatorID() const override;
virtual void bump(quint8 priority) override;
virtual void bump(uint8_t priority) override;
EntityItemPointer getEntity() const { return _entityPtr.lock(); }
@ -83,7 +83,7 @@ public:
virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override;
// eternal logic can suggest a simuator priority bid for the next outgoing update
void setOutgoingPriority(quint8 priority);
void setOutgoingPriority(uint8_t priority);
friend class PhysicalEntitySimulation;
@ -106,10 +106,6 @@ protected:
// Meanwhile we also keep a raw EntityItem* for internal stuff where the pointer is guaranteed valid.
EntityItem* _entity;
bool _sentInactive; // true if body was inactive when we sent last update
// these are for the prediction of the remote server's simple extrapolation
uint32_t _lastStep; // last step of server extrapolation
glm::vec3 _serverPosition; // in simulation-frame (not world-frame)
glm::quat _serverRotation;
glm::vec3 _serverVelocity;
@ -118,15 +114,18 @@ protected:
glm::vec3 _serverAcceleration;
QByteArray _serverActionData;
uint32_t _lastMeasureStep;
glm::vec3 _lastVelocity;
glm::vec3 _measuredAcceleration;
float _measuredDeltaTime;
quint64 _nextOwnershipBid { 0 };
quint8 _accelerationNearlyGravityCount;
quint64 _nextOwnershipBid = NO_PRORITY;
uint32_t _loopsWithoutOwner;
quint8 _outgoingPriority = NO_PRORITY;
float _measuredDeltaTime;
uint32_t _lastMeasureStep;
uint32_t _lastStep; // last step of server extrapolation
uint8_t _loopsWithoutOwner;
uint8_t _accelerationNearlyGravityCount;
uint8_t _numInactiveUpdates { 1 };
uint8_t _outgoingPriority { ZERO_SIMULATION_PRIORITY };
};
#endif // hifi_EntityMotionState_h

View file

@ -249,6 +249,7 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result)
void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates, const QUuid& sessionID) {
QMutexLocker lock(&_mutex);
// walk the motionStates looking for those that correspond to entities
for (auto stateItr : motionStates) {
ObjectMotionState* state = &(*stateItr);
@ -273,13 +274,15 @@ void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates&
return;
}
// send outgoing packets
// look for entities to prune or update
QSet<EntityMotionState*>::iterator stateItr = _outgoingChanges.begin();
while (stateItr != _outgoingChanges.end()) {
EntityMotionState* state = *stateItr;
if (!state->isCandidateForOwnership(sessionID)) {
// prune
stateItr = _outgoingChanges.erase(stateItr);
} else if (state->shouldSendUpdate(numSubsteps, sessionID)) {
// update
state->sendUpdate(_entityPacketSender, sessionID, numSubsteps);
++stateItr;
} else {