Improve proxy animation motion handling
Add explicit motion-driven animation sync and strengthen moving logic to reduce desyncs. Initialize puppetGraphSpeed and reset applied animation state for non-moving puppets on restore. Introduce SyncProxyAnimationFromMotion to drive proxy animation from measured per-frame speed (with a 8.0F threshold) and to clear puppet graph speed when the remote isn't locomoting. Replace previous inline motion/graph merge logic in ApplySmoothFrameMovement with this helper. In F4TProxyAnimationSync, make isMoving authoritative (require both isMoving flag and movementSpeed >= idle threshold) and clear movingHoldUntil when the remote is not moving to avoid stale hold states.
This commit is contained in:
@@ -4814,6 +4814,10 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_slot.firstSmoothedMovementLogged = false;
|
||||
a_slot.remoteStateMovedLogged = false;
|
||||
a_slot.lastRemotePlayerSnapshot = a_remotePlayer;
|
||||
a_slot.puppetGraphSpeed = -1.0F;
|
||||
if (!a_remotePlayer.isMoving) {
|
||||
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||
}
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at visible X={:.2f}, Y={:.2f}, Z={:.2f} from remote X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
@@ -5316,6 +5320,42 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr float kProxyMotionSpeedThreshold = 8.0F;
|
||||
|
||||
void SyncProxyAnimationFromMotion(
|
||||
ProxyActorSlot& a_slot,
|
||||
RE::Actor& a_proxy,
|
||||
float a_measuredSpeed)
|
||||
{
|
||||
if (!a_slot.lastRemotePlayerSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto effectiveRemote = *a_slot.lastRemotePlayerSnapshot;
|
||||
const bool remoteLocomoting =
|
||||
effectiveRemote.isMoving && effectiveRemote.movementSpeed >= 1.0F;
|
||||
|
||||
if (!remoteLocomoting) {
|
||||
a_slot.puppetGraphSpeed = -1.0F;
|
||||
effectiveRemote.isMoving = false;
|
||||
effectiveRemote.movementSpeed = 0.0F;
|
||||
effectiveRemote.animationGraphSpeed = -1.0F;
|
||||
F4T::ProxyPuppet::SetProxyLocomotionState(a_proxy.GetFormID(), -1.0F, a_slot.puppetDirection);
|
||||
} else if (a_measuredSpeed >= kProxyMotionSpeedThreshold) {
|
||||
effectiveRemote.movementSpeed = std::max(effectiveRemote.movementSpeed, a_measuredSpeed);
|
||||
a_slot.puppetGraphSpeed = F4T::ProxyAnimationSync::ResolveLocomotionGraphSpeed(
|
||||
effectiveRemote.movementSpeed,
|
||||
effectiveRemote.animationGraphSpeed,
|
||||
true,
|
||||
effectiveRemote.isSprinting);
|
||||
}
|
||||
|
||||
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
||||
a_slot.appliedAnimationState,
|
||||
a_proxy,
|
||||
effectiveRemote);
|
||||
}
|
||||
|
||||
// Apply smooth per-frame movement for all active proxies
|
||||
// This is called every frame to interpolate between network updates
|
||||
void ApplySmoothFrameMovement()
|
||||
@@ -5359,12 +5399,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
frameDeltaSeconds,
|
||||
-1.0F,
|
||||
slot.puppetDirection);
|
||||
if (slot.lastRemotePlayerSnapshot) {
|
||||
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
||||
slot.appliedAnimationState,
|
||||
*proxy,
|
||||
*slot.lastRemotePlayerSnapshot);
|
||||
}
|
||||
SyncProxyAnimationFromMotion(slot, *proxy, 0.0F);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5395,32 +5430,11 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
slot.puppetGraphSpeed,
|
||||
slot.puppetDirection);
|
||||
|
||||
// Drive animation from measured proxy motion every frame. Network isMoving is
|
||||
// often false while the proxy is visibly jogging (log: recvMoving=false during
|
||||
// active translation). MTBehavior also needs explicit Jog/Run/Walk events.
|
||||
if (slot.lastRemotePlayerSnapshot) {
|
||||
auto effectiveRemote = *slot.lastRemotePlayerSnapshot;
|
||||
const float horizontalDelta =
|
||||
std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y));
|
||||
const float measuredSpeed =
|
||||
frameDeltaSeconds > 0.0F ? horizontalDelta / frameDeltaSeconds : 0.0F;
|
||||
const float puppetSpeed =
|
||||
slot.puppetGraphSpeed >= 0.0F ? slot.puppetGraphSpeed : measuredSpeed;
|
||||
const float locomotionSpeed = std::max(measuredSpeed, puppetSpeed);
|
||||
|
||||
if (locomotionSpeed >= 8.0F) {
|
||||
effectiveRemote.isMoving = true;
|
||||
effectiveRemote.movementSpeed = locomotionSpeed;
|
||||
if (effectiveRemote.animationGraphSpeed < 1.0F) {
|
||||
effectiveRemote.animationGraphSpeed = locomotionSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
||||
slot.appliedAnimationState,
|
||||
*proxy,
|
||||
effectiveRemote);
|
||||
}
|
||||
SyncProxyAnimationFromMotion(slot, *proxy, measuredSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,9 +244,10 @@ namespace
|
||||
DesiredProxyAnimationState BuildDesiredState(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||
{
|
||||
DesiredProxyAnimationState desired{};
|
||||
// movementSpeed can be valid while isMoving is false (sparse network flags).
|
||||
const bool hasLocomotionSpeed = a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
||||
desired.isMoving = (a_remotePlayer.isMoving || hasLocomotionSpeed) && hasLocomotionSpeed;
|
||||
// isMoving is authoritative for idle vs locomotion. movementSpeed alone can stay
|
||||
// stale across packets and was starting proxies in Jog on spawn / after remote stop.
|
||||
desired.isMoving =
|
||||
a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
||||
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
||||
desired.isSneaking = a_remotePlayer.isSneaking;
|
||||
desired.isJumping = a_remotePlayer.isJumping;
|
||||
@@ -276,6 +277,9 @@ namespace
|
||||
{
|
||||
const auto rawDesired = BuildDesiredState(a_remotePlayer);
|
||||
auto desired = rawDesired;
|
||||
if (!rawDesired.isMoving) {
|
||||
a_state.movingHoldUntil = {};
|
||||
}
|
||||
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
||||
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
||||
// Jump state follows the network value directly. The sender already holds isJumping
|
||||
|
||||
Reference in New Issue
Block a user