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.firstSmoothedMovementLogged = false;
|
||||||
a_slot.remoteStateMovedLogged = false;
|
a_slot.remoteStateMovedLogged = false;
|
||||||
a_slot.lastRemotePlayerSnapshot = a_remotePlayer;
|
a_slot.lastRemotePlayerSnapshot = a_remotePlayer;
|
||||||
|
a_slot.puppetGraphSpeed = -1.0F;
|
||||||
|
if (!a_remotePlayer.isMoving) {
|
||||||
|
F4T::ProxyAnimationSync::ResetAppliedAnimationState(a_slot.appliedAnimationState);
|
||||||
|
}
|
||||||
|
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
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}.",
|
"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;
|
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
|
// Apply smooth per-frame movement for all active proxies
|
||||||
// This is called every frame to interpolate between network updates
|
// This is called every frame to interpolate between network updates
|
||||||
void ApplySmoothFrameMovement()
|
void ApplySmoothFrameMovement()
|
||||||
@@ -5359,12 +5399,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
frameDeltaSeconds,
|
frameDeltaSeconds,
|
||||||
-1.0F,
|
-1.0F,
|
||||||
slot.puppetDirection);
|
slot.puppetDirection);
|
||||||
if (slot.lastRemotePlayerSnapshot) {
|
SyncProxyAnimationFromMotion(slot, *proxy, 0.0F);
|
||||||
F4T::ProxyAnimationSync::ApplyProxyAnimationFromRemoteState(
|
|
||||||
slot.appliedAnimationState,
|
|
||||||
*proxy,
|
|
||||||
*slot.lastRemotePlayerSnapshot);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5395,32 +5430,11 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
slot.puppetGraphSpeed,
|
slot.puppetGraphSpeed,
|
||||||
slot.puppetDirection);
|
slot.puppetDirection);
|
||||||
|
|
||||||
// Drive animation from measured proxy motion every frame. Network isMoving is
|
const float horizontalDelta =
|
||||||
// often false while the proxy is visibly jogging (log: recvMoving=false during
|
std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y));
|
||||||
// active translation). MTBehavior also needs explicit Jog/Run/Walk events.
|
const float measuredSpeed =
|
||||||
if (slot.lastRemotePlayerSnapshot) {
|
frameDeltaSeconds > 0.0F ? horizontalDelta / frameDeltaSeconds : 0.0F;
|
||||||
auto effectiveRemote = *slot.lastRemotePlayerSnapshot;
|
SyncProxyAnimationFromMotion(slot, *proxy, measuredSpeed);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,9 +244,10 @@ namespace
|
|||||||
DesiredProxyAnimationState BuildDesiredState(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
DesiredProxyAnimationState BuildDesiredState(const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||||
{
|
{
|
||||||
DesiredProxyAnimationState desired{};
|
DesiredProxyAnimationState desired{};
|
||||||
// movementSpeed can be valid while isMoving is false (sparse network flags).
|
// isMoving is authoritative for idle vs locomotion. movementSpeed alone can stay
|
||||||
const bool hasLocomotionSpeed = a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
// stale across packets and was starting proxies in Jog on spawn / after remote stop.
|
||||||
desired.isMoving = (a_remotePlayer.isMoving || hasLocomotionSpeed) && hasLocomotionSpeed;
|
desired.isMoving =
|
||||||
|
a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= kIdleSpeedThreshold;
|
||||||
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
||||||
desired.isSneaking = a_remotePlayer.isSneaking;
|
desired.isSneaking = a_remotePlayer.isSneaking;
|
||||||
desired.isJumping = a_remotePlayer.isJumping;
|
desired.isJumping = a_remotePlayer.isJumping;
|
||||||
@@ -276,6 +277,9 @@ namespace
|
|||||||
{
|
{
|
||||||
const auto rawDesired = BuildDesiredState(a_remotePlayer);
|
const auto rawDesired = BuildDesiredState(a_remotePlayer);
|
||||||
auto desired = rawDesired;
|
auto desired = rawDesired;
|
||||||
|
if (!rawDesired.isMoving) {
|
||||||
|
a_state.movingHoldUntil = {};
|
||||||
|
}
|
||||||
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
||||||
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
||||||
// Jump state follows the network value directly. The sender already holds isJumping
|
// Jump state follows the network value directly. The sender already holds isJumping
|
||||||
|
|||||||
Reference in New Issue
Block a user