Throttle transform sends and smooth proxy movement
Separate normal transform send cadence from movement logging and improve proxy smoothing. Key changes: - Polling/send logic (plugin/src/main.cpp): introduced distinct send vs log intervals (100ms send, 1s log), lowered send thresholds (≈3.0 units position, 0.02 rad rotation), renamed/clarified functions, and added special movement types (worldspace_change, cell_change, teleport) that bypass the send gate and are logged immediately. Normal movement targets roughly 10 Hz while moving to avoid packet-per-frame traffic. - Proxy controller (plugin/src/F4TProxyActorController.cpp): tuned proxy position lerp alpha to 0.15 for smoother motion, renamed movement-interval variables for clarity, and removed the generic per-update movement gate so normal proxy visual updates are applied each game update while only the safety-offset path is throttled. - Documentation updates (docs/architecture.md, docs/dev-log.md, protocol/player-sync.md): describe the new send/log separation, cadence and thresholds, smoothing behavior, and recorded dev-log about smoothing tests and results. These changes reduce network noise, produce smoother remote visuals, and keep readable local movement logs while preserving immediate sends for large/categorical movement changes.
This commit is contained in:
@@ -23,8 +23,8 @@ 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 float kProxyPositionLerpAlpha = 0.15F;
|
||||
constexpr auto kSafetyOffsetMovementInterval = 200ms;
|
||||
constexpr auto kWarningLogInterval = 5s;
|
||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace
|
||||
bool g_remoteStateMovedLogged = false;
|
||||
bool g_proxySmoothingEnabledLogged = false;
|
||||
bool g_firstSmoothedMovementLogged = false;
|
||||
auto g_lastMovementTime = std::chrono::steady_clock::time_point{};
|
||||
auto g_lastSafetyOffsetMovementTime = 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;
|
||||
|
||||
@@ -393,14 +393,15 @@ namespace
|
||||
return proxy;
|
||||
}
|
||||
|
||||
bool ShouldMoveProxy()
|
||||
bool ShouldMoveSafetyOffsetProxy()
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (g_lastMovementTime.time_since_epoch().count() != 0 && now - g_lastMovementTime < kMovementInterval) {
|
||||
if (g_lastSafetyOffsetMovementTime.time_since_epoch().count() != 0 &&
|
||||
now - g_lastSafetyOffsetMovementTime < kSafetyOffsetMovementInterval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_lastMovementTime = now;
|
||||
g_lastSafetyOffsetMovementTime = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -563,7 +564,7 @@ namespace F4T::ProxyActorController
|
||||
|
||||
if constexpr (kProxyMovementMode == ProxyMovementMode::kSafetyOffsetTest) {
|
||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||
if (!proxy || !ShouldMoveProxy()) {
|
||||
if (!proxy || !ShouldMoveSafetyOffsetProxy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -577,7 +578,7 @@ namespace F4T::ProxyActorController
|
||||
}
|
||||
|
||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||
if (!proxy || !ShouldMoveProxy()) {
|
||||
if (!proxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+62
-34
@@ -24,10 +24,10 @@ namespace
|
||||
std::uint32_t worldspaceId;
|
||||
};
|
||||
|
||||
constexpr auto kPositionLogThreshold = 10.0F;
|
||||
constexpr auto kRotationLogThresholdDegrees = 1.0F;
|
||||
constexpr auto kRotationLogThresholdRadians = kRotationLogThresholdDegrees * 3.14159265358979323846F / 180.0F;
|
||||
constexpr auto kMinimumLogInterval = 1s;
|
||||
constexpr auto kTransformSendInterval = 100ms;
|
||||
constexpr auto kTransformLogInterval = 1s;
|
||||
constexpr auto kTransformSendPositionThreshold = 3.0F;
|
||||
constexpr auto kTransformSendRotationThreshold = 0.02F;
|
||||
constexpr auto kTeleportDistanceThreshold = 5000.0F;
|
||||
|
||||
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||
@@ -99,7 +99,7 @@ namespace
|
||||
return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);
|
||||
}
|
||||
|
||||
bool HasTransformChanged(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
||||
bool HasMeaningfulTransformDelta(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
||||
{
|
||||
auto deltaAngleZ = a_current.angleZ - a_previous.angleZ;
|
||||
|
||||
@@ -113,10 +113,10 @@ namespace
|
||||
}
|
||||
|
||||
const auto squaredPositionDelta = GetSquaredPositionDistance(a_previous, a_current);
|
||||
constexpr auto squaredPositionThreshold = kPositionLogThreshold * kPositionLogThreshold;
|
||||
constexpr auto squaredPositionThreshold = kTransformSendPositionThreshold * kTransformSendPositionThreshold;
|
||||
|
||||
return squaredPositionDelta > squaredPositionThreshold ||
|
||||
deltaAngleZ > kRotationLogThresholdRadians || deltaAngleZ < -kRotationLogThresholdRadians;
|
||||
deltaAngleZ > kTransformSendRotationThreshold || deltaAngleZ < -kTransformSendRotationThreshold;
|
||||
}
|
||||
|
||||
bool HasCellChanged(const PlayerLocation& a_previous, const PlayerLocation& a_current)
|
||||
@@ -146,7 +146,37 @@ namespace
|
||||
a_transform.angleZ));
|
||||
}
|
||||
|
||||
void CheckAndLogPlayerPositionChange()
|
||||
const char* GetSpecialMovementType(
|
||||
const PlayerLocation& a_previousLocation,
|
||||
const PlayerLocation& a_currentLocation,
|
||||
const PlayerTransform& a_previousTransform,
|
||||
const PlayerTransform& a_currentTransform)
|
||||
{
|
||||
if (HasWorldspaceChanged(a_previousLocation, a_currentLocation)) {
|
||||
return "worldspace_change";
|
||||
}
|
||||
if (HasCellChanged(a_previousLocation, a_currentLocation)) {
|
||||
return "cell_change";
|
||||
}
|
||||
if (HasTeleportDistance(a_previousTransform, a_currentTransform)) {
|
||||
return "teleport";
|
||||
}
|
||||
|
||||
return "normal";
|
||||
}
|
||||
|
||||
void LogSpecialMovement(std::string_view a_movementType)
|
||||
{
|
||||
if (a_movementType == "worldspace_change") {
|
||||
LogInfoWithLocalPlayerPrefix("Detected worldspace change. Sending immediate transform update.");
|
||||
} else if (a_movementType == "cell_change") {
|
||||
LogInfoWithLocalPlayerPrefix("Detected cell change. Sending immediate transform update.");
|
||||
} else if (a_movementType == "teleport") {
|
||||
LogInfoWithLocalPlayerPrefix("Detected teleport/large position jump. Sending immediate transform update.");
|
||||
}
|
||||
}
|
||||
|
||||
void PollLocalPlayerTransform()
|
||||
{
|
||||
const auto* player = TryGetLocalPlayer();
|
||||
if (!player) {
|
||||
@@ -161,42 +191,33 @@ namespace
|
||||
static bool hasLastTransform = false;
|
||||
static PlayerTransform lastSentTransform{};
|
||||
static PlayerLocation lastKnownLocation{};
|
||||
static auto lastLogTime = std::chrono::steady_clock::now();
|
||||
static auto lastSendTime = std::chrono::steady_clock::time_point{};
|
||||
static auto lastLogTime = std::chrono::steady_clock::time_point{};
|
||||
|
||||
const auto currentTransform = GetPlayerTransform(*player);
|
||||
const auto currentLocation = GetPlayerLocation(*player);
|
||||
const auto currentTime = std::chrono::steady_clock::now();
|
||||
if (!hasLastTransform) {
|
||||
lastSentTransform = currentTransform;
|
||||
lastKnownLocation = currentLocation;
|
||||
lastSendTime = currentTime;
|
||||
lastLogTime = currentTime;
|
||||
hasLastTransform = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* movementType = "normal";
|
||||
if (HasWorldspaceChanged(lastKnownLocation, currentLocation)) {
|
||||
LogInfoWithLocalPlayerPrefix("Detected worldspace change. Sending immediate transform update.");
|
||||
movementType = "worldspace_change";
|
||||
} else if (HasCellChanged(lastKnownLocation, currentLocation)) {
|
||||
LogInfoWithLocalPlayerPrefix("Detected cell change. Sending immediate transform update.");
|
||||
movementType = "cell_change";
|
||||
} else if (HasTeleportDistance(lastSentTransform, currentTransform)) {
|
||||
LogInfoWithLocalPlayerPrefix("Detected teleport/large position jump. Sending immediate transform update.");
|
||||
movementType = "teleport";
|
||||
}
|
||||
|
||||
const auto* movementType =
|
||||
GetSpecialMovementType(lastKnownLocation, currentLocation, lastSentTransform, currentTransform);
|
||||
const auto isSpecialMovement = movementType != std::string_view{ "normal" };
|
||||
if (!isSpecialMovement && !HasTransformChanged(lastSentTransform, currentTransform)) {
|
||||
if (!isSpecialMovement && !HasMeaningfulTransformDelta(lastSentTransform, currentTransform)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto currentTime = std::chrono::steady_clock::now();
|
||||
if (!isSpecialMovement && currentTime - lastLogTime < kMinimumLogInterval) {
|
||||
if (!isSpecialMovement && currentTime - lastSendTime < kTransformSendInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Player transforms can change by tiny amounts every frame. Throttling keeps the
|
||||
// readout useful for debugging movement without flooding Fallout4Together.log or
|
||||
// turning this first local networking test into a packet every frame.
|
||||
// Normal movement sends more often than logs, but still avoids packet-per-frame noise.
|
||||
const auto sentTransform = F4T::Networking::SendTransformPacket(
|
||||
currentTransform.x,
|
||||
currentTransform.y,
|
||||
@@ -208,11 +229,18 @@ namespace
|
||||
currentLocation.hasWorldspaceId,
|
||||
currentLocation.worldspaceId);
|
||||
if (sentTransform) {
|
||||
LogPlayerTransform(currentTransform);
|
||||
lastSentTransform = currentTransform;
|
||||
lastKnownLocation = currentLocation;
|
||||
lastSendTime = currentTime;
|
||||
|
||||
if (isSpecialMovement) {
|
||||
LogSpecialMovement(movementType);
|
||||
lastLogTime = currentTime;
|
||||
} else if (currentTime - lastLogTime >= kTransformLogInterval) {
|
||||
LogPlayerTransform(currentTransform);
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
}
|
||||
lastSentTransform = currentTransform;
|
||||
lastKnownLocation = currentLocation;
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
|
||||
void StartPlayerPositionPolling()
|
||||
@@ -224,14 +252,14 @@ namespace
|
||||
|
||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||
if (!taskInterface) {
|
||||
LogWarningWithLocalPlayerPrefix("Failed to get F4SE task interface; player position changes will not be logged.");
|
||||
LogWarningWithLocalPlayerPrefix("Failed to get F4SE task interface; player position changes will not be sent or logged.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The player reference may not exist at plugin load time. A permanent task lets us
|
||||
// check on the game thread after load, while the logging gate keeps updates readable.
|
||||
// check on the game thread after load, while separate send and log gates keep updates useful.
|
||||
taskInterface->AddTaskPermanent([]() {
|
||||
CheckAndLogPlayerPositionChange();
|
||||
PollLocalPlayerTransform();
|
||||
F4T::ProxyActorController::Update();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user