Add position smoothing and movement snapping
Introduce simple position smoothing for normal proxy movement and snap special movements. Adds kProxyPositionLerpAlpha, Lerp/LerpPosition helpers, and ShouldSnapRemoteMovement to lerp proxy position on normal updates while snapping on cell_change, worldspace_change, and teleport. Update MoveProxyToRemotePlayer to use lerp or snap, keep heading snapping (TODO: wrapped angle smoothing), and add throttled/readable logs for smoothing start, first smoothed movement, snaps, and idle-on-cell-mismatch. Minor messaging change on disconnect to leave proxy idle at last position. Update docs (architecture, setup, protocol) and dev log to reflect the new behavior and testing notes; dynamic spawning/multiple proxies remain out of scope.
This commit is contained in:
@@ -23,6 +23,7 @@ namespace
|
||||
constexpr auto kTestPluginName = "Fallout4Together_Test.esp";
|
||||
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
||||
constexpr auto kProxyOffsetX = 150.0F;
|
||||
constexpr float kProxyPositionLerpAlpha = 0.25F;
|
||||
constexpr auto kMovementInterval = 200ms;
|
||||
constexpr auto kWarningLogInterval = 5s;
|
||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||
@@ -47,6 +48,8 @@ namespace
|
||||
bool g_safetyOffsetMovedLogged = false;
|
||||
bool g_remoteStateMovementStartedLogged = false;
|
||||
bool g_remoteStateMovedLogged = false;
|
||||
bool g_proxySmoothingEnabledLogged = false;
|
||||
bool g_firstSmoothedMovementLogged = false;
|
||||
auto g_lastMovementTime = std::chrono::steady_clock::time_point{};
|
||||
std::optional<std::uint32_t> g_representedPlayerId;
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
|
||||
@@ -102,6 +105,25 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
float Lerp(float a_current, float a_target, float a_alpha)
|
||||
{
|
||||
return a_current + ((a_target - a_current) * a_alpha);
|
||||
}
|
||||
|
||||
RE::NiPoint3 LerpPosition(const RE::NiPoint3& a_current, const RE::NiPoint3& a_target, float a_alpha)
|
||||
{
|
||||
return {
|
||||
Lerp(a_current.x, a_target.x, a_alpha),
|
||||
Lerp(a_current.y, a_target.y, a_alpha),
|
||||
Lerp(a_current.z, a_target.z, a_alpha)
|
||||
};
|
||||
}
|
||||
|
||||
bool ShouldSnapRemoteMovement(std::string_view a_movementType)
|
||||
{
|
||||
return a_movementType == "cell_change" || a_movementType == "worldspace_change" || a_movementType == "teleport";
|
||||
}
|
||||
|
||||
bool HasEditorId(const RE::TESForm& a_form, std::string_view a_editorId)
|
||||
{
|
||||
const auto* editorId = a_form.GetFormEditorID();
|
||||
@@ -392,10 +414,11 @@ namespace
|
||||
});
|
||||
if (!representedPlayerStillExists) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Represented remote player {} disconnected or disappeared from state; stopping proxy movement. TODO: hide or disable the proxy actor after disconnect.",
|
||||
"Represented remote player {} disconnected or disappeared from state; leaving proxy actor idle at its last position. TODO: hide or disable the proxy actor after disconnect.",
|
||||
*g_representedPlayerId));
|
||||
g_representedPlayerId.reset();
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,10 +449,17 @@ namespace
|
||||
if (g_representedPlayerId != selectedPlayer->playerId) {
|
||||
g_representedPlayerId = selectedPlayer->playerId;
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
if (!g_remoteStateMovementStartedLogged) {
|
||||
LogInfoWithLocalPlayerPrefix("Remote-state-driven proxy movement started.");
|
||||
g_remoteStateMovementStartedLogged = true;
|
||||
}
|
||||
if (!g_proxySmoothingEnabledLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy actor smoothing enabled for normal movement with position lerp alpha {:.2f}. Rotation still snaps; TODO: add wrapped angle smoothing later.",
|
||||
kProxyPositionLerpAlpha));
|
||||
g_proxySmoothingEnabledLogged = true;
|
||||
}
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy actor {} is representing remote player {}.",
|
||||
kProxyEditorId,
|
||||
@@ -458,14 +488,41 @@ namespace
|
||||
|
||||
void MoveProxyToRemotePlayer(RE::Actor& a_proxy, const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
auto targetPosition = a_proxy.GetPosition();
|
||||
RE::NiPoint3 targetPosition = a_proxy.GetPosition();
|
||||
targetPosition.x = a_remotePlayer.x;
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
|
||||
a_proxy.SetPosition(targetPosition, true);
|
||||
const auto shouldSnap = ShouldSnapRemoteMovement(a_remotePlayer.movementType);
|
||||
const auto nextPosition = shouldSnap ? targetPosition : LerpPosition(a_proxy.GetPosition(), targetPosition, kProxyPositionLerpAlpha);
|
||||
|
||||
a_proxy.SetPosition(nextPosition, true);
|
||||
// TODO: Smooth rotation with wrapped angle interpolation. Heading snaps for now
|
||||
// to avoid wraparound bugs near the angle boundary.
|
||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||
|
||||
if (shouldSnap) {
|
||||
LogThrottledInfo(
|
||||
"proxy_snap_" + std::to_string(a_remotePlayer.playerId) + "_" + a_remotePlayer.movementType,
|
||||
std::format(
|
||||
"Snapped proxy actor {} to remote player {} because movementType={} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
kProxyEditorId,
|
||||
a_remotePlayer.playerId,
|
||||
a_remotePlayer.movementType,
|
||||
a_remotePlayer.x,
|
||||
a_remotePlayer.y,
|
||||
a_remotePlayer.z));
|
||||
} else if (!g_firstSmoothedMovementLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"First smoothed proxy movement for remote player {}: current moves toward target X={:.2f}, Y={:.2f}, Z={:.2f} with alpha {:.2f}.",
|
||||
a_remotePlayer.playerId,
|
||||
a_remotePlayer.x,
|
||||
a_remotePlayer.y,
|
||||
a_remotePlayer.z,
|
||||
kProxyPositionLerpAlpha));
|
||||
g_firstSmoothedMovementLogged = true;
|
||||
}
|
||||
|
||||
if (!g_remoteStateMovedLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Moved proxy actor {} from remote player {} state for the first time: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}.",
|
||||
@@ -525,6 +582,12 @@ namespace F4T::ProxyActorController
|
||||
}
|
||||
|
||||
if (!IsRemotePlayerInSameLocation(*selectedRemotePlayer, *player, *parentCell)) {
|
||||
LogThrottledInfo(
|
||||
"proxy_idle_cell_mismatch_" + std::to_string(selectedRemotePlayer->playerId),
|
||||
std::format(
|
||||
"Proxy actor {} is idle because represented remote player {} is not in the same cell. TODO: hide or disable the proxy actor after cell leave.",
|
||||
kProxyEditorId,
|
||||
selectedRemotePlayer->playerId));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user