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:
2026-06-05 15:52:01 +12:00
parent a137821406
commit f4b37b6d12
2 changed files with 53 additions and 35 deletions
+46 -32
View File
@@ -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);
}
const float horizontalDelta =
std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y));
const float measuredSpeed =
frameDeltaSeconds > 0.0F ? horizontalDelta / frameDeltaSeconds : 0.0F;
SyncProxyAnimationFromMotion(slot, *proxy, measuredSpeed);
}
}
}
+7 -3
View File
@@ -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