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,7 +23,9 @@ both send and receive paths:
|
||||
9. The plugin and fake client remove disconnected players from their remote
|
||||
player tables.
|
||||
10. On the game-thread update path, the plugin reads a copied remote-player
|
||||
snapshot and moves the single placed proxy actor in `F4TTestCell01`.
|
||||
snapshot and moves the single placed proxy actor in `F4TTestCell01`,
|
||||
smoothing normal movement and snapping cell changes, worldspace changes, and
|
||||
teleports.
|
||||
|
||||
The Fallout 4 plugin still does not dynamically spawn remote actors.
|
||||
|
||||
@@ -102,7 +104,6 @@ update path.
|
||||
|
||||
- Remote actor spawning
|
||||
- Multiple proxy actors
|
||||
- Interpolation or smoothing
|
||||
- Combat sync
|
||||
- Quest sync
|
||||
- Inventory sync
|
||||
|
||||
@@ -644,3 +644,41 @@ Use this format for future updates:
|
||||
- Build the plugin with `xmake build`.
|
||||
- Run the single-client, two-client, and disconnect tests in `F4TTestCell01`.
|
||||
|
||||
## 2026-06-02
|
||||
|
||||
### What Changed
|
||||
|
||||
- Added simple position smoothing for normal remote proxy movement.
|
||||
- Kept proxy actor movement on the `F4TProxyActorController` game-thread update
|
||||
path using copied remote-player snapshots.
|
||||
- Kept the single-proxy, lowest-`playerId` selection behavior.
|
||||
- Added snap movement for `cell_change`, `worldspace_change`, and `teleport`
|
||||
remote transforms.
|
||||
- Left rotation snapping in place with a TODO for wrapped angle smoothing.
|
||||
- Left the proxy idle at its last position when the represented remote player
|
||||
disconnects, disappears, or leaves the same cell.
|
||||
- Added readable one-shot and throttled logs for smoothing, first smoothed
|
||||
movement, special movement snaps, disappearance, and same-cell mismatch idle
|
||||
behavior.
|
||||
|
||||
### What Worked
|
||||
|
||||
- Dynamic spawning and multiple proxy actors remain out of scope.
|
||||
- Networking still only updates plain remote-player state and does not touch
|
||||
Fallout 4 actors or references.
|
||||
|
||||
### What Broke
|
||||
|
||||
- Not tested in-game yet.
|
||||
|
||||
### Notes
|
||||
|
||||
- Hiding or disabling the proxy actor is still deferred. The current safe
|
||||
behavior is to leave the placed proxy idle at its last valid position.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Build the plugin with `xmake build`.
|
||||
- Run the single-client, two-client, special-movement, disconnect, and cell-leave
|
||||
tests in `F4TTestCell01`.
|
||||
|
||||
|
||||
+2
-1
@@ -38,6 +38,8 @@ This means the plugin should be able to:
|
||||
* Store remote player state internally
|
||||
* Move the single placed test proxy actor from remote player state on the game
|
||||
thread without dynamically spawning actors
|
||||
* Smooth normal proxy movement and snap cell changes, worldspace changes, and
|
||||
teleports safely
|
||||
|
||||
## Not Included Yet
|
||||
|
||||
@@ -45,7 +47,6 @@ The plugin does not currently include:
|
||||
|
||||
* Dynamic actor spawning
|
||||
* Multiple proxy actors
|
||||
* Interpolation or smoothing
|
||||
* Remote player spawning
|
||||
* Combat sync
|
||||
* Quest sync
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,12 +72,14 @@ player exists.
|
||||
- The fake client removes disconnected remote players from its state table.
|
||||
- The plugin moves the single placed test proxy actor from a thread-safe
|
||||
remote-player snapshot on the game-thread update path.
|
||||
- Normal proxy movement is smoothed with a simple position lerp, while
|
||||
`cell_change`, `worldspace_change`, and `teleport` transforms snap directly to
|
||||
avoid slow movement across large discontinuities.
|
||||
|
||||
No Fallout 4 remote actor is dynamically spawned yet.
|
||||
|
||||
## Planned Later
|
||||
|
||||
- Add interpolation or smoothing after snap movement is proven stable.
|
||||
- Add one proxy actor per remote player after the single-proxy test is stable.
|
||||
- Dynamically spawn remote actors only after placed-proxy movement is stable.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user